@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.
@@ -46,19 +46,20 @@ __export(genre_tree_exports, {
46
46
  module.exports = __toCommonJS(genre_tree_exports);
47
47
 
48
48
  // src/genre-tree/GenreTreeView.tsx
49
- var import_react16 = require("react");
49
+ var import_react17 = require("react");
50
50
  var import_react_query3 = require("@tanstack/react-query");
51
51
  var import_lucide_react11 = require("lucide-react");
52
52
  var import_lucide_react12 = require("lucide-react");
53
53
  var import_ui16 = require("@behindthemusictree/ui");
54
54
  var import_genre_tree_view4 = require("@behindthemusictree/genre-tree-view");
55
- var import_genre_tree21 = require("@behindthemusictree/app-kit/genre-tree");
55
+ var import_genre_tree27 = require("@behindthemusictree/app-kit/genre-tree");
56
56
  var import_popup5 = require("@behindthemusictree/app-kit/popup");
57
57
 
58
58
  // src/genre-tree/playlist-tree/size-metric.ts
59
+ var import_genre_tree = require("@behindthemusictree/app-kit/genre-tree");
59
60
  var hasViewsCount = (genrePlaylists) => genrePlaylists.every((genrePlaylist) => genrePlaylist.viewsCount !== void 0);
60
61
  var genrePlaylistSize = (genrePlaylist, metric) => {
61
- if (metric === "songs") return genrePlaylist.tracksCount;
62
+ if (metric === "songs") return (0, import_genre_tree.genrePlaylistTrackCount)(genrePlaylist);
62
63
  if (genrePlaylist.viewsCount === void 0) {
63
64
  throw new Error(`Genre playlist ${genrePlaylist.uuid} has no viewsCount to size by views`);
64
65
  }
@@ -82,6 +83,7 @@ function toTreeNodes(genrePlaylists, sizeMetric) {
82
83
  }
83
84
 
84
85
  // src/genre-tree/playlist-tree/TreePerRoot.tsx
86
+ var import_genre_tree3 = require("@behindthemusictree/app-kit/genre-tree");
85
87
  var import_react3 = require("react");
86
88
  var import_genre_tree_view = require("@behindthemusictree/genre-tree-view");
87
89
  var import_player = require("@behindthemusictree/app-kit/player");
@@ -306,29 +308,28 @@ var import_transport = require("@behindthemusictree/app-kit/transport");
306
308
 
307
309
  // src/genre-tree/playlist-tree/useGenrePlaylistPlayback.ts
308
310
  var import_react2 = require("react");
309
- var import_genre_tree = require("@behindthemusictree/app-kit/genre-tree");
311
+ var import_genre_tree2 = require("@behindthemusictree/app-kit/genre-tree");
310
312
  function useGenrePlaylistPlayback({
311
313
  scope,
312
314
  genrePlaylists,
313
315
  artist = null
314
316
  }) {
315
- const { trackList, playNewTrackListFromGenrePlaylist, playNewTrackListFromArtist } = (0, import_genre_tree.useTrackList)();
317
+ const { trackList, playNewTrackListFromGenrePlaylist } = (0, import_genre_tree2.useTrackList)();
316
318
  const origin = trackList?.origin;
317
319
  const playingNodeId = (0, import_react2.useMemo)(() => {
318
- if (!origin) return null;
319
- if (!artist) return origin.type === import_genre_tree.TrackListOriginType.GENRE_PLAYLIST ? origin.uuid : null;
320
- if (origin.type !== import_genre_tree.TrackListOriginType.ARTIST || origin.uuid !== artist.uuid) return null;
321
- const { genreUuid } = origin;
322
- return genreUuid ? genrePlaylists.find((gp) => gp.criteria?.uuid === genreUuid)?.uuid ?? null : null;
323
- }, [origin, artist, genrePlaylists]);
324
- const play = (0, import_react2.useCallback)(
325
- async (genrePlaylist) => {
326
- if (!artist) return playNewTrackListFromGenrePlaylist(genrePlaylist, scope);
327
- if (!genrePlaylist.criteria) return;
328
- return playNewTrackListFromArtist(artist, scope, { ordering: "title", genreUuid: genrePlaylist.criteria.uuid });
329
- },
330
- [artist, scope, playNewTrackListFromGenrePlaylist, playNewTrackListFromArtist]
331
- );
320
+ if (!origin || origin.type !== import_genre_tree2.TrackListOriginType.GENRE_PLAYLIST || origin.scope !== scope) return null;
321
+ const playlistOrigin = origin;
322
+ if (playlistOrigin.artistUuid !== artist?.uuid) return null;
323
+ const playlist = genrePlaylists.find((item) => item.uuid === origin.uuid) ?? playlistOrigin.criteriaPlaylist;
324
+ const expected = scope === "reference" && playlist && !("criteria" in playlist && playlist.criteria === null) ? (0, import_genre_tree2.defaultGenreMusicSelection)(import_genre_tree2.GenreCatalogueCountsSchema.parse(playlist)) : void 0;
325
+ if (playlistOrigin.selection && expected && (0, import_genre_tree2.genreMusicSelectionKey)(playlistOrigin.selection).join() !== (0, import_genre_tree2.genreMusicSelectionKey)(expected).join()) return null;
326
+ return origin.uuid;
327
+ }, [origin, artist, genrePlaylists, scope]);
328
+ const play = (0, import_react2.useCallback)(async (playlist) => {
329
+ if (artist && !playlist.criteria) return;
330
+ const selection = scope === "reference" && playlist.criteria !== null ? (0, import_genre_tree2.defaultGenreMusicSelection)(import_genre_tree2.GenreCatalogueCountsSchema.parse(playlist)) : void 0;
331
+ return selection || artist ? playNewTrackListFromGenrePlaylist(playlist, scope, { selection, ...artist && { artistUuid: artist.uuid } }) : playNewTrackListFromGenrePlaylist(playlist, scope);
332
+ }, [artist, scope, playNewTrackListFromGenrePlaylist]);
332
333
  return (0, import_react2.useMemo)(() => ({ playingNodeId, play }), [playingNodeId, play]);
333
334
  }
334
335
 
@@ -363,7 +364,7 @@ function GenrePlaylistTreePerRoot({
363
364
  id: genrePlaylist.uuid,
364
365
  parentId: genrePlaylist.parent?.uuid ?? null,
365
366
  name: genrePlaylist.name,
366
- itemCount: genrePlaylist.tracksCount,
367
+ itemCount: (0, import_genre_tree3.genrePlaylistTrackCount)(genrePlaylist),
367
368
  actionable: Boolean(genrePlaylist.criteria),
368
369
  side: genrePlaylist.criteria?.side ?? void 0,
369
370
  wikidataId: genrePlaylist.criteria?.wikidataId ?? null,
@@ -379,7 +380,7 @@ function GenrePlaylistTreePerRoot({
379
380
  setIsPlaying(!isPlaying);
380
381
  return;
381
382
  }
382
- if (genrePlaylist.tracksCount === 0) {
383
+ if ((0, import_genre_tree3.genrePlaylistTrackCount)(genrePlaylist) === 0) {
383
384
  return;
384
385
  }
385
386
  play(genrePlaylist).catch((error) => {
@@ -443,6 +444,7 @@ function GenrePlaylistTreePerRoot({
443
444
  }
444
445
 
445
446
  // src/genre-tree/playlist-tree/TreeWheel.tsx
447
+ var import_genre_tree4 = require("@behindthemusictree/app-kit/genre-tree");
446
448
  var import_react4 = require("react");
447
449
  var import_genre_tree_view2 = require("@behindthemusictree/genre-tree-view");
448
450
  var import_player2 = require("@behindthemusictree/app-kit/player");
@@ -495,7 +497,7 @@ function GenrePlaylistTreeWheel({
495
497
  setIsPlaying(!isPlaying);
496
498
  return;
497
499
  }
498
- if (genrePlaylist.tracksCount === 0) {
500
+ if ((0, import_genre_tree4.genrePlaylistTrackCount)(genrePlaylist) === 0) {
499
501
  return;
500
502
  }
501
503
  play(genrePlaylist).catch((error) => {
@@ -560,6 +562,7 @@ function GenrePlaylistTreeWheel({
560
562
  }
561
563
 
562
564
  // src/genre-tree/playlist-tree/TreeWheelRadialPopCore.tsx
565
+ var import_genre_tree5 = require("@behindthemusictree/app-kit/genre-tree");
563
566
  var import_react5 = require("react");
564
567
  var import_genre_tree_view3 = require("@behindthemusictree/genre-tree-view");
565
568
  var import_player3 = require("@behindthemusictree/app-kit/player");
@@ -604,7 +607,7 @@ function GenrePlaylistTreeWheelRadialPopCore(props) {
604
607
  setIsPlaying(!isPlaying);
605
608
  return;
606
609
  }
607
- if (genrePlaylist.tracksCount === 0) {
610
+ if ((0, import_genre_tree5.genrePlaylistTrackCount)(genrePlaylist) === 0) {
608
611
  return;
609
612
  }
610
613
  play(genrePlaylist).catch((error) => {
@@ -699,10 +702,13 @@ function GenreTreeWheelHandoff({ skeleton, children, onRevealedChange }) {
699
702
  ] });
700
703
  }
701
704
 
705
+ // src/genre-tree/GenreTreeSearch.tsx
706
+ var import_react8 = require("react");
707
+
702
708
  // src/genre-tree/GenreSearch.tsx
703
709
  var import_lucide_react3 = require("lucide-react");
704
710
  var import_ui4 = require("@behindthemusictree/ui");
705
- var import_genre_tree5 = require("@behindthemusictree/app-kit/genre-tree");
711
+ var import_genre_tree9 = require("@behindthemusictree/app-kit/genre-tree");
706
712
 
707
713
  // src/genre-tree/search/SearchBox.tsx
708
714
  var import_react7 = require("react");
@@ -758,10 +764,10 @@ var SEARCH_FAILED_STATUS = "Search failed";
758
764
  var searchStatus = (state, count, noun) => state.isError ? SEARCH_FAILED_STATUS : state.settled && count === 0 ? `No ${noun} found` : "";
759
765
 
760
766
  // src/genre-tree/search/useArtistSearchGroup.tsx
761
- var import_genre_tree2 = require("@behindthemusictree/app-kit/genre-tree");
767
+ var import_genre_tree6 = require("@behindthemusictree/app-kit/genre-tree");
762
768
  var import_jsx_runtime10 = require("react/jsx-runtime");
763
769
  function useArtistSearchGroup(getBaseUrl, text, onSelect, enabled = true) {
764
- const search = (0, import_genre_tree2.useArtistSearch)(getBaseUrl, enabled ? text : "");
770
+ const search = (0, import_genre_tree6.useArtistSearch)(getBaseUrl, enabled ? text : "");
765
771
  if (!enabled) return void 0;
766
772
  return {
767
773
  key: "artists",
@@ -789,17 +795,17 @@ function useArtistSearchGroup(getBaseUrl, text, onSelect, enabled = true) {
789
795
  }
790
796
 
791
797
  // src/genre-tree/search/useSongSearchGroup.tsx
792
- var import_genre_tree3 = require("@behindthemusictree/app-kit/genre-tree");
798
+ var import_genre_tree7 = require("@behindthemusictree/app-kit/genre-tree");
793
799
  var import_jsx_runtime11 = require("react/jsx-runtime");
794
800
  function useSongSearchGroup(getBaseUrl, text, onSelect, enabled = true) {
795
- const search = (0, import_genre_tree3.useSongSearch)(getBaseUrl, enabled ? text : "");
801
+ const search = (0, import_genre_tree7.useSongSearch)(getBaseUrl, enabled ? text : "");
796
802
  if (!enabled) return void 0;
797
803
  return {
798
804
  key: "songs",
799
805
  label: "Songs",
800
806
  items: search.songs.map((song) => ({
801
807
  key: song.uuid,
802
- content: /* @__PURE__ */ (0, import_jsx_runtime11.jsxs)("span", { className: (0, import_genre_tree3.isSongSearchResultPlayable)(song) ? void 0 : "opacity-60", children: [
808
+ content: /* @__PURE__ */ (0, import_jsx_runtime11.jsxs)("span", { className: (0, import_genre_tree7.isSongSearchResultPlayable)(song) ? void 0 : "opacity-60", children: [
803
809
  /* @__PURE__ */ (0, import_jsx_runtime11.jsx)("span", { className: "block truncate font-medium", children: song.title }),
804
810
  /* @__PURE__ */ (0, import_jsx_runtime11.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 ") })
805
811
  ] }),
@@ -811,10 +817,10 @@ function useSongSearchGroup(getBaseUrl, text, onSelect, enabled = true) {
811
817
  }
812
818
 
813
819
  // src/genre-tree/search/useGenreRemoteSearchGroup.tsx
814
- var import_genre_tree4 = require("@behindthemusictree/app-kit/genre-tree");
820
+ var import_genre_tree8 = require("@behindthemusictree/app-kit/genre-tree");
815
821
  var import_jsx_runtime12 = require("react/jsx-runtime");
816
822
  function useGenreRemoteSearchGroup(getBaseUrl, text, onSelect, { key = "genres", label = "Genres", filter, describe, ...options } = {}) {
817
- const search = (0, import_genre_tree4.useGenreRemoteSearch)(getBaseUrl, text, options);
823
+ const search = (0, import_genre_tree8.useGenreRemoteSearch)(getBaseUrl, text, options);
818
824
  const genres = filter ? search.genres.filter(filter) : search.genres;
819
825
  return {
820
826
  key,
@@ -848,7 +854,7 @@ function GenreSearch({
848
854
  extraGroups = [],
849
855
  onFocus
850
856
  }) {
851
- const { query, setQuery, results, canClear, clear, select } = (0, import_genre_tree5.useGenreSearch)({
857
+ const { query, setQuery, results, canClear, clear, select } = (0, import_genre_tree9.useGenreSearch)({
852
858
  genrePlaylists,
853
859
  onSelect,
854
860
  selectedName,
@@ -899,7 +905,47 @@ function GenreSearch({
899
905
  ) });
900
906
  }
901
907
 
908
+ // src/genre-tree/GenreTreeSearch.tsx
909
+ var import_jsx_runtime14 = require("react/jsx-runtime");
910
+ function GenreTreeSearch({
911
+ artistBaseUrl,
912
+ songBaseUrl,
913
+ onArtistSelect,
914
+ onSongSelect,
915
+ artistSearch,
916
+ songSearch,
917
+ SearchGroups,
918
+ extraGroups = [],
919
+ onQueryChange,
920
+ onFocus,
921
+ ...props
922
+ }) {
923
+ const [query, setQuery] = (0, import_react8.useState)("");
924
+ const [focused, setFocused] = (0, import_react8.useState)(false);
925
+ const deferredQuery = (0, import_react8.useDeferredValue)(query);
926
+ const artistGroup = useArtistSearchGroup(artistBaseUrl, deferredQuery, onArtistSelect, artistSearch);
927
+ const songGroup = useSongSearchGroup(songBaseUrl, deferredQuery, onSongSelect, songSearch);
928
+ const render = (hostGroups) => /* @__PURE__ */ (0, import_jsx_runtime14.jsx)(
929
+ GenreSearch,
930
+ {
931
+ ...props,
932
+ onQueryChange: (text) => {
933
+ setQuery(text);
934
+ onQueryChange?.(text);
935
+ },
936
+ onFocus: () => {
937
+ setFocused(true);
938
+ onFocus?.();
939
+ },
940
+ extraGroups: [...extraGroups, ...hostGroups, artistGroup, songGroup].filter((group) => group !== void 0)
941
+ }
942
+ );
943
+ return SearchGroups ? /* @__PURE__ */ (0, import_jsx_runtime14.jsx)(SearchGroups, { query: deferredQuery, focused, children: render }) : render([]);
944
+ }
945
+
902
946
  // src/genre-tree/GenreInfoPanelBody.tsx
947
+ var import_genre_tree18 = require("@behindthemusictree/app-kit/genre-tree");
948
+ var import_track_noun3 = require("@behindthemusictree/app-kit/track-noun");
903
949
  var import_player6 = require("@behindthemusictree/app-kit/player");
904
950
 
905
951
  // src/genre-tree/GenreActionBand.tsx
@@ -910,25 +956,25 @@ var import_lucide_react6 = require("lucide-react");
910
956
  var import_popup3 = require("@behindthemusictree/app-kit/popup");
911
957
 
912
958
  // src/genre-tree/SharePopup.tsx
913
- var import_react9 = require("react");
959
+ var import_react10 = require("react");
914
960
  var import_lucide_react5 = require("lucide-react");
915
961
  var import_fa = require("react-icons/fa");
916
962
  var import_si = require("react-icons/si");
917
963
 
918
964
  // src/popup/Toast.tsx
919
- var import_react8 = require("react");
965
+ var import_react9 = require("react");
920
966
  var import_react_dom2 = require("react-dom");
921
- var import_jsx_runtime14 = require("react/jsx-runtime");
967
+ var import_jsx_runtime15 = require("react/jsx-runtime");
922
968
  function Toast({ message, onClose, durationMs = 2200 }) {
923
- const [mounted, setMounted] = (0, import_react8.useState)(false);
924
- (0, import_react8.useEffect)(() => {
969
+ const [mounted, setMounted] = (0, import_react9.useState)(false);
970
+ (0, import_react9.useEffect)(() => {
925
971
  setMounted(true);
926
972
  const timeout = window.setTimeout(onClose, durationMs);
927
973
  return () => window.clearTimeout(timeout);
928
974
  }, [durationMs, onClose]);
929
975
  if (!mounted) return null;
930
976
  return (0, import_react_dom2.createPortal)(
931
- /* @__PURE__ */ (0, import_jsx_runtime14.jsx)("div", { className: "pointer-events-none fixed inset-x-0 bottom-8 z-[60] flex justify-center px-4", children: /* @__PURE__ */ (0, import_jsx_runtime14.jsx)(
977
+ /* @__PURE__ */ (0, import_jsx_runtime15.jsx)("div", { className: "pointer-events-none fixed inset-x-0 bottom-8 z-[60] flex justify-center px-4", children: /* @__PURE__ */ (0, import_jsx_runtime15.jsx)(
932
978
  "div",
933
979
  {
934
980
  role: "status",
@@ -943,12 +989,12 @@ function Toast({ message, onClose, durationMs = 2200 }) {
943
989
  }
944
990
 
945
991
  // src/genre-tree/ShareDestinationButton.tsx
946
- var import_jsx_runtime15 = require("react/jsx-runtime");
992
+ var import_jsx_runtime16 = require("react/jsx-runtime");
947
993
  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";
948
994
  function Contents({ icon, label }) {
949
- return /* @__PURE__ */ (0, import_jsx_runtime15.jsxs)(import_jsx_runtime15.Fragment, { children: [
950
- /* @__PURE__ */ (0, import_jsx_runtime15.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 }),
951
- /* @__PURE__ */ (0, import_jsx_runtime15.jsx)("span", { className: "max-w-full truncate", children: label })
995
+ return /* @__PURE__ */ (0, import_jsx_runtime16.jsxs)(import_jsx_runtime16.Fragment, { children: [
996
+ /* @__PURE__ */ (0, import_jsx_runtime16.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 }),
997
+ /* @__PURE__ */ (0, import_jsx_runtime16.jsx)("span", { className: "max-w-full truncate", children: label })
952
998
  ] });
953
999
  }
954
1000
  function ShareDestinationButton({
@@ -960,29 +1006,29 @@ function ShareDestinationButton({
960
1006
  ariaLabel
961
1007
  }) {
962
1008
  if (href) {
963
- return /* @__PURE__ */ (0, import_jsx_runtime15.jsx)("a", { href, target: "_blank", rel: "noopener noreferrer", "aria-label": ariaLabel, className, children: /* @__PURE__ */ (0, import_jsx_runtime15.jsx)(Contents, { icon, label }) });
1009
+ return /* @__PURE__ */ (0, import_jsx_runtime16.jsx)("a", { href, target: "_blank", rel: "noopener noreferrer", "aria-label": ariaLabel, className, children: /* @__PURE__ */ (0, import_jsx_runtime16.jsx)(Contents, { icon, label }) });
964
1010
  }
965
- return /* @__PURE__ */ (0, import_jsx_runtime15.jsx)("button", { type: "button", onClick, disabled, "aria-label": ariaLabel, className, children: /* @__PURE__ */ (0, import_jsx_runtime15.jsx)(Contents, { icon, label }) });
1011
+ return /* @__PURE__ */ (0, import_jsx_runtime16.jsx)("button", { type: "button", onClick, disabled, "aria-label": ariaLabel, className, children: /* @__PURE__ */ (0, import_jsx_runtime16.jsx)(Contents, { icon, label }) });
966
1012
  }
967
1013
 
968
1014
  // src/genre-tree/SharePreviewCard.tsx
969
1015
  var import_lucide_react4 = require("lucide-react");
970
- var import_jsx_runtime16 = require("react/jsx-runtime");
1016
+ var import_jsx_runtime17 = require("react/jsx-runtime");
971
1017
  function SharePreviewCard({ target, url }) {
972
1018
  const Icon = { genre: import_lucide_react4.Music2, artist: import_lucide_react4.UserRound, song: import_lucide_react4.Music }[target.kind];
973
1019
  const typeLabel = { genre: "Genre", artist: "Artist", song: "Song" }[target.kind];
974
- return /* @__PURE__ */ (0, import_jsx_runtime16.jsx)("div", { className: "rounded-3xl bg-[var(--color-surface-muted)] p-4", children: /* @__PURE__ */ (0, import_jsx_runtime16.jsxs)("div", { className: "flex min-h-32 overflow-hidden rounded-2xl bg-[var(--color-surface)] shadow-lg", children: [
975
- /* @__PURE__ */ (0, import_jsx_runtime16.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_runtime16.jsx)(Icon, { "aria-hidden": "true", className: "h-10 w-10" }) }),
976
- /* @__PURE__ */ (0, import_jsx_runtime16.jsxs)("div", { className: "flex min-w-0 flex-1 flex-col justify-center gap-1 p-4", children: [
977
- /* @__PURE__ */ (0, import_jsx_runtime16.jsx)("span", { className: "text-xs font-medium uppercase tracking-wide text-[var(--color-text-muted)]", children: typeLabel }),
978
- /* @__PURE__ */ (0, import_jsx_runtime16.jsx)("span", { className: "truncate text-base font-semibold text-[var(--color-text)]", title: target.name, children: target.name }),
979
- /* @__PURE__ */ (0, import_jsx_runtime16.jsx)("span", { className: "truncate text-sm text-[var(--color-primary)]", title: url, children: url })
1020
+ return /* @__PURE__ */ (0, import_jsx_runtime17.jsx)("div", { className: "rounded-3xl bg-[var(--color-surface-muted)] p-4", children: /* @__PURE__ */ (0, import_jsx_runtime17.jsxs)("div", { className: "flex min-h-32 overflow-hidden rounded-2xl bg-[var(--color-surface)] shadow-lg", children: [
1021
+ /* @__PURE__ */ (0, import_jsx_runtime17.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_runtime17.jsx)(Icon, { "aria-hidden": "true", className: "h-10 w-10" }) }),
1022
+ /* @__PURE__ */ (0, import_jsx_runtime17.jsxs)("div", { className: "flex min-w-0 flex-1 flex-col justify-center gap-1 p-4", children: [
1023
+ /* @__PURE__ */ (0, import_jsx_runtime17.jsx)("span", { className: "text-xs font-medium uppercase tracking-wide text-[var(--color-text-muted)]", children: typeLabel }),
1024
+ /* @__PURE__ */ (0, import_jsx_runtime17.jsx)("span", { className: "truncate text-base font-semibold text-[var(--color-text)]", title: target.name, children: target.name }),
1025
+ /* @__PURE__ */ (0, import_jsx_runtime17.jsx)("span", { className: "truncate text-sm text-[var(--color-primary)]", title: url, children: url })
980
1026
  ] })
981
1027
  ] }) });
982
1028
  }
983
1029
 
984
1030
  // src/genre-tree/SharePopup.tsx
985
- var import_jsx_runtime17 = require("react/jsx-runtime");
1031
+ var import_jsx_runtime18 = require("react/jsx-runtime");
986
1032
  function resolveAbsoluteUrl(url) {
987
1033
  try {
988
1034
  const parsed = new URL(url, window.location.href);
@@ -1015,20 +1061,20 @@ async function copyText(text) {
1015
1061
  }
1016
1062
  }
1017
1063
  function SharePopup({ target, url, onClose }) {
1018
- const [absoluteUrl, setAbsoluteUrl] = (0, import_react9.useState)(url);
1019
- const [copyStatus, setCopyStatus] = (0, import_react9.useState)("idle");
1020
- const [canUseNativeShare, setCanUseNativeShare] = (0, import_react9.useState)(false);
1021
- const [shareError, setShareError] = (0, import_react9.useState)(null);
1022
- const [toastId, setToastId] = (0, import_react9.useState)(0);
1023
- (0, import_react9.useEffect)(() => {
1064
+ const [absoluteUrl, setAbsoluteUrl] = (0, import_react10.useState)(url);
1065
+ const [copyStatus, setCopyStatus] = (0, import_react10.useState)("idle");
1066
+ const [canUseNativeShare, setCanUseNativeShare] = (0, import_react10.useState)(false);
1067
+ const [shareError, setShareError] = (0, import_react10.useState)(null);
1068
+ const [toastId, setToastId] = (0, import_react10.useState)(0);
1069
+ (0, import_react10.useEffect)(() => {
1024
1070
  setAbsoluteUrl(resolveAbsoluteUrl(url) ?? "");
1025
1071
  setCopyStatus("idle");
1026
1072
  setShareError(null);
1027
1073
  }, [url]);
1028
- (0, import_react9.useEffect)(() => {
1074
+ (0, import_react10.useEffect)(() => {
1029
1075
  setCanUseNativeShare(typeof navigator !== "undefined" && typeof navigator.share === "function");
1030
1076
  }, []);
1031
- const destinationUrls = (0, import_react9.useMemo)(() => {
1077
+ const destinationUrls = (0, import_react10.useMemo)(() => {
1032
1078
  if (!absoluteUrl) return null;
1033
1079
  const message = `${target.name} \u2014 ${absoluteUrl}`;
1034
1080
  return {
@@ -1046,7 +1092,7 @@ function SharePopup({ target, url, onClose }) {
1046
1092
  setCopyStatus(copied ? "idle" : "failed");
1047
1093
  if (copied) setToastId((id) => id + 1);
1048
1094
  };
1049
- const dismissToast = (0, import_react9.useCallback)(() => setToastId(0), []);
1095
+ const dismissToast = (0, import_react10.useCallback)(() => setToastId(0), []);
1050
1096
  const handleNativeShare = async () => {
1051
1097
  if (!absoluteUrl || !navigator.share) return;
1052
1098
  setShareError(null);
@@ -1057,8 +1103,8 @@ function SharePopup({ target, url, onClose }) {
1057
1103
  setShareError("Could not open the share options.");
1058
1104
  }
1059
1105
  };
1060
- return /* @__PURE__ */ (0, import_jsx_runtime17.jsxs)(import_jsx_runtime17.Fragment, { children: [
1061
- /* @__PURE__ */ (0, import_jsx_runtime17.jsx)(
1106
+ return /* @__PURE__ */ (0, import_jsx_runtime18.jsxs)(import_jsx_runtime18.Fragment, { children: [
1107
+ /* @__PURE__ */ (0, import_jsx_runtime18.jsx)(
1062
1108
  BasePopup_default,
1063
1109
  {
1064
1110
  title: "Share",
@@ -1073,67 +1119,67 @@ function SharePopup({ target, url, onClose }) {
1073
1119
  titleTextClassName: "!text-xl !font-semibold !text-[var(--color-text)]",
1074
1120
  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)]",
1075
1121
  contentClassName: "!rounded-none !bg-transparent !px-6 !pb-6 !pt-2",
1076
- children: /* @__PURE__ */ (0, import_jsx_runtime17.jsxs)("div", { className: "flex flex-col gap-4", children: [
1077
- /* @__PURE__ */ (0, import_jsx_runtime17.jsx)(SharePreviewCard, { target, url: absoluteUrl || url }),
1078
- /* @__PURE__ */ (0, import_jsx_runtime17.jsxs)("div", { className: "grid grid-cols-3 gap-1 rounded-3xl bg-[var(--color-surface)] p-3 sm:grid-cols-4", children: [
1079
- /* @__PURE__ */ (0, import_jsx_runtime17.jsx)(
1122
+ children: /* @__PURE__ */ (0, import_jsx_runtime18.jsxs)("div", { className: "flex flex-col gap-4", children: [
1123
+ /* @__PURE__ */ (0, import_jsx_runtime18.jsx)(SharePreviewCard, { target, url: absoluteUrl || url }),
1124
+ /* @__PURE__ */ (0, import_jsx_runtime18.jsxs)("div", { className: "grid grid-cols-3 gap-1 rounded-3xl bg-[var(--color-surface)] p-3 sm:grid-cols-4", children: [
1125
+ /* @__PURE__ */ (0, import_jsx_runtime18.jsx)(
1080
1126
  ShareDestinationButton,
1081
1127
  {
1082
1128
  label: "Copy Link",
1083
- icon: /* @__PURE__ */ (0, import_jsx_runtime17.jsx)(import_lucide_react5.Link2, { "aria-hidden": "true", className: "h-6 w-6" }),
1129
+ icon: /* @__PURE__ */ (0, import_jsx_runtime18.jsx)(import_lucide_react5.Link2, { "aria-hidden": "true", className: "h-6 w-6" }),
1084
1130
  onClick: () => void handleCopy(),
1085
1131
  ariaLabel: "Copy link",
1086
1132
  disabled: !absoluteUrl
1087
1133
  }
1088
1134
  ),
1089
- /* @__PURE__ */ (0, import_jsx_runtime17.jsx)(
1135
+ /* @__PURE__ */ (0, import_jsx_runtime18.jsx)(
1090
1136
  ShareDestinationButton,
1091
1137
  {
1092
1138
  label: "WhatsApp",
1093
- icon: /* @__PURE__ */ (0, import_jsx_runtime17.jsx)(import_fa.FaWhatsapp, { "aria-hidden": "true", className: "h-9 w-9 text-[#25D366]" }),
1139
+ icon: /* @__PURE__ */ (0, import_jsx_runtime18.jsx)(import_fa.FaWhatsapp, { "aria-hidden": "true", className: "h-9 w-9 text-[#25D366]" }),
1094
1140
  href: destinationUrls?.whatsapp,
1095
1141
  ariaLabel: `Share ${target.name} via WhatsApp`,
1096
1142
  disabled: !absoluteUrl
1097
1143
  }
1098
1144
  ),
1099
- /* @__PURE__ */ (0, import_jsx_runtime17.jsx)(
1145
+ /* @__PURE__ */ (0, import_jsx_runtime18.jsx)(
1100
1146
  ShareDestinationButton,
1101
1147
  {
1102
1148
  label: "Gmail",
1103
- icon: /* @__PURE__ */ (0, import_jsx_runtime17.jsx)(import_si.SiGmail, { "aria-hidden": "true", className: "h-8 w-8 text-[#EA4335]" }),
1149
+ icon: /* @__PURE__ */ (0, import_jsx_runtime18.jsx)(import_si.SiGmail, { "aria-hidden": "true", className: "h-8 w-8 text-[#EA4335]" }),
1104
1150
  href: destinationUrls?.gmail,
1105
1151
  ariaLabel: `Email ${target.name} with Gmail`,
1106
1152
  disabled: !absoluteUrl
1107
1153
  }
1108
1154
  ),
1109
- canUseNativeShare && /* @__PURE__ */ (0, import_jsx_runtime17.jsx)(
1155
+ canUseNativeShare && /* @__PURE__ */ (0, import_jsx_runtime18.jsx)(
1110
1156
  ShareDestinationButton,
1111
1157
  {
1112
1158
  label: "More",
1113
- icon: /* @__PURE__ */ (0, import_jsx_runtime17.jsx)(import_lucide_react5.Share2, { "aria-hidden": "true", className: "h-6 w-6" }),
1159
+ icon: /* @__PURE__ */ (0, import_jsx_runtime18.jsx)(import_lucide_react5.Share2, { "aria-hidden": "true", className: "h-6 w-6" }),
1114
1160
  onClick: () => void handleNativeShare(),
1115
1161
  ariaLabel: "More sharing options",
1116
1162
  disabled: !absoluteUrl
1117
1163
  }
1118
1164
  )
1119
1165
  ] }),
1120
- copyStatus === "failed" && /* @__PURE__ */ (0, import_jsx_runtime17.jsx)("p", { role: "alert", className: "text-center text-sm text-[var(--color-red-500)]", children: "Could not copy the link." }),
1121
- shareError && /* @__PURE__ */ (0, import_jsx_runtime17.jsx)("p", { role: "alert", className: "text-center text-sm text-[var(--color-red-500)]", children: shareError })
1166
+ copyStatus === "failed" && /* @__PURE__ */ (0, import_jsx_runtime18.jsx)("p", { role: "alert", className: "text-center text-sm text-[var(--color-red-500)]", children: "Could not copy the link." }),
1167
+ shareError && /* @__PURE__ */ (0, import_jsx_runtime18.jsx)("p", { role: "alert", className: "text-center text-sm text-[var(--color-red-500)]", children: shareError })
1122
1168
  ] })
1123
1169
  }
1124
1170
  ),
1125
- toastId > 0 && /* @__PURE__ */ (0, import_jsx_runtime17.jsx)(Toast, { message: "Link copied", onClose: dismissToast }, toastId)
1171
+ toastId > 0 && /* @__PURE__ */ (0, import_jsx_runtime18.jsx)(Toast, { message: "Link copied", onClose: dismissToast }, toastId)
1126
1172
  ] });
1127
1173
  }
1128
1174
 
1129
1175
  // src/genre-tree/ShareActionButton.tsx
1130
- var import_jsx_runtime18 = require("react/jsx-runtime");
1176
+ var import_jsx_runtime19 = require("react/jsx-runtime");
1131
1177
  function ShareActionButton({ target, url }) {
1132
1178
  const { showPopup, hidePopup } = (0, import_popup3.usePopup)();
1133
1179
  const openSharePopup = () => {
1134
- showPopup(/* @__PURE__ */ (0, import_jsx_runtime18.jsx)(SharePopup, { target, url, onClose: hidePopup }));
1180
+ showPopup(/* @__PURE__ */ (0, import_jsx_runtime19.jsx)(SharePopup, { target, url, onClose: hidePopup }));
1135
1181
  };
1136
- return /* @__PURE__ */ (0, import_jsx_runtime18.jsxs)(
1182
+ return /* @__PURE__ */ (0, import_jsx_runtime19.jsxs)(
1137
1183
  "button",
1138
1184
  {
1139
1185
  type: "button",
@@ -1142,7 +1188,7 @@ function ShareActionButton({ target, url }) {
1142
1188
  "aria-label": `Share ${target.name}`,
1143
1189
  title: `Share ${target.name}`,
1144
1190
  children: [
1145
- /* @__PURE__ */ (0, import_jsx_runtime18.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_runtime18.jsx)(import_lucide_react6.Share2, { "aria-hidden": "true", className: "w-5 h-5" }) }),
1191
+ /* @__PURE__ */ (0, import_jsx_runtime19.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_runtime19.jsx)(import_lucide_react6.Share2, { "aria-hidden": "true", className: "w-5 h-5" }) }),
1146
1192
  "Share"
1147
1193
  ]
1148
1194
  }
@@ -1150,7 +1196,7 @@ function ShareActionButton({ target, url }) {
1150
1196
  }
1151
1197
 
1152
1198
  // src/genre-tree/GenreActionBand.tsx
1153
- var import_jsx_runtime19 = require("react/jsx-runtime");
1199
+ var import_jsx_runtime20 = require("react/jsx-runtime");
1154
1200
  function GenreActionBand({
1155
1201
  node,
1156
1202
  onPlayPause,
@@ -1172,7 +1218,7 @@ function GenreActionBand({
1172
1218
  items.push({
1173
1219
  key: "play-pause",
1174
1220
  label: isPlaying ? "Pause" : "Play",
1175
- icon: isPlaying ? /* @__PURE__ */ (0, import_jsx_runtime19.jsx)(import_lucide_react7.Pause, { "aria-hidden": "true", className: "w-5 h-5" }) : /* @__PURE__ */ (0, import_jsx_runtime19.jsx)(import_lucide_react7.Play, { "aria-hidden": "true", className: "w-5 h-5" }),
1221
+ icon: isPlaying ? /* @__PURE__ */ (0, import_jsx_runtime20.jsx)(import_lucide_react7.Pause, { "aria-hidden": "true", className: "w-5 h-5" }) : /* @__PURE__ */ (0, import_jsx_runtime20.jsx)(import_lucide_react7.Play, { "aria-hidden": "true", className: "w-5 h-5" }),
1176
1222
  onClick: onPlayPause
1177
1223
  });
1178
1224
  }
@@ -1180,24 +1226,24 @@ function GenreActionBand({
1180
1226
  items.push({
1181
1227
  key: "zoom-in",
1182
1228
  label: "Focus",
1183
- icon: /* @__PURE__ */ (0, import_jsx_runtime19.jsx)(import_lucide_react7.ZoomIn, { "aria-hidden": "true", className: "w-5 h-5" }),
1229
+ icon: /* @__PURE__ */ (0, import_jsx_runtime20.jsx)(import_lucide_react7.ZoomIn, { "aria-hidden": "true", className: "w-5 h-5" }),
1184
1230
  onClick: onZoomIn
1185
1231
  });
1186
1232
  }
1187
1233
  if (onAddChild) {
1188
- adminItems.push({ key: "add-child", label: "Add", icon: /* @__PURE__ */ (0, import_jsx_runtime19.jsx)(import_lucide_react7.Plus, { "aria-hidden": "true", className: "w-5 h-5" }), onClick: onAddChild });
1234
+ adminItems.push({ key: "add-child", label: "Add", icon: /* @__PURE__ */ (0, import_jsx_runtime20.jsx)(import_lucide_react7.Plus, { "aria-hidden": "true", className: "w-5 h-5" }), onClick: onAddChild });
1189
1235
  }
1190
1236
  if (onRename) {
1191
- adminItems.push({ key: "rename", label: "Rename", icon: /* @__PURE__ */ (0, import_jsx_runtime19.jsx)(import_lucide_react7.Pencil, { "aria-hidden": "true", className: "w-5 h-5" }), onClick: onRename });
1237
+ adminItems.push({ key: "rename", label: "Rename", icon: /* @__PURE__ */ (0, import_jsx_runtime20.jsx)(import_lucide_react7.Pencil, { "aria-hidden": "true", className: "w-5 h-5" }), onClick: onRename });
1192
1238
  }
1193
1239
  if (onChangeParent) {
1194
- adminItems.push({ key: "change-parent", label: "Parent", icon: /* @__PURE__ */ (0, import_jsx_runtime19.jsx)(import_lucide_react7.MoveUpRight, { "aria-hidden": "true", className: "w-5 h-5" }), onClick: onChangeParent });
1240
+ adminItems.push({ key: "change-parent", label: "Parent", icon: /* @__PURE__ */ (0, import_jsx_runtime20.jsx)(import_lucide_react7.MoveUpRight, { "aria-hidden": "true", className: "w-5 h-5" }), onClick: onChangeParent });
1195
1241
  }
1196
1242
  if (onToggleSide) {
1197
1243
  adminItems.push({
1198
1244
  key: "toggle-side",
1199
1245
  label: side === "pop" ? "Make core" : "Make pop",
1200
- icon: /* @__PURE__ */ (0, import_jsx_runtime19.jsx)(import_lucide_react7.ArrowLeftRight, { "aria-hidden": "true", className: "w-5 h-5" }),
1246
+ icon: /* @__PURE__ */ (0, import_jsx_runtime20.jsx)(import_lucide_react7.ArrowLeftRight, { "aria-hidden": "true", className: "w-5 h-5" }),
1201
1247
  onClick: onToggleSide,
1202
1248
  disabled: !!toggleSideDisabledReason,
1203
1249
  title: toggleSideDisabledReason
@@ -1213,7 +1259,7 @@ function GenreActionBand({
1213
1259
  });
1214
1260
  }
1215
1261
  if (items.length === 0 && (!shareTarget || !shareUrl) && adminItems.length === 0) return null;
1216
- const renderItems = (list) => list.map((item) => /* @__PURE__ */ (0, import_jsx_runtime19.jsxs)(
1262
+ const renderItems = (list) => list.map((item) => /* @__PURE__ */ (0, import_jsx_runtime20.jsxs)(
1217
1263
  "button",
1218
1264
  {
1219
1265
  type: "button",
@@ -1222,25 +1268,25 @@ function GenreActionBand({
1222
1268
  onClick: item.onClick,
1223
1269
  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",
1224
1270
  children: [
1225
- /* @__PURE__ */ (0, import_jsx_runtime19.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 }),
1271
+ /* @__PURE__ */ (0, import_jsx_runtime20.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 }),
1226
1272
  item.label
1227
1273
  ]
1228
1274
  },
1229
1275
  item.key
1230
1276
  ));
1231
- return /* @__PURE__ */ (0, import_jsx_runtime19.jsxs)("div", { className: "genre-action-band border-b border-[var(--color-border)]", children: [
1232
- (items.length > 0 || shareTarget && shareUrl) && /* @__PURE__ */ (0, import_jsx_runtime19.jsxs)("div", { className: "flex flex-wrap gap-1 px-3.5 py-3", children: [
1277
+ return /* @__PURE__ */ (0, import_jsx_runtime20.jsxs)("div", { className: "genre-action-band border-b border-[var(--color-border)]", children: [
1278
+ (items.length > 0 || shareTarget && shareUrl) && /* @__PURE__ */ (0, import_jsx_runtime20.jsxs)("div", { className: "flex flex-wrap gap-1 px-3.5 py-3", children: [
1233
1279
  renderItems(items),
1234
- shareTarget && shareUrl && /* @__PURE__ */ (0, import_jsx_runtime19.jsx)(ShareActionButton, { target: shareTarget, url: shareUrl })
1280
+ shareTarget && shareUrl && /* @__PURE__ */ (0, import_jsx_runtime20.jsx)(ShareActionButton, { target: shareTarget, url: shareUrl })
1235
1281
  ] }),
1236
- adminItems.length > 0 && /* @__PURE__ */ (0, import_jsx_runtime19.jsxs)(
1282
+ adminItems.length > 0 && /* @__PURE__ */ (0, import_jsx_runtime20.jsxs)(
1237
1283
  "section",
1238
1284
  {
1239
1285
  "aria-label": "Admin actions",
1240
1286
  className: `px-3.5 py-3 ${items.length > 0 ? "border-t border-[var(--color-border)]" : ""}`,
1241
1287
  children: [
1242
- /* @__PURE__ */ (0, import_jsx_runtime19.jsx)("h3", { className: "mb-2 text-xs font-semibold uppercase tracking-wide text-[var(--color-text-muted)]", children: "Admin" }),
1243
- /* @__PURE__ */ (0, import_jsx_runtime19.jsx)("div", { className: "flex flex-wrap gap-1", children: renderItems(adminItems) })
1288
+ /* @__PURE__ */ (0, import_jsx_runtime20.jsx)("h3", { className: "mb-2 text-xs font-semibold uppercase tracking-wide text-[var(--color-text-muted)]", children: "Admin" }),
1289
+ /* @__PURE__ */ (0, import_jsx_runtime20.jsx)("div", { className: "flex flex-wrap gap-1", children: renderItems(adminItems) })
1244
1290
  ]
1245
1291
  }
1246
1292
  )
@@ -1249,23 +1295,23 @@ function GenreActionBand({
1249
1295
 
1250
1296
  // src/genre-tree/GenreDetailExtras.tsx
1251
1297
  var import_ui6 = require("@behindthemusictree/ui");
1252
- var import_genre_tree12 = require("@behindthemusictree/app-kit/genre-tree");
1298
+ var import_genre_tree17 = require("@behindthemusictree/app-kit/genre-tree");
1253
1299
 
1254
1300
  // src/genre-tree/ClampedText.tsx
1255
- var import_react10 = require("react");
1256
- var import_jsx_runtime20 = require("react/jsx-runtime");
1301
+ var import_react11 = require("react");
1302
+ var import_jsx_runtime21 = require("react/jsx-runtime");
1257
1303
  function ClampedText({ children, className: className2 = "" }) {
1258
- const ref = (0, import_react10.useRef)(null);
1259
- const [expanded, setExpanded] = (0, import_react10.useState)(false);
1260
- const [overflows, setOverflows] = (0, import_react10.useState)(false);
1261
- (0, import_react10.useLayoutEffect)(() => {
1304
+ const ref = (0, import_react11.useRef)(null);
1305
+ const [expanded, setExpanded] = (0, import_react11.useState)(false);
1306
+ const [overflows, setOverflows] = (0, import_react11.useState)(false);
1307
+ (0, import_react11.useLayoutEffect)(() => {
1262
1308
  const element = ref.current;
1263
1309
  if (!element || expanded) return;
1264
1310
  setOverflows(element.scrollHeight > element.clientHeight);
1265
1311
  }, [children, expanded]);
1266
- return /* @__PURE__ */ (0, import_jsx_runtime20.jsxs)(import_jsx_runtime20.Fragment, { children: [
1267
- /* @__PURE__ */ (0, import_jsx_runtime20.jsx)("p", { ref, className: `${className2} ${expanded ? "" : "line-clamp-3"}`, children }),
1268
- (overflows || expanded) && /* @__PURE__ */ (0, import_jsx_runtime20.jsx)(
1312
+ return /* @__PURE__ */ (0, import_jsx_runtime21.jsxs)(import_jsx_runtime21.Fragment, { children: [
1313
+ /* @__PURE__ */ (0, import_jsx_runtime21.jsx)("p", { ref, className: `${className2} ${expanded ? "" : "line-clamp-3"}`, children }),
1314
+ (overflows || expanded) && /* @__PURE__ */ (0, import_jsx_runtime21.jsx)(
1269
1315
  "button",
1270
1316
  {
1271
1317
  type: "button",
@@ -1279,7 +1325,7 @@ function ClampedText({ children, className: className2 = "" }) {
1279
1325
  }
1280
1326
 
1281
1327
  // src/genre-tree/GenrePlaylistTracks.tsx
1282
- var import_react11 = require("react");
1328
+ var import_react12 = require("react");
1283
1329
  var import_react_query = require("@tanstack/react-query");
1284
1330
  var import_zod = require("zod");
1285
1331
  var import_fa2 = require("react-icons/fa");
@@ -1292,22 +1338,23 @@ var import_player4 = require("@behindthemusictree/app-kit/player");
1292
1338
  var import_player5 = require("@behindthemusictree/app-kit/player");
1293
1339
  var import_popup4 = require("@behindthemusictree/app-kit/popup");
1294
1340
  var import_transport6 = require("@behindthemusictree/app-kit/transport");
1295
- var import_genre_tree7 = require("@behindthemusictree/app-kit/genre-tree");
1296
- var import_genre_tree8 = require("@behindthemusictree/app-kit/genre-tree");
1297
- var import_genre_tree9 = require("@behindthemusictree/app-kit/genre-tree");
1298
- var import_genre_tree10 = require("@behindthemusictree/app-kit/genre-tree");
1341
+ var import_genre_tree11 = require("@behindthemusictree/app-kit/genre-tree");
1342
+ var import_genre_tree12 = require("@behindthemusictree/app-kit/genre-tree");
1343
+ var import_genre_tree13 = require("@behindthemusictree/app-kit/genre-tree");
1344
+ var import_genre_tree14 = require("@behindthemusictree/app-kit/genre-tree");
1345
+ var import_genre_tree15 = require("@behindthemusictree/app-kit/genre-tree");
1299
1346
 
1300
1347
  // src/genre-tree/MusicBrainzRecordingLink.tsx
1301
- var import_genre_tree6 = require("@behindthemusictree/app-kit/genre-tree");
1302
- var import_jsx_runtime21 = require("react/jsx-runtime");
1348
+ var import_genre_tree10 = require("@behindthemusictree/app-kit/genre-tree");
1349
+ var import_jsx_runtime22 = require("react/jsx-runtime");
1303
1350
  function MusicBrainzRecordingLink({
1304
1351
  song,
1305
1352
  children,
1306
1353
  className: className2 = ""
1307
1354
  }) {
1308
- const url = (0, import_genre_tree6.musicBrainzRecordingUrl)(song);
1309
- if (!url) return /* @__PURE__ */ (0, import_jsx_runtime21.jsx)(import_jsx_runtime21.Fragment, { children });
1310
- return /* @__PURE__ */ (0, import_jsx_runtime21.jsx)(
1355
+ const url = (0, import_genre_tree10.musicBrainzRecordingUrl)(song);
1356
+ if (!url) return /* @__PURE__ */ (0, import_jsx_runtime22.jsx)(import_jsx_runtime22.Fragment, { children });
1357
+ return /* @__PURE__ */ (0, import_jsx_runtime22.jsx)(
1311
1358
  "a",
1312
1359
  {
1313
1360
  href: url,
@@ -1322,11 +1369,11 @@ function MusicBrainzRecordingLink({
1322
1369
  }
1323
1370
 
1324
1371
  // src/genre-tree/GenrePlaylistTracks.tsx
1325
- var import_genre_tree11 = require("@behindthemusictree/app-kit/genre-tree");
1372
+ var import_genre_tree16 = require("@behindthemusictree/app-kit/genre-tree");
1326
1373
  var import_track_noun = require("@behindthemusictree/app-kit/track-noun");
1327
- var import_jsx_runtime22 = require("react/jsx-runtime");
1374
+ var import_jsx_runtime23 = require("react/jsx-runtime");
1328
1375
  var CATALOGUE_PAGE_SIZE = 100;
1329
- var SongPageSchema = (0, import_transport4.PaginatedResponseSchema)(import_genre_tree9.SongDetailedSchema).extend({
1376
+ var SongPageSchema = (0, import_transport4.PaginatedResponseSchema)(import_genre_tree14.SongDetailedSchema).extend({
1330
1377
  overallTotal: import_zod.z.number().nullable(),
1331
1378
  page: import_zod.z.number().nullable(),
1332
1379
  totalPages: import_zod.z.number().nullable()
@@ -1344,6 +1391,7 @@ var nextCursor = (next) => {
1344
1391
  function GenrePlaylistTracks({
1345
1392
  genreUuid,
1346
1393
  genrePlaylist,
1394
+ selection,
1347
1395
  scope,
1348
1396
  getBackendBaseUrl,
1349
1397
  renderArtistLink,
@@ -1353,7 +1401,7 @@ function GenrePlaylistTracks({
1353
1401
  renderTracksToolbar,
1354
1402
  artist = null
1355
1403
  }) {
1356
- const { fetchGenrePlaylistTracksPage, playNewTrackListFromGenrePlaylist, playNewTrackListFromArtist } = (0, import_genre_tree10.useTrackList)();
1404
+ const { fetchGenrePlaylistTracksPage, playNewTrackListFromGenrePlaylist, playNewTrackListFromArtist, playNewTrackListFromGenreCatalogue } = (0, import_genre_tree15.useTrackList)();
1357
1405
  const { session, sessionRestored } = (0, import_auth.useSession)();
1358
1406
  const { playerTrackObject, playState, handlePlayPauseAction } = (0, import_player4.usePlayer)();
1359
1407
  const { showPopup } = (0, import_popup4.usePopup)();
@@ -1362,15 +1410,15 @@ function GenrePlaylistTracks({
1362
1410
  const { fetch } = (0, import_transport3.useFetchWrapper)(getBackendBaseUrl);
1363
1411
  const playback = (0, import_transport3.usePlaybackContext)();
1364
1412
  const { data, isPending, isError, refetch, hasNextPage, isFetchingNextPage, fetchNextPage } = (0, import_react_query.useInfiniteQuery)({
1365
- 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] : []],
1413
+ 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] : []],
1366
1414
  queryFn: async ({ pageParam }) => {
1367
1415
  if (scope === "me") return fetchGenrePlaylistTracksPage(uuid, scope, Number(pageParam));
1368
1416
  const response = await fetch(
1369
- import_genre_tree8.libraryEndpoints.reference.songs.list(),
1417
+ import_genre_tree13.libraryEndpoints.reference.songs.list(),
1370
1418
  true,
1371
1419
  false,
1372
1420
  {},
1373
- { genre: genreUuid, ...artist && { artist: artist.uuid }, pageSize: CATALOGUE_PAGE_SIZE, withTotal: false, ...pageParam && { cursor: pageParam } }
1421
+ { genre: genreUuid, ...(0, import_genre_tree11.genreMusicQueryParams)(selection), ...artist && { artist: artist.uuid }, pageSize: CATALOGUE_PAGE_SIZE, withTotal: false, ...pageParam && { cursor: pageParam } }
1374
1422
  );
1375
1423
  const parsed = (0, import_transport5.parseWithLog)(SongPageSchema, response, "GenrePlaylistTracks");
1376
1424
  return { tracks: parsed.results, total: parsed.overallTotal, nextPage: parsed.next ? nextCursor(parsed.next) : null };
@@ -1379,8 +1427,8 @@ function GenrePlaylistTracks({
1379
1427
  getNextPageParam: (lastPage) => lastPage.nextPage ?? void 0,
1380
1428
  enabled: playback.ready && (scope !== "me" || sessionRestored && !!session?.accessToken)
1381
1429
  });
1382
- const [sentinel, setSentinel] = (0, import_react11.useState)(null);
1383
- (0, import_react11.useEffect)(() => {
1430
+ const [sentinel, setSentinel] = (0, import_react12.useState)(null);
1431
+ (0, import_react12.useEffect)(() => {
1384
1432
  if (!hasNextPage || isFetchingNextPage || !sentinel) return;
1385
1433
  const observer = new IntersectionObserver((entries) => {
1386
1434
  if (entries.some((entry) => entry.isIntersecting)) {
@@ -1394,38 +1442,43 @@ function GenrePlaylistTracks({
1394
1442
  const tracks = pages.flatMap((page) => page.tracks);
1395
1443
  const total = pages[0]?.total ?? void 0;
1396
1444
  const play = (startSongUuid) => {
1397
- const started = artist ? playNewTrackListFromArtist(artist, scope, { startSongUuid, ordering: "title", genreUuid }) : playNewTrackListFromGenrePlaylist(genrePlaylist, scope, { startSongUuid });
1445
+ const started = artist ? playNewTrackListFromArtist(artist, scope, {
1446
+ startSongUuid,
1447
+ ordering: "title",
1448
+ genreUuid,
1449
+ ...selection && { selection }
1450
+ }) : scope === "reference" ? playNewTrackListFromGenreCatalogue({ uuid: genreUuid, name: genrePlaylist.name }, scope, { startSongUuid, ...selection && { selection } }) : playNewTrackListFromGenrePlaylist(genrePlaylist, scope, { startSongUuid });
1398
1451
  started.catch((error) => {
1399
1452
  console.error("Failed to play genre playlist:", error);
1400
- showPopup(/* @__PURE__ */ (0, import_jsx_runtime22.jsx)(InternalErrorPopup, { errorCode: import_transport6.ErrorCode.CLIENT_UNKNOWN }));
1453
+ showPopup(/* @__PURE__ */ (0, import_jsx_runtime23.jsx)(InternalErrorPopup, { errorCode: import_transport6.ErrorCode.CLIENT_UNKNOWN }));
1401
1454
  });
1402
1455
  };
1403
- return /* @__PURE__ */ (0, import_jsx_runtime22.jsx)(import_ui5.PanelSection, { title: `${(0, import_track_noun.capitalize)(trackNoun.plural)}${total !== void 0 ? ` (${total})` : ""}`, children: /* @__PURE__ */ (0, import_jsx_runtime22.jsxs)("div", { children: [
1456
+ return /* @__PURE__ */ (0, import_jsx_runtime23.jsx)(import_ui5.PanelSection, { title: `${(0, import_track_noun.capitalize)(trackNoun.plural)}${total !== void 0 ? ` (${total})` : ""}`, children: /* @__PURE__ */ (0, import_jsx_runtime23.jsxs)("div", { children: [
1404
1457
  renderTracksToolbar?.(),
1405
- isPending ? /* @__PURE__ */ (0, import_jsx_runtime22.jsx)(import_ui5.Skeleton, { className: "h-4 w-3/4", "aria-busy": "true" }) : isError && tracks.length === 0 ? /* @__PURE__ */ (0, import_jsx_runtime22.jsxs)("p", { role: "alert", children: [
1458
+ isPending ? /* @__PURE__ */ (0, import_jsx_runtime23.jsx)(import_ui5.Skeleton, { className: "h-4 w-3/4", "aria-busy": "true" }) : isError && tracks.length === 0 ? /* @__PURE__ */ (0, import_jsx_runtime23.jsxs)("p", { role: "alert", children: [
1406
1459
  "Couldn't load ",
1407
1460
  trackNoun.plural,
1408
1461
  ".",
1409
1462
  " ",
1410
- /* @__PURE__ */ (0, import_jsx_runtime22.jsx)("button", { type: "button", className: "underline", onClick: () => void refetch(), children: "Retry" })
1411
- ] }) : tracks.length === 0 ? /* @__PURE__ */ (0, import_jsx_runtime22.jsx)("p", { children: "\u2014" }) : /* @__PURE__ */ (0, import_jsx_runtime22.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: [
1463
+ /* @__PURE__ */ (0, import_jsx_runtime23.jsx)("button", { type: "button", className: "underline", onClick: () => void refetch(), children: "Retry" })
1464
+ ] }) : tracks.length === 0 ? /* @__PURE__ */ (0, import_jsx_runtime23.jsx)("p", { children: "\u2014" }) : /* @__PURE__ */ (0, import_jsx_runtime23.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: [
1412
1465
  tracks.map((track, index) => {
1413
1466
  const label = track.artists?.length ? `${track.title} \u2014 ${track.artists.map((artist2) => artist2.name).join(", ")}` : track.title;
1414
1467
  const isCurrent = playerTrackObject?.track.id === track.uuid;
1415
1468
  const isPlaying = isCurrent && playState === import_player5.PlayStates.PLAYING;
1416
- const text = /* @__PURE__ */ (0, import_jsx_runtime22.jsxs)("span", { className: TEXT_CELL, children: [
1417
- /* @__PURE__ */ (0, import_jsx_runtime22.jsx)("span", { className: TITLE_CELL, children: renderSongLink ? renderSongLink(track) : (0, import_genre_tree9.isSong)(track) ? /* @__PURE__ */ (0, import_jsx_runtime22.jsx)(MusicBrainzRecordingLink, { song: track, className: "hover:underline focus-visible:underline", children: track.title }) : track.title }),
1418
- track.artists?.length ? /* @__PURE__ */ (0, import_jsx_runtime22.jsx)("span", { className: SEPARATOR, children: " \u2014 " }) : null,
1419
- /* @__PURE__ */ (0, import_jsx_runtime22.jsx)("span", { className: COLUMN_CELL, children: track.artists?.length ? /* @__PURE__ */ (0, import_jsx_runtime22.jsx)(import_genre_tree11.ArtistNames, { artists: track.artists, renderArtistLink }) : null }),
1420
- track.album && /* @__PURE__ */ (0, import_jsx_runtime22.jsx)("span", { className: ALBUM_CELL, children: track.album.name })
1469
+ const text = /* @__PURE__ */ (0, import_jsx_runtime23.jsxs)("span", { className: TEXT_CELL, children: [
1470
+ /* @__PURE__ */ (0, import_jsx_runtime23.jsx)("span", { className: TITLE_CELL, children: renderSongLink ? renderSongLink(track) : (0, import_genre_tree14.isSong)(track) ? /* @__PURE__ */ (0, import_jsx_runtime23.jsx)(MusicBrainzRecordingLink, { song: track, className: "hover:underline focus-visible:underline", children: track.title }) : track.title }),
1471
+ track.artists?.length ? /* @__PURE__ */ (0, import_jsx_runtime23.jsx)("span", { className: SEPARATOR, children: " \u2014 " }) : null,
1472
+ /* @__PURE__ */ (0, import_jsx_runtime23.jsx)("span", { className: COLUMN_CELL, children: track.artists?.length ? /* @__PURE__ */ (0, import_jsx_runtime23.jsx)(import_genre_tree16.ArtistNames, { artists: track.artists, renderArtistLink }) : null }),
1473
+ track.album && /* @__PURE__ */ (0, import_jsx_runtime23.jsx)("span", { className: ALBUM_CELL, children: track.album.name })
1421
1474
  ] });
1422
1475
  const leading = renderSongLeading?.(track, index);
1423
1476
  const actions = renderSongActions?.(track);
1424
- const isUnplayable = (0, import_genre_tree9.isSong)(track) && (0, import_genre_tree9.songUnplayableReason)(track) !== null;
1477
+ const isUnplayable = (0, import_genre_tree14.isSong)(track) && (0, import_genre_tree14.songUnplayableReason)(track) !== null;
1425
1478
  if (isUnplayable) {
1426
- return /* @__PURE__ */ (0, import_jsx_runtime22.jsxs)("li", { className: "flex min-h-8 items-center gap-1 px-1 opacity-50", children: [
1479
+ return /* @__PURE__ */ (0, import_jsx_runtime23.jsxs)("li", { className: "flex min-h-8 items-center gap-1 px-1 opacity-50", children: [
1427
1480
  leading,
1428
- /* @__PURE__ */ (0, import_jsx_runtime22.jsx)(
1481
+ /* @__PURE__ */ (0, import_jsx_runtime23.jsx)(
1429
1482
  "button",
1430
1483
  {
1431
1484
  type: "button",
@@ -1433,27 +1486,27 @@ function GenrePlaylistTracks({
1433
1486
  "aria-disabled": "true",
1434
1487
  "aria-label": label,
1435
1488
  className: "flex h-6 w-6 shrink-0 items-center justify-center rounded-full opacity-0 cursor-not-allowed",
1436
- children: /* @__PURE__ */ (0, import_jsx_runtime22.jsx)(import_fa2.FaPlay, { size: 10 })
1489
+ children: /* @__PURE__ */ (0, import_jsx_runtime23.jsx)(import_fa2.FaPlay, { size: 10 })
1437
1490
  }
1438
1491
  ),
1439
1492
  text,
1440
1493
  actions
1441
1494
  ] }, track.uuid);
1442
1495
  }
1443
- return /* @__PURE__ */ (0, import_jsx_runtime22.jsxs)(
1496
+ return /* @__PURE__ */ (0, import_jsx_runtime23.jsxs)(
1444
1497
  "li",
1445
1498
  {
1446
1499
  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)]" : ""}`,
1447
1500
  children: [
1448
1501
  leading,
1449
- /* @__PURE__ */ (0, import_jsx_runtime22.jsx)(
1502
+ /* @__PURE__ */ (0, import_jsx_runtime23.jsx)(
1450
1503
  "button",
1451
1504
  {
1452
1505
  type: "button",
1453
1506
  "aria-label": `${isPlaying ? "Pause" : "Play"} ${label}`,
1454
1507
  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"}`,
1455
1508
  onClick: () => isCurrent ? handlePlayPauseAction() : play(track.uuid),
1456
- children: isCurrent && playState === import_player5.PlayStates.LOADING ? /* @__PURE__ */ (0, import_jsx_runtime22.jsx)(import_ui5.RingLoader, { size: 10 }) : isPlaying ? /* @__PURE__ */ (0, import_jsx_runtime22.jsx)(import_fa2.FaPause, { size: 10 }) : /* @__PURE__ */ (0, import_jsx_runtime22.jsx)(import_fa2.FaPlay, { size: 10 })
1509
+ children: isCurrent && playState === import_player5.PlayStates.LOADING ? /* @__PURE__ */ (0, import_jsx_runtime23.jsx)(import_ui5.RingLoader, { size: 10 }) : isPlaying ? /* @__PURE__ */ (0, import_jsx_runtime23.jsx)(import_fa2.FaPause, { size: 10 }) : /* @__PURE__ */ (0, import_jsx_runtime23.jsx)(import_fa2.FaPlay, { size: 10 })
1457
1510
  }
1458
1511
  ),
1459
1512
  text,
@@ -1463,7 +1516,7 @@ function GenrePlaylistTracks({
1463
1516
  track.uuid
1464
1517
  );
1465
1518
  }),
1466
- hasNextPage && /* @__PURE__ */ (0, import_jsx_runtime22.jsx)("li", { ref: setSentinel, "aria-hidden": "true", className: "h-4" })
1519
+ hasNextPage && /* @__PURE__ */ (0, import_jsx_runtime23.jsx)("li", { ref: setSentinel, "aria-hidden": "true", className: "h-4" })
1467
1520
  ] })
1468
1521
  ] }) });
1469
1522
  }
@@ -1471,10 +1524,11 @@ function GenrePlaylistTracks({
1471
1524
  // src/genre-tree/GenreDetailExtras.tsx
1472
1525
  var import_track_noun2 = require("@behindthemusictree/app-kit/track-noun");
1473
1526
  var import_si2 = require("react-icons/si");
1474
- var import_jsx_runtime23 = require("react/jsx-runtime");
1527
+ var import_jsx_runtime24 = require("react/jsx-runtime");
1475
1528
  function GenreDetailExtras({
1476
1529
  genreUuid,
1477
1530
  genrePlaylist,
1531
+ selection,
1478
1532
  wikidataId,
1479
1533
  scope,
1480
1534
  getBackendBaseUrl,
@@ -1488,24 +1542,24 @@ function GenreDetailExtras({
1488
1542
  ...trackSlots
1489
1543
  }) {
1490
1544
  const trackNoun = (0, import_track_noun2.useTrackNoun)();
1491
- const { data: overview, isPending } = (0, import_genre_tree12.useFetchGenreOverview)(
1545
+ const { data: overview, isPending } = (0, import_genre_tree17.useFetchGenreOverview)(
1492
1546
  genreUuid,
1493
1547
  scope,
1494
1548
  getBackendBaseUrl,
1495
1549
  criteriaOverviewSchema
1496
1550
  );
1497
1551
  if (isPending) {
1498
- return /* @__PURE__ */ (0, import_jsx_runtime23.jsxs)(import_jsx_runtime23.Fragment, { children: [
1499
- /* @__PURE__ */ (0, import_jsx_runtime23.jsx)(import_ui6.PanelSection, { title: "Summary", children: /* @__PURE__ */ (0, import_jsx_runtime23.jsx)("div", { "aria-busy": "true", "data-testid": "genre-detail-extras-skeleton", children: /* @__PURE__ */ (0, import_jsx_runtime23.jsx)(import_ui6.Skeleton, { className: "h-4 w-3/4" }) }) }),
1552
+ return /* @__PURE__ */ (0, import_jsx_runtime24.jsxs)(import_jsx_runtime24.Fragment, { children: [
1553
+ /* @__PURE__ */ (0, import_jsx_runtime24.jsx)(import_ui6.PanelSection, { title: "Summary", children: /* @__PURE__ */ (0, import_jsx_runtime24.jsx)("div", { "aria-busy": "true", "data-testid": "genre-detail-extras-skeleton", children: /* @__PURE__ */ (0, import_jsx_runtime24.jsx)(import_ui6.Skeleton, { className: "h-4 w-3/4" }) }) }),
1500
1554
  family
1501
1555
  ] });
1502
1556
  }
1503
- if (!overview) return /* @__PURE__ */ (0, import_jsx_runtime23.jsx)(import_jsx_runtime23.Fragment, { children: family });
1557
+ if (!overview) return /* @__PURE__ */ (0, import_jsx_runtime24.jsx)(import_jsx_runtime24.Fragment, { children: family });
1504
1558
  const { summary, summarySourceUrl, essentialSongs } = overview;
1505
1559
  const wikidataUrl = wikidataId ? `https://www.wikidata.org/wiki/${encodeURIComponent(wikidataId)}` : null;
1506
- return /* @__PURE__ */ (0, import_jsx_runtime23.jsxs)(import_jsx_runtime23.Fragment, { children: [
1507
- (summarySourceUrl || wikidataUrl) && /* @__PURE__ */ (0, import_jsx_runtime23.jsx)(import_ui6.PanelSection, { title: "Links", children: /* @__PURE__ */ (0, import_jsx_runtime23.jsxs)("div", { className: "flex flex-wrap gap-x-3 gap-y-1 text-xs text-[var(--color-text)]", children: [
1508
- summarySourceUrl && /* @__PURE__ */ (0, import_jsx_runtime23.jsxs)(
1560
+ return /* @__PURE__ */ (0, import_jsx_runtime24.jsxs)(import_jsx_runtime24.Fragment, { children: [
1561
+ (summarySourceUrl || wikidataUrl) && /* @__PURE__ */ (0, import_jsx_runtime24.jsx)(import_ui6.PanelSection, { title: "Links", children: /* @__PURE__ */ (0, import_jsx_runtime24.jsxs)("div", { className: "flex flex-wrap gap-x-3 gap-y-1 text-xs text-[var(--color-text)]", children: [
1562
+ summarySourceUrl && /* @__PURE__ */ (0, import_jsx_runtime24.jsxs)(
1509
1563
  "a",
1510
1564
  {
1511
1565
  href: summarySourceUrl,
@@ -1513,12 +1567,12 @@ function GenreDetailExtras({
1513
1567
  rel: "noopener noreferrer",
1514
1568
  className: "inline-flex items-center gap-1.5 text-[var(--color-text-muted)] hover:text-[var(--color-text)]",
1515
1569
  children: [
1516
- /* @__PURE__ */ (0, import_jsx_runtime23.jsx)(import_si2.SiWikipedia, { "aria-hidden": "true", className: "h-3.5 w-3.5" }),
1570
+ /* @__PURE__ */ (0, import_jsx_runtime24.jsx)(import_si2.SiWikipedia, { "aria-hidden": "true", className: "h-3.5 w-3.5" }),
1517
1571
  "Wikipedia"
1518
1572
  ]
1519
1573
  }
1520
1574
  ),
1521
- wikidataUrl && /* @__PURE__ */ (0, import_jsx_runtime23.jsxs)(
1575
+ wikidataUrl && /* @__PURE__ */ (0, import_jsx_runtime24.jsxs)(
1522
1576
  "a",
1523
1577
  {
1524
1578
  href: wikidataUrl,
@@ -1526,21 +1580,22 @@ function GenreDetailExtras({
1526
1580
  rel: "noopener noreferrer",
1527
1581
  className: "inline-flex items-center gap-1.5 text-[var(--color-text-muted)] hover:text-[var(--color-text)]",
1528
1582
  children: [
1529
- /* @__PURE__ */ (0, import_jsx_runtime23.jsx)(import_si2.SiWikidata, { "aria-hidden": "true", className: "h-3.5 w-3.5" }),
1583
+ /* @__PURE__ */ (0, import_jsx_runtime24.jsx)(import_si2.SiWikidata, { "aria-hidden": "true", className: "h-3.5 w-3.5" }),
1530
1584
  "Wikidata"
1531
1585
  ]
1532
1586
  }
1533
1587
  )
1534
1588
  ] }) }),
1535
- /* @__PURE__ */ (0, import_jsx_runtime23.jsx)(import_ui6.PanelSection, { title: "Summary", children: renderSummary ? renderSummary(overview) : /* @__PURE__ */ (0, import_jsx_runtime23.jsx)(ClampedText, { className: "text-[0.8125rem] leading-[1.45] text-[var(--color-text)]", children: summary ?? "\u2014" }, genreUuid) }),
1589
+ /* @__PURE__ */ (0, import_jsx_runtime24.jsx)(import_ui6.PanelSection, { title: "Summary", children: renderSummary ? renderSummary(overview) : /* @__PURE__ */ (0, import_jsx_runtime24.jsx)(ClampedText, { className: "text-[0.8125rem] leading-[1.45] text-[var(--color-text)]", children: summary ?? "\u2014" }, genreUuid) }),
1536
1590
  family,
1537
1591
  renderGenreDetailExtras?.(overview),
1538
- (renderEssentialTracks || essentialSongs) && /* @__PURE__ */ (0, import_jsx_runtime23.jsx)(import_ui6.PanelSection, { title: `Essential ${trackNoun.plural}`, children: renderEssentialTracks ? renderEssentialTracks(overview) : essentialSongs && essentialSongs.length > 0 ? /* @__PURE__ */ (0, import_jsx_runtime23.jsx)("ul", { className: "text-[0.8125rem] leading-[1.45] text-[var(--color-text)]", children: essentialSongs.map((track) => /* @__PURE__ */ (0, import_jsx_runtime23.jsx)("li", { className: "py-1", children: track.title }, track.uuid)) }) : /* @__PURE__ */ (0, import_jsx_runtime23.jsx)("p", { className: "text-[0.8125rem] leading-[1.45] text-[var(--color-text)]", children: "\u2014" }) }),
1539
- /* @__PURE__ */ (0, import_jsx_runtime23.jsx)(
1592
+ (renderEssentialTracks || essentialSongs) && /* @__PURE__ */ (0, import_jsx_runtime24.jsx)(import_ui6.PanelSection, { title: `Essential ${trackNoun.plural}`, children: renderEssentialTracks ? renderEssentialTracks(overview) : essentialSongs && essentialSongs.length > 0 ? /* @__PURE__ */ (0, import_jsx_runtime24.jsx)("ul", { className: "text-[0.8125rem] leading-[1.45] text-[var(--color-text)]", children: essentialSongs.map((track) => /* @__PURE__ */ (0, import_jsx_runtime24.jsx)("li", { className: "py-1", children: track.title }, track.uuid)) }) : /* @__PURE__ */ (0, import_jsx_runtime24.jsx)("p", { className: "text-[0.8125rem] leading-[1.45] text-[var(--color-text)]", children: "\u2014" }) }),
1593
+ /* @__PURE__ */ (0, import_jsx_runtime24.jsx)(
1540
1594
  GenrePlaylistTracks,
1541
1595
  {
1542
1596
  genreUuid,
1543
1597
  genrePlaylist,
1598
+ selection,
1544
1599
  scope,
1545
1600
  getBackendBaseUrl,
1546
1601
  renderArtistLink,
@@ -1552,8 +1607,8 @@ function GenreDetailExtras({
1552
1607
  }
1553
1608
 
1554
1609
  // src/genre-tree/GenreInfoPanelBody.tsx
1555
- var import_jsx_runtime24 = require("react/jsx-runtime");
1556
- var import_react12 = require("react");
1610
+ var import_jsx_runtime25 = require("react/jsx-runtime");
1611
+ var import_react13 = require("react");
1557
1612
  function GenreNodeActionBand({ genrePlaylist, playback, ...bandProps }) {
1558
1613
  const { isPlaying, setIsPlaying } = (0, import_player6.usePlayer)();
1559
1614
  const isCurrentTrackList = playback.playingNodeId === genrePlaylist.uuid;
@@ -1566,11 +1621,11 @@ function GenreNodeActionBand({ genrePlaylist, playback, ...bandProps }) {
1566
1621
  console.error("Failed to load genre playlist tracks:", error);
1567
1622
  });
1568
1623
  };
1569
- return /* @__PURE__ */ (0, import_jsx_runtime24.jsx)(
1624
+ return /* @__PURE__ */ (0, import_jsx_runtime25.jsx)(
1570
1625
  GenreActionBand,
1571
1626
  {
1572
1627
  ...bandProps,
1573
- onPlayPause: genrePlaylist.tracksCount > 0 ? handlePlayPause : void 0,
1628
+ onPlayPause: ("genreOnlyTracksCount" in genrePlaylist ? (0, import_genre_tree18.genreMusicCount)(genrePlaylist) : genrePlaylist.tracksCount) > 0 ? handlePlayPause : void 0,
1574
1629
  isPlaying: isCurrentTrackList && isPlaying
1575
1630
  }
1576
1631
  );
@@ -1580,6 +1635,7 @@ function GenreInfoPanelBody({
1580
1635
  family,
1581
1636
  genreUuid,
1582
1637
  genrePlaylist,
1638
+ catalogue,
1583
1639
  wikidataId,
1584
1640
  scope,
1585
1641
  getBackendBaseUrl,
@@ -1587,10 +1643,31 @@ function GenreInfoPanelBody({
1587
1643
  band,
1588
1644
  extras
1589
1645
  }) {
1590
- return /* @__PURE__ */ (0, import_jsx_runtime24.jsxs)(import_jsx_runtime24.Fragment, { children: [
1646
+ const noun = (0, import_track_noun3.useTrackNoun)();
1647
+ const summary = scope === "reference" ? import_genre_tree18.GenreCatalogueCountsSchema.parse(genrePlaylist) : void 0;
1648
+ const selection = summary ? (0, import_genre_tree18.defaultGenreMusicSelection)(summary) : void 0;
1649
+ const describeCounts = (counts) => {
1650
+ const scopes = counts.genreAndSubgenresTracksCount;
1651
+ 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`;
1652
+ };
1653
+ return /* @__PURE__ */ (0, import_jsx_runtime25.jsxs)(import_jsx_runtime25.Fragment, { children: [
1591
1654
  toolbar,
1592
- /* @__PURE__ */ (0, import_jsx_runtime24.jsx)(GenreNodeActionBand, { ...band, genrePlaylist, playback }),
1593
- /* @__PURE__ */ (0, import_react12.createElement)(
1655
+ /* @__PURE__ */ (0, import_jsx_runtime25.jsx)(GenreNodeActionBand, { ...band, genrePlaylist, playback }),
1656
+ summary && catalogue && /* @__PURE__ */ (0, import_jsx_runtime25.jsxs)("div", { className: "px-3.5 py-3 text-sm text-[var(--color-text)]", children: [
1657
+ /* @__PURE__ */ (0, import_jsx_runtime25.jsxs)("p", { children: [
1658
+ "Catalogue ",
1659
+ noun.plural,
1660
+ ": ",
1661
+ describeCounts(catalogue)
1662
+ ] }),
1663
+ /* @__PURE__ */ (0, import_jsx_runtime25.jsxs)("p", { children: [
1664
+ "Playlist ",
1665
+ noun.plural,
1666
+ ": ",
1667
+ describeCounts(summary)
1668
+ ] })
1669
+ ] }),
1670
+ /* @__PURE__ */ (0, import_react13.createElement)(
1594
1671
  GenreDetailExtras,
1595
1672
  {
1596
1673
  ...extras,
@@ -1598,6 +1675,7 @@ function GenreInfoPanelBody({
1598
1675
  family,
1599
1676
  genreUuid,
1600
1677
  genrePlaylist,
1678
+ selection,
1601
1679
  wikidataId,
1602
1680
  scope,
1603
1681
  getBackendBaseUrl
@@ -1607,9 +1685,9 @@ function GenreInfoPanelBody({
1607
1685
  }
1608
1686
 
1609
1687
  // src/genre-tree/ChangeParentPopup.tsx
1610
- var import_react13 = require("react");
1611
- var import_genre_tree13 = require("@behindthemusictree/app-kit/genre-tree");
1612
- var import_jsx_runtime25 = require("react/jsx-runtime");
1688
+ var import_react14 = require("react");
1689
+ var import_genre_tree19 = require("@behindthemusictree/app-kit/genre-tree");
1690
+ var import_jsx_runtime26 = require("react/jsx-runtime");
1613
1691
  function ChangeParentPopup({
1614
1692
  genrePlaylist,
1615
1693
  genrePlaylists,
@@ -1618,9 +1696,9 @@ function ChangeParentPopup({
1618
1696
  onClose,
1619
1697
  onParentChanged
1620
1698
  }) {
1621
- const { mutate, isPending, formErrors } = (0, import_genre_tree13.useUpdateGenre)(scope, getBackendBaseUrl);
1622
- const candidates = (0, import_react13.useMemo)(
1623
- () => (0, import_genre_tree13.getReparentCandidates)(genrePlaylists, genrePlaylist.uuid),
1699
+ const { mutate, isPending, formErrors } = (0, import_genre_tree19.useUpdateGenre)(scope, getBackendBaseUrl);
1700
+ const candidates = (0, import_react14.useMemo)(
1701
+ () => (0, import_genre_tree19.getReparentCandidates)(genrePlaylists, genrePlaylist.uuid),
1624
1702
  [genrePlaylists, genrePlaylist.uuid]
1625
1703
  );
1626
1704
  const handleSelect = (newParent) => {
@@ -1637,19 +1715,19 @@ function ChangeParentPopup({
1637
1715
  }
1638
1716
  );
1639
1717
  };
1640
- (0, import_react13.useEffect)(() => {
1718
+ (0, import_react14.useEffect)(() => {
1641
1719
  const handleKeyDown = (event) => {
1642
1720
  if (event.key === "Escape") onClose();
1643
1721
  };
1644
1722
  window.addEventListener("keydown", handleKeyDown);
1645
1723
  return () => window.removeEventListener("keydown", handleKeyDown);
1646
1724
  }, [onClose]);
1647
- return /* @__PURE__ */ (0, import_jsx_runtime25.jsxs)(BasePopup_default, { title: `Change parent of ${genrePlaylist.name}`, onClose, showCancelButton: true, fitContent: true, children: [
1648
- /* @__PURE__ */ (0, import_jsx_runtime25.jsxs)("p", { className: "mb-3 text-sm text-[var(--color-text-muted)]", children: [
1725
+ return /* @__PURE__ */ (0, import_jsx_runtime26.jsxs)(BasePopup_default, { title: `Change parent of ${genrePlaylist.name}`, onClose, showCancelButton: true, fitContent: true, children: [
1726
+ /* @__PURE__ */ (0, import_jsx_runtime26.jsxs)("p", { className: "mb-3 text-sm text-[var(--color-text-muted)]", children: [
1649
1727
  "Current parent: ",
1650
1728
  genrePlaylist.parent?.name ?? "none"
1651
1729
  ] }),
1652
- /* @__PURE__ */ (0, import_jsx_runtime25.jsx)("div", { "aria-busy": isPending, className: isPending ? "pointer-events-none opacity-60" : void 0, children: /* @__PURE__ */ (0, import_jsx_runtime25.jsx)(
1730
+ /* @__PURE__ */ (0, import_jsx_runtime26.jsx)("div", { "aria-busy": isPending, className: isPending ? "pointer-events-none opacity-60" : void 0, children: /* @__PURE__ */ (0, import_jsx_runtime26.jsx)(
1653
1731
  GenreSearch,
1654
1732
  {
1655
1733
  genrePlaylists: candidates,
@@ -1657,13 +1735,13 @@ function ChangeParentPopup({
1657
1735
  placeholder: "Search a new parent\u2026"
1658
1736
  }
1659
1737
  ) }),
1660
- formErrors.length > 0 && /* @__PURE__ */ (0, import_jsx_runtime25.jsx)("p", { role: "alert", className: "mt-3 text-sm text-[var(--color-red-500)]", children: formErrors.map(({ message }) => message).join(" ") })
1738
+ formErrors.length > 0 && /* @__PURE__ */ (0, import_jsx_runtime26.jsx)("p", { role: "alert", className: "mt-3 text-sm text-[var(--color-red-500)]", children: formErrors.map(({ message }) => message).join(" ") })
1661
1739
  ] });
1662
1740
  }
1663
1741
 
1664
1742
  // src/genre-tree/artist/useArtistPanel.tsx
1665
1743
  var import_ui12 = require("@behindthemusictree/ui");
1666
- var import_genre_tree18 = require("@behindthemusictree/app-kit/genre-tree");
1744
+ var import_genre_tree24 = require("@behindthemusictree/app-kit/genre-tree");
1667
1745
 
1668
1746
  // src/genre-tree/artist/ArtistHeader.tsx
1669
1747
  var import_ui8 = require("@behindthemusictree/ui");
@@ -1672,7 +1750,7 @@ var import_ui8 = require("@behindthemusictree/ui");
1672
1750
  var import_lucide_react8 = require("lucide-react");
1673
1751
  var import_si3 = require("react-icons/si");
1674
1752
  var import_ui7 = require("@behindthemusictree/ui");
1675
- var import_jsx_runtime26 = require("react/jsx-runtime");
1753
+ var import_jsx_runtime27 = require("react/jsx-runtime");
1676
1754
  var LINK_KINDS = {
1677
1755
  official: { label: "Official website", Icon: import_lucide_react8.Globe },
1678
1756
  wikipedia: { label: "Wikipedia", Icon: import_si3.SiWikipedia },
@@ -1709,7 +1787,7 @@ function artistLinks(artist) {
1709
1787
  return links;
1710
1788
  }
1711
1789
  function ArtistLinks({ links }) {
1712
- return /* @__PURE__ */ (0, import_jsx_runtime26.jsx)("ul", { className: "flex flex-wrap gap-1.5", children: links.map(({ key, url, Icon, label }) => /* @__PURE__ */ (0, import_jsx_runtime26.jsx)("li", { children: /* @__PURE__ */ (0, import_jsx_runtime26.jsx)(import_ui7.Chip, { href: url, icon: /* @__PURE__ */ (0, import_jsx_runtime26.jsx)(Icon, { className: "h-3.5 w-3.5 shrink-0", "aria-hidden": "true" }), children: label }) }, key)) });
1790
+ return /* @__PURE__ */ (0, import_jsx_runtime27.jsx)("ul", { className: "flex flex-wrap gap-1.5", children: links.map(({ key, url, Icon, label }) => /* @__PURE__ */ (0, import_jsx_runtime27.jsx)("li", { children: /* @__PURE__ */ (0, import_jsx_runtime27.jsx)(import_ui7.Chip, { href: url, icon: /* @__PURE__ */ (0, import_jsx_runtime27.jsx)(Icon, { className: "h-3.5 w-3.5 shrink-0", "aria-hidden": "true" }), children: label }) }, key)) });
1713
1791
  }
1714
1792
 
1715
1793
  // src/genre-tree/artist/formatYears.ts
@@ -1720,7 +1798,7 @@ function formatYears(beginYear, endYear, ended) {
1720
1798
  }
1721
1799
 
1722
1800
  // src/genre-tree/artist/ArtistHeader.tsx
1723
- var import_jsx_runtime27 = require("react/jsx-runtime");
1801
+ var import_jsx_runtime28 = require("react/jsx-runtime");
1724
1802
  var countryNames = new Intl.DisplayNames(["en"], { type: "region" });
1725
1803
  function ArtistHeader({ artist }) {
1726
1804
  const fields = [];
@@ -1730,35 +1808,35 @@ function ArtistHeader({ artist }) {
1730
1808
  const years = formatYears(artist.beginYear, artist.endYear, artist.ended);
1731
1809
  if (years) fields.push({ label: "Active", value: years });
1732
1810
  const links = artistLinks(artist);
1733
- if (links.length > 0) fields.push({ label: "Links", value: /* @__PURE__ */ (0, import_jsx_runtime27.jsx)(ArtistLinks, { links }) });
1811
+ if (links.length > 0) fields.push({ label: "Links", value: /* @__PURE__ */ (0, import_jsx_runtime28.jsx)(ArtistLinks, { links }) });
1734
1812
  if (fields.length === 0) return null;
1735
- return /* @__PURE__ */ (0, import_jsx_runtime27.jsx)(import_ui8.FieldList, { items: fields });
1813
+ return /* @__PURE__ */ (0, import_jsx_runtime28.jsx)(import_ui8.FieldList, { items: fields });
1736
1814
  }
1737
1815
 
1738
1816
  // src/genre-tree/artist/ArtistPhoto.tsx
1739
- var import_jsx_runtime28 = require("react/jsx-runtime");
1817
+ var import_jsx_runtime29 = require("react/jsx-runtime");
1740
1818
  function ExternalLink({ href, children }) {
1741
- return /* @__PURE__ */ (0, import_jsx_runtime28.jsx)("a", { href, target: "_blank", rel: "noopener noreferrer", className: "underline-offset-2 hover:underline", children });
1819
+ return /* @__PURE__ */ (0, import_jsx_runtime29.jsx)("a", { href, target: "_blank", rel: "noopener noreferrer", className: "underline-offset-2 hover:underline", children });
1742
1820
  }
1743
1821
  function ArtistPhoto({ image, name }) {
1744
- return /* @__PURE__ */ (0, import_jsx_runtime28.jsxs)("figure", { className: "flex flex-col gap-1.5 px-6 py-2.5", children: [
1745
- /* @__PURE__ */ (0, import_jsx_runtime28.jsx)("img", { src: image.url, alt: name, className: "h-64 w-full object-contain bg-[var(--color-surface-muted)] rounded" }),
1746
- /* @__PURE__ */ (0, import_jsx_runtime28.jsxs)("figcaption", { className: "text-xs text-[var(--color-text-muted)]", children: [
1822
+ return /* @__PURE__ */ (0, import_jsx_runtime29.jsxs)("figure", { className: "flex flex-col gap-1.5 px-6 py-2.5", children: [
1823
+ /* @__PURE__ */ (0, import_jsx_runtime29.jsx)("img", { src: image.url, alt: name, className: "h-64 w-full object-contain bg-[var(--color-surface-muted)] rounded" }),
1824
+ /* @__PURE__ */ (0, import_jsx_runtime29.jsxs)("figcaption", { className: "text-xs text-[var(--color-text-muted)]", children: [
1747
1825
  "Photo: ",
1748
- /* @__PURE__ */ (0, import_jsx_runtime28.jsx)(ExternalLink, { href: image.pageUrl, children: image.author ?? "Wikimedia Commons" }),
1826
+ /* @__PURE__ */ (0, import_jsx_runtime29.jsx)(ExternalLink, { href: image.pageUrl, children: image.author ?? "Wikimedia Commons" }),
1749
1827
  ",",
1750
1828
  " ",
1751
- image.licenseUrl ? /* @__PURE__ */ (0, import_jsx_runtime28.jsx)(ExternalLink, { href: image.licenseUrl, children: image.license }) : image.license
1829
+ image.licenseUrl ? /* @__PURE__ */ (0, import_jsx_runtime29.jsx)(ExternalLink, { href: image.licenseUrl, children: image.license }) : image.license
1752
1830
  ] })
1753
1831
  ] });
1754
1832
  }
1755
1833
 
1756
1834
  // src/genre-tree/artist/ArtistMembers.tsx
1757
1835
  var import_ui9 = require("@behindthemusictree/ui");
1758
- var import_jsx_runtime29 = require("react/jsx-runtime");
1836
+ var import_jsx_runtime30 = require("react/jsx-runtime");
1759
1837
  function MembershipList({ title, memberships, renderArtistLink }) {
1760
1838
  if (memberships.length === 0) return null;
1761
- return /* @__PURE__ */ (0, import_jsx_runtime29.jsx)(import_ui9.PanelGroup, { title, children: /* @__PURE__ */ (0, import_jsx_runtime29.jsx)("ul", { children: memberships.map((membership, index) => {
1839
+ return /* @__PURE__ */ (0, import_jsx_runtime30.jsx)(import_ui9.PanelGroup, { title, children: /* @__PURE__ */ (0, import_jsx_runtime30.jsx)("ul", { children: memberships.map((membership, index) => {
1762
1840
  const details = [
1763
1841
  formatYears(membership.beginYear, membership.endYear, membership.ended),
1764
1842
  ...membership.roles
@@ -1766,9 +1844,9 @@ function MembershipList({ title, memberships, renderArtistLink }) {
1766
1844
  const detailsText = details.filter(Boolean).join(" \xB7 ");
1767
1845
  return (
1768
1846
  // A member can have several periods in the same group.
1769
- /* @__PURE__ */ (0, import_jsx_runtime29.jsxs)("li", { className: "truncate py-0.5", title: detailsText || void 0, children: [
1770
- /* @__PURE__ */ (0, import_jsx_runtime29.jsx)("span", { className: "font-medium text-[var(--color-text)]", children: renderArtistLink(membership.artist) }),
1771
- detailsText && /* @__PURE__ */ (0, import_jsx_runtime29.jsxs)("span", { className: "text-[var(--color-text-muted)]", children: [
1847
+ /* @__PURE__ */ (0, import_jsx_runtime30.jsxs)("li", { className: "truncate py-0.5", title: detailsText || void 0, children: [
1848
+ /* @__PURE__ */ (0, import_jsx_runtime30.jsx)("span", { className: "font-medium text-[var(--color-text)]", children: renderArtistLink(membership.artist) }),
1849
+ detailsText && /* @__PURE__ */ (0, import_jsx_runtime30.jsxs)("span", { className: "text-[var(--color-text-muted)]", children: [
1772
1850
  " \u2014 ",
1773
1851
  detailsText
1774
1852
  ] })
@@ -1779,9 +1857,9 @@ function MembershipList({ title, memberships, renderArtistLink }) {
1779
1857
  function ArtistMembers({ title, memberships, renderArtistLink }) {
1780
1858
  if (memberships.length === 0) return null;
1781
1859
  const isCurrent = (membership) => !membership.ended && membership.endYear === null;
1782
- return /* @__PURE__ */ (0, import_jsx_runtime29.jsxs)(import_ui9.PanelSection, { title, children: [
1783
- /* @__PURE__ */ (0, import_jsx_runtime29.jsx)(MembershipList, { title: "Current", memberships: memberships.filter(isCurrent), renderArtistLink }),
1784
- /* @__PURE__ */ (0, import_jsx_runtime29.jsx)(
1860
+ return /* @__PURE__ */ (0, import_jsx_runtime30.jsxs)(import_ui9.PanelSection, { title, children: [
1861
+ /* @__PURE__ */ (0, import_jsx_runtime30.jsx)(MembershipList, { title: "Current", memberships: memberships.filter(isCurrent), renderArtistLink }),
1862
+ /* @__PURE__ */ (0, import_jsx_runtime30.jsx)(
1785
1863
  MembershipList,
1786
1864
  {
1787
1865
  title: "Past",
@@ -1794,7 +1872,7 @@ function ArtistMembers({ title, memberships, renderArtistLink }) {
1794
1872
 
1795
1873
  // src/genre-tree/artist/ArtistDiscography.tsx
1796
1874
  var import_ui10 = require("@behindthemusictree/ui");
1797
- var import_jsx_runtime30 = require("react/jsx-runtime");
1875
+ var import_jsx_runtime31 = require("react/jsx-runtime");
1798
1876
  var GROUPS = ["Albums", "EPs", "Singles", "Live", "Compilations", "Other"];
1799
1877
  var PRIMARY_GROUPS = { album: "Albums", ep: "EPs", single: "Singles" };
1800
1878
  function discographyGroup({ primaryType, secondaryTypes }) {
@@ -1814,34 +1892,34 @@ function byYear(a, b) {
1814
1892
  function ArtistDiscography({ albums }) {
1815
1893
  if (albums.length === 0) return null;
1816
1894
  const sorted = [...albums].sort(byYear);
1817
- return /* @__PURE__ */ (0, import_jsx_runtime30.jsx)(import_ui10.PanelSection, { title: "Discography", children: GROUPS.map((group) => {
1895
+ return /* @__PURE__ */ (0, import_jsx_runtime31.jsx)(import_ui10.PanelSection, { title: "Discography", children: GROUPS.map((group) => {
1818
1896
  const groupAlbums = sorted.filter((album) => discographyGroup(album) === group);
1819
1897
  if (groupAlbums.length === 0) return null;
1820
- return /* @__PURE__ */ (0, import_jsx_runtime30.jsx)(import_ui10.PanelGroup, { title: group, children: /* @__PURE__ */ (0, import_jsx_runtime30.jsx)("ul", { children: groupAlbums.map((album) => /* @__PURE__ */ (0, import_jsx_runtime30.jsxs)("li", { className: "grid grid-cols-[2.5rem_minmax(0,1fr)] gap-2 py-0.5", children: [
1821
- /* @__PURE__ */ (0, import_jsx_runtime30.jsx)("span", { className: "tabular-nums text-[var(--color-text-muted)]", children: album.year ?? "\u2014" }),
1822
- /* @__PURE__ */ (0, import_jsx_runtime30.jsx)("span", { className: "truncate text-[var(--color-text)]", title: album.name, children: album.name })
1898
+ return /* @__PURE__ */ (0, import_jsx_runtime31.jsx)(import_ui10.PanelGroup, { title: group, children: /* @__PURE__ */ (0, import_jsx_runtime31.jsx)("ul", { children: groupAlbums.map((album) => /* @__PURE__ */ (0, import_jsx_runtime31.jsxs)("li", { className: "grid grid-cols-[2.5rem_minmax(0,1fr)] gap-2 py-0.5", children: [
1899
+ /* @__PURE__ */ (0, import_jsx_runtime31.jsx)("span", { className: "tabular-nums text-[var(--color-text-muted)]", children: album.year ?? "\u2014" }),
1900
+ /* @__PURE__ */ (0, import_jsx_runtime31.jsx)("span", { className: "truncate text-[var(--color-text)]", title: album.name, children: album.name })
1823
1901
  ] }, album.uuid)) }) }, group);
1824
1902
  }) });
1825
1903
  }
1826
1904
 
1827
1905
  // src/genre-tree/artist/ArtistSongs.tsx
1828
- var import_react14 = require("react");
1906
+ var import_react15 = require("react");
1829
1907
  var import_react_query2 = require("@tanstack/react-query");
1830
1908
  var import_lucide_react9 = require("lucide-react");
1831
1909
  var import_ui11 = require("@behindthemusictree/ui");
1832
1910
  var import_player10 = require("@behindthemusictree/app-kit/player");
1833
- var import_genre_tree17 = require("@behindthemusictree/app-kit/genre-tree");
1911
+ var import_genre_tree23 = require("@behindthemusictree/app-kit/genre-tree");
1834
1912
  var import_transport7 = require("@behindthemusictree/app-kit/transport");
1835
1913
 
1836
1914
  // src/genre-tree/track-list-sidebar/TrackItem.tsx
1837
1915
  var import_player9 = require("@behindthemusictree/app-kit/player");
1838
- var import_genre_tree14 = require("@behindthemusictree/app-kit/genre-tree");
1916
+ var import_genre_tree20 = require("@behindthemusictree/app-kit/genre-tree");
1839
1917
 
1840
1918
  // src/genre-tree/TrackPositionPlayPause.tsx
1841
1919
  var import_fa3 = require("react-icons/fa");
1842
1920
  var import_player7 = require("@behindthemusictree/app-kit/player");
1843
1921
  var import_player8 = require("@behindthemusictree/app-kit/player");
1844
- var import_jsx_runtime31 = require("react/jsx-runtime");
1922
+ var import_jsx_runtime32 = require("react/jsx-runtime");
1845
1923
  function TrackPositionPlayPause({
1846
1924
  position,
1847
1925
  uuid,
@@ -1855,14 +1933,14 @@ function TrackPositionPlayPause({
1855
1933
  const isPlaying = isThisTrack && isConfirmedPlayable && playState === import_player8.PlayStates.PLAYING;
1856
1934
  const isPaused = isThisTrack && isConfirmedPlayable && playState === import_player8.PlayStates.PAUSED;
1857
1935
  const isCurrentTrackActive = isPlaying || isPaused;
1858
- return /* @__PURE__ */ (0, import_jsx_runtime31.jsxs)(
1936
+ return /* @__PURE__ */ (0, import_jsx_runtime32.jsxs)(
1859
1937
  "div",
1860
1938
  {
1861
1939
  className: "track-position-play-pause relative flex items-center justify-center gap-2 text-lg w-16 cursor-pointer",
1862
1940
  style: { minWidth: "4rem", maxWidth: "4rem" },
1863
1941
  onClick: handlePlayPauseClick,
1864
1942
  children: [
1865
- /* @__PURE__ */ (0, import_jsx_runtime31.jsx)("div", { className: "track-position-play-pause__status group-hover:hidden flex items-center justify-center", children: isAwaitingPlayback ? /* @__PURE__ */ (0, import_jsx_runtime31.jsx)("span", { className: "track-position-play-pause__spinner", role: "status", "aria-label": "Loading track" }) : isCurrentTrackActive ? /* @__PURE__ */ (0, import_jsx_runtime31.jsxs)(
1943
+ /* @__PURE__ */ (0, import_jsx_runtime32.jsx)("div", { className: "track-position-play-pause__status group-hover:hidden flex items-center justify-center", children: isAwaitingPlayback ? /* @__PURE__ */ (0, import_jsx_runtime32.jsx)("span", { className: "track-position-play-pause__spinner", role: "status", "aria-label": "Loading track" }) : isCurrentTrackActive ? /* @__PURE__ */ (0, import_jsx_runtime32.jsxs)(
1866
1944
  "span",
1867
1945
  {
1868
1946
  className: "track-position-play-pause__bars",
@@ -1870,18 +1948,18 @@ function TrackPositionPlayPause({
1870
1948
  role: "img",
1871
1949
  "aria-label": isPlaying ? "Playing" : "Paused",
1872
1950
  children: [
1873
- /* @__PURE__ */ (0, import_jsx_runtime31.jsx)("span", { className: "track-position-play-pause__bar" }),
1874
- /* @__PURE__ */ (0, import_jsx_runtime31.jsx)("span", { className: "track-position-play-pause__bar" }),
1875
- /* @__PURE__ */ (0, import_jsx_runtime31.jsx)("span", { className: "track-position-play-pause__bar" })
1951
+ /* @__PURE__ */ (0, import_jsx_runtime32.jsx)("span", { className: "track-position-play-pause__bar" }),
1952
+ /* @__PURE__ */ (0, import_jsx_runtime32.jsx)("span", { className: "track-position-play-pause__bar" }),
1953
+ /* @__PURE__ */ (0, import_jsx_runtime32.jsx)("span", { className: "track-position-play-pause__bar" })
1876
1954
  ]
1877
1955
  }
1878
1956
  ) : position }),
1879
- /* @__PURE__ */ (0, import_jsx_runtime31.jsx)(
1957
+ /* @__PURE__ */ (0, import_jsx_runtime32.jsx)(
1880
1958
  "div",
1881
1959
  {
1882
1960
  className: `hidden group-hover:flex items-center justify-center ${isCurrentTrackActive ? "" : "absolute inset-0"}`,
1883
1961
  "aria-hidden": "true",
1884
- children: isAwaitingPlayback ? /* @__PURE__ */ (0, import_jsx_runtime31.jsx)("span", { className: "track-position-play-pause__spinner" }) : isPlaying ? /* @__PURE__ */ (0, import_jsx_runtime31.jsx)(import_fa3.FaPause, {}) : /* @__PURE__ */ (0, import_jsx_runtime31.jsx)(import_fa3.FaPlay, {})
1962
+ children: isAwaitingPlayback ? /* @__PURE__ */ (0, import_jsx_runtime32.jsx)("span", { className: "track-position-play-pause__spinner" }) : isPlaying ? /* @__PURE__ */ (0, import_jsx_runtime32.jsx)(import_fa3.FaPause, {}) : /* @__PURE__ */ (0, import_jsx_runtime32.jsx)(import_fa3.FaPlay, {})
1885
1963
  }
1886
1964
  )
1887
1965
  ]
@@ -1890,9 +1968,9 @@ function TrackPositionPlayPause({
1890
1968
  }
1891
1969
 
1892
1970
  // src/genre-tree/track-list-sidebar/TrackItem.tsx
1893
- var import_genre_tree15 = require("@behindthemusictree/app-kit/genre-tree");
1894
- var import_genre_tree16 = require("@behindthemusictree/app-kit/genre-tree");
1895
- var import_jsx_runtime32 = require("react/jsx-runtime");
1971
+ var import_genre_tree21 = require("@behindthemusictree/app-kit/genre-tree");
1972
+ var import_genre_tree22 = require("@behindthemusictree/app-kit/genre-tree");
1973
+ var import_jsx_runtime33 = require("react/jsx-runtime");
1896
1974
  function TrackItem({
1897
1975
  track,
1898
1976
  position,
@@ -1902,11 +1980,11 @@ function TrackItem({
1902
1980
  renderSongLink
1903
1981
  }) {
1904
1982
  const { handlePlayPauseAction, playerTrackObject, loadTrackForPlayer } = (0, import_player9.usePlayer)();
1905
- const { trackList, selectedTrack, toTrackAtPosition, runtimeUnplayableTrackIds } = (0, import_genre_tree14.useTrackList)();
1983
+ const { trackList, selectedTrack, toTrackAtPosition, runtimeUnplayableTrackIds } = (0, import_genre_tree20.useTrackList)();
1906
1984
  const scope = trackList?.origin?.scope ?? null;
1907
1985
  const isCurrentTrack = playerTrackObject?.track.id === track.uuid;
1908
1986
  const hasPlaybackError = isCurrentTrack && !!playerTrackObject?.loadError;
1909
- const isUnplayable = hasPlaybackError || runtimeUnplayableTrackIds?.has(track.uuid) === true || (0, import_genre_tree15.isSong)(track) && (0, import_genre_tree15.songUnplayableReason)(track) !== null;
1987
+ const isUnplayable = hasPlaybackError || runtimeUnplayableTrackIds?.has(track.uuid) === true || (0, import_genre_tree21.isSong)(track) && (0, import_genre_tree21.songUnplayableReason)(track) !== null;
1910
1988
  const handlePlayPauseClick = (event) => {
1911
1989
  event.stopPropagation();
1912
1990
  if (isCurrentTrack) {
@@ -1916,12 +1994,12 @@ function TrackItem({
1916
1994
  loadTrackForPlayer(track.uuid);
1917
1995
  }
1918
1996
  };
1919
- return /* @__PURE__ */ (0, import_jsx_runtime32.jsxs)(
1997
+ return /* @__PURE__ */ (0, import_jsx_runtime33.jsxs)(
1920
1998
  "div",
1921
1999
  {
1922
2000
  className: `track-item flex h-14 text-[var(--color-text)] ${isUnplayable ? "opacity-50" : "hover:bg-[var(--color-surface-muted)] group"}`,
1923
2001
  children: [
1924
- isUnplayable ? /* @__PURE__ */ (0, import_jsx_runtime32.jsx)(
2002
+ isUnplayable ? /* @__PURE__ */ (0, import_jsx_runtime33.jsx)(
1925
2003
  "div",
1926
2004
  {
1927
2005
  className: "flex items-center justify-center text-lg w-16",
@@ -1929,7 +2007,7 @@ function TrackItem({
1929
2007
  "aria-disabled": "true",
1930
2008
  children: position
1931
2009
  }
1932
- ) : /* @__PURE__ */ (0, import_jsx_runtime32.jsx)(
2010
+ ) : /* @__PURE__ */ (0, import_jsx_runtime33.jsx)(
1933
2011
  TrackPositionPlayPause,
1934
2012
  {
1935
2013
  position,
@@ -1938,12 +2016,12 @@ function TrackItem({
1938
2016
  isSelected: selectedTrack?.uuid === track.uuid
1939
2017
  }
1940
2018
  ),
1941
- /* @__PURE__ */ (0, import_jsx_runtime32.jsxs)("div", { className: "title-artist-container flex flex-col items-start justify-center min-w-0 flex-1", children: [
1942
- /* @__PURE__ */ (0, import_jsx_runtime32.jsx)("div", { className: "flex max-w-full items-center gap-2", children: /* @__PURE__ */ (0, import_jsx_runtime32.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_runtime32.jsx)(MusicBrainzRecordingLink, { song: track, className: "hover:underline focus-visible:underline", children: track.title }) : track.title }) }),
1943
- track.artists && track.artists.length > 0 ? /* @__PURE__ */ (0, import_jsx_runtime32.jsx)("div", { className: "artist text-base max-w-full truncate text-[var(--color-text-muted)]", children: /* @__PURE__ */ (0, import_jsx_runtime32.jsx)(import_genre_tree16.ArtistNames, { artists: track.artists, renderArtistLink }) }) : ""
2019
+ /* @__PURE__ */ (0, import_jsx_runtime33.jsxs)("div", { className: "title-artist-container flex flex-col items-start justify-center min-w-0 flex-1", children: [
2020
+ /* @__PURE__ */ (0, import_jsx_runtime33.jsx)("div", { className: "flex max-w-full items-center gap-2", children: /* @__PURE__ */ (0, import_jsx_runtime33.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_runtime33.jsx)(MusicBrainzRecordingLink, { song: track, className: "hover:underline focus-visible:underline", children: track.title }) : track.title }) }),
2021
+ track.artists && track.artists.length > 0 ? /* @__PURE__ */ (0, import_jsx_runtime33.jsx)("div", { className: "artist text-base max-w-full truncate text-[var(--color-text-muted)]", children: /* @__PURE__ */ (0, import_jsx_runtime33.jsx)(import_genre_tree22.ArtistNames, { artists: track.artists, renderArtistLink }) }) : ""
1944
2022
  ] }),
1945
- /* @__PURE__ */ (0, import_jsx_runtime32.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_runtime32.jsx)("div", { className: "genre-name max-w-full whitespace-normal break-words p-1 text-xs font-bold", children: track.genre.name }) : "" }),
1946
- renderDuration && /* @__PURE__ */ (0, import_jsx_runtime32.jsx)(
2023
+ /* @__PURE__ */ (0, import_jsx_runtime33.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_runtime33.jsx)("div", { className: "genre-name max-w-full whitespace-normal break-words p-1 text-xs font-bold", children: track.genre.name }) : "" }),
2024
+ renderDuration && /* @__PURE__ */ (0, import_jsx_runtime33.jsx)(
1947
2025
  "div",
1948
2026
  {
1949
2027
  className: "duration flex text-base w-16 items-center justify-center",
@@ -1951,28 +2029,28 @@ function TrackItem({
1951
2029
  children: renderDuration(track)
1952
2030
  }
1953
2031
  ),
1954
- renderActions && /* @__PURE__ */ (0, import_jsx_runtime32.jsx)("div", { className: "edit flex text-base w-6 items-center justify-center mr-2 cursor-pointer", children: renderActions(track) })
2032
+ renderActions && /* @__PURE__ */ (0, import_jsx_runtime33.jsx)("div", { className: "edit flex text-base w-6 items-center justify-center mr-2 cursor-pointer", children: renderActions(track) })
1955
2033
  ]
1956
2034
  }
1957
2035
  );
1958
2036
  }
1959
2037
 
1960
2038
  // src/genre-tree/artist/ArtistSongs.tsx
1961
- var import_jsx_runtime33 = require("react/jsx-runtime");
1962
- var SongPageSchema2 = (0, import_transport7.PaginatedResponseSchema)(import_genre_tree17.SongDetailedSchema);
2039
+ var import_jsx_runtime34 = require("react/jsx-runtime");
2040
+ var SongPageSchema2 = (0, import_transport7.PaginatedResponseSchema)(import_genre_tree23.SongDetailedSchema);
1963
2041
  var SORTS = [
1964
2042
  { ordering: "title", label: "Title" },
1965
2043
  { ordering: "-albumYear", label: "Year" }
1966
2044
  ];
1967
2045
  function ArtistSongs({ artist, getBackendBaseUrl, renderArtistLink, renderSongLink }) {
1968
- const [page, setPage] = (0, import_react14.useState)(1);
1969
- const [ordering, setOrdering] = (0, import_react14.useState)("title");
2046
+ const [page, setPage] = (0, import_react15.useState)(1);
2047
+ const [ordering, setOrdering] = (0, import_react15.useState)("title");
1970
2048
  const { fetch } = (0, import_transport7.useFetchWrapper)(getBackendBaseUrl);
1971
- const { playNewTrackListFromArtist } = (0, import_genre_tree17.useTrackList)();
2049
+ const { playNewTrackListFromArtist } = (0, import_genre_tree23.useTrackList)();
1972
2050
  const { playerTrackObject } = (0, import_player10.usePlayer)();
1973
2051
  const { data, isPending, isError } = (0, import_transport7.useQueryWithParse)({
1974
2052
  queryKey: ["referenceSongs", "artist", artist.uuid, ordering, page],
1975
- queryFn: () => fetch(import_genre_tree17.libraryEndpoints.reference.songs.list(), true, false, void 0, {
2053
+ queryFn: () => fetch(import_genre_tree23.libraryEndpoints.reference.songs.list(), true, false, void 0, {
1976
2054
  artist: artist.uuid,
1977
2055
  ordering,
1978
2056
  page
@@ -1987,25 +2065,25 @@ function ArtistSongs({ artist, getBackendBaseUrl, renderArtistLink, renderSongLi
1987
2065
  });
1988
2066
  const handleRowClickCapture = (event, song) => {
1989
2067
  if (!(event.target instanceof Element) || !event.target.closest(".track-position-play-pause")) return;
1990
- if (playerTrackObject?.track.id === song.uuid || (0, import_genre_tree17.songUnplayableReason)(song) !== null) return;
2068
+ if (playerTrackObject?.track.id === song.uuid || (0, import_genre_tree23.songUnplayableReason)(song) !== null) return;
1991
2069
  event.stopPropagation();
1992
2070
  void play(song.uuid);
1993
2071
  };
1994
- return /* @__PURE__ */ (0, import_jsx_runtime33.jsx)(import_ui11.PanelSection, { title: "Songs", children: isPending ? /* @__PURE__ */ (0, import_jsx_runtime33.jsx)(import_ui11.Skeleton, { className: "h-14", "aria-busy": "true" }) : isError ? /* @__PURE__ */ (0, import_jsx_runtime33.jsx)("p", { className: "text-red-600", children: "Could not load songs." }) : data.results.length === 0 ? /* @__PURE__ */ (0, import_jsx_runtime33.jsx)("p", { className: "text-[var(--color-text-muted)]", children: "No songs yet." }) : /* @__PURE__ */ (0, import_jsx_runtime33.jsxs)(import_jsx_runtime33.Fragment, { children: [
1995
- /* @__PURE__ */ (0, import_jsx_runtime33.jsxs)("div", { className: "flex flex-wrap items-center justify-between gap-2 mb-2", children: [
1996
- /* @__PURE__ */ (0, import_jsx_runtime33.jsxs)(
2072
+ return /* @__PURE__ */ (0, import_jsx_runtime34.jsx)(import_ui11.PanelSection, { title: "Songs", children: isPending ? /* @__PURE__ */ (0, import_jsx_runtime34.jsx)(import_ui11.Skeleton, { className: "h-14", "aria-busy": "true" }) : isError ? /* @__PURE__ */ (0, import_jsx_runtime34.jsx)("p", { className: "text-red-600", children: "Could not load songs." }) : data.results.length === 0 ? /* @__PURE__ */ (0, import_jsx_runtime34.jsx)("p", { className: "text-[var(--color-text-muted)]", children: "No songs yet." }) : /* @__PURE__ */ (0, import_jsx_runtime34.jsxs)(import_jsx_runtime34.Fragment, { children: [
2073
+ /* @__PURE__ */ (0, import_jsx_runtime34.jsxs)("div", { className: "flex flex-wrap items-center justify-between gap-2 mb-2", children: [
2074
+ /* @__PURE__ */ (0, import_jsx_runtime34.jsxs)(
1997
2075
  "button",
1998
2076
  {
1999
2077
  type: "button",
2000
2078
  onClick: () => void play(),
2001
2079
  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",
2002
2080
  children: [
2003
- /* @__PURE__ */ (0, import_jsx_runtime33.jsx)(import_lucide_react9.Play, { className: "h-3.5 w-3.5", "aria-hidden": "true" }),
2081
+ /* @__PURE__ */ (0, import_jsx_runtime34.jsx)(import_lucide_react9.Play, { className: "h-3.5 w-3.5", "aria-hidden": "true" }),
2004
2082
  "Play all"
2005
2083
  ]
2006
2084
  }
2007
2085
  ),
2008
- /* @__PURE__ */ (0, import_jsx_runtime33.jsx)("div", { role: "group", "aria-label": "Sort songs by", className: "flex gap-1 text-xs", children: SORTS.map((sort) => /* @__PURE__ */ (0, import_jsx_runtime33.jsx)(
2086
+ /* @__PURE__ */ (0, import_jsx_runtime34.jsx)("div", { role: "group", "aria-label": "Sort songs by", className: "flex gap-1 text-xs", children: SORTS.map((sort) => /* @__PURE__ */ (0, import_jsx_runtime34.jsx)(
2009
2087
  "button",
2010
2088
  {
2011
2089
  type: "button",
@@ -2020,7 +2098,7 @@ function ArtistSongs({ artist, getBackendBaseUrl, renderArtistLink, renderSongLi
2020
2098
  sort.ordering
2021
2099
  )) })
2022
2100
  ] }),
2023
- /* @__PURE__ */ (0, import_jsx_runtime33.jsx)("ul", { className: "overflow-hidden mb-2 bg-zinc-950 rounded-lg", children: data.results.map((song, index) => /* @__PURE__ */ (0, import_jsx_runtime33.jsx)("li", { onClickCapture: (event) => handleRowClickCapture(event, song), children: /* @__PURE__ */ (0, import_jsx_runtime33.jsx)(
2101
+ /* @__PURE__ */ (0, import_jsx_runtime34.jsx)("ul", { className: "overflow-hidden mb-2 bg-zinc-950 rounded-lg", children: data.results.map((song, index) => /* @__PURE__ */ (0, import_jsx_runtime34.jsx)("li", { onClickCapture: (event) => handleRowClickCapture(event, song), children: /* @__PURE__ */ (0, import_jsx_runtime34.jsx)(
2024
2102
  TrackItem,
2025
2103
  {
2026
2104
  track: song,
@@ -2029,12 +2107,12 @@ function ArtistSongs({ artist, getBackendBaseUrl, renderArtistLink, renderSongLi
2029
2107
  renderSongLink
2030
2108
  }
2031
2109
  ) }, song.uuid)) }),
2032
- /* @__PURE__ */ (0, import_jsx_runtime33.jsx)(import_ui11.Pagination, { currentPage: page, totalPages: data.totalPages, onPageChange: setPage })
2110
+ /* @__PURE__ */ (0, import_jsx_runtime34.jsx)(import_ui11.Pagination, { currentPage: page, totalPages: data.totalPages, onPageChange: setPage })
2033
2111
  ] }) });
2034
2112
  }
2035
2113
 
2036
2114
  // src/genre-tree/artist/useArtistPanel.tsx
2037
- var import_jsx_runtime34 = require("react/jsx-runtime");
2115
+ var import_jsx_runtime35 = require("react/jsx-runtime");
2038
2116
  function useArtistPanel({
2039
2117
  uuid,
2040
2118
  scope,
@@ -2046,7 +2124,7 @@ function useArtistPanel({
2046
2124
  onExpandedChange,
2047
2125
  onClose
2048
2126
  }) {
2049
- const result = (0, import_genre_tree18.useArtist)(uuid, getBackendBaseUrl);
2127
+ const result = (0, import_genre_tree24.useArtist)(uuid, getBackendBaseUrl);
2050
2128
  const panel = { expanded, onExpandedChange, onClose };
2051
2129
  switch (result.status) {
2052
2130
  case "idle":
@@ -2055,26 +2133,26 @@ function useArtistPanel({
2055
2133
  return {
2056
2134
  ...panel,
2057
2135
  title: "Artist",
2058
- content: /* @__PURE__ */ (0, import_jsx_runtime34.jsxs)("div", { "aria-busy": "true", className: "flex flex-col gap-2.5 px-6 py-2.5", children: [
2059
- /* @__PURE__ */ (0, import_jsx_runtime34.jsx)(import_ui12.Skeleton, { className: "h-3 w-16" }),
2060
- /* @__PURE__ */ (0, import_jsx_runtime34.jsx)(import_ui12.Skeleton, { className: "h-4 w-32" }),
2061
- /* @__PURE__ */ (0, import_jsx_runtime34.jsx)(import_ui12.Skeleton, { className: "h-3 w-16" }),
2062
- /* @__PURE__ */ (0, import_jsx_runtime34.jsx)(import_ui12.Skeleton, { className: "h-4 w-24" }),
2063
- /* @__PURE__ */ (0, import_jsx_runtime34.jsx)(import_ui12.Skeleton, { className: "h-16 mt-2.5" }),
2064
- /* @__PURE__ */ (0, import_jsx_runtime34.jsx)(import_ui12.Skeleton, { className: "h-16" })
2136
+ content: /* @__PURE__ */ (0, import_jsx_runtime35.jsxs)("div", { "aria-busy": "true", className: "flex flex-col gap-2.5 px-6 py-2.5", children: [
2137
+ /* @__PURE__ */ (0, import_jsx_runtime35.jsx)(import_ui12.Skeleton, { className: "h-3 w-16" }),
2138
+ /* @__PURE__ */ (0, import_jsx_runtime35.jsx)(import_ui12.Skeleton, { className: "h-4 w-32" }),
2139
+ /* @__PURE__ */ (0, import_jsx_runtime35.jsx)(import_ui12.Skeleton, { className: "h-3 w-16" }),
2140
+ /* @__PURE__ */ (0, import_jsx_runtime35.jsx)(import_ui12.Skeleton, { className: "h-4 w-24" }),
2141
+ /* @__PURE__ */ (0, import_jsx_runtime35.jsx)(import_ui12.Skeleton, { className: "h-16 mt-2.5" }),
2142
+ /* @__PURE__ */ (0, import_jsx_runtime35.jsx)(import_ui12.Skeleton, { className: "h-16" })
2065
2143
  ] })
2066
2144
  };
2067
2145
  case "not-found":
2068
2146
  return {
2069
2147
  ...panel,
2070
2148
  title: "Artist not found",
2071
- content: /* @__PURE__ */ (0, import_jsx_runtime34.jsx)("p", { className: "px-6 py-2.5 text-[var(--color-text-muted)]", children: "This artist does not exist." })
2149
+ content: /* @__PURE__ */ (0, import_jsx_runtime35.jsx)("p", { className: "px-6 py-2.5 text-[var(--color-text-muted)]", children: "This artist does not exist." })
2072
2150
  };
2073
2151
  case "error":
2074
2152
  return {
2075
2153
  ...panel,
2076
2154
  title: "Artist",
2077
- content: /* @__PURE__ */ (0, import_jsx_runtime34.jsx)("p", { className: "px-6 py-2.5 text-red-600", children: "Could not load this artist." })
2155
+ content: /* @__PURE__ */ (0, import_jsx_runtime35.jsx)("p", { className: "px-6 py-2.5 text-red-600", children: "Could not load this artist." })
2078
2156
  };
2079
2157
  case "success": {
2080
2158
  const { artist } = result;
@@ -2083,13 +2161,13 @@ function useArtistPanel({
2083
2161
  return {
2084
2162
  ...panel,
2085
2163
  title: artist.name,
2086
- content: /* @__PURE__ */ (0, import_jsx_runtime34.jsxs)(import_jsx_runtime34.Fragment, { children: [
2087
- shareUrl && /* @__PURE__ */ (0, import_jsx_runtime34.jsx)("div", { className: "genre-action-band border-b border-[var(--color-border)]", children: /* @__PURE__ */ (0, import_jsx_runtime34.jsx)("div", { className: "flex flex-wrap gap-1 px-3.5 py-3", children: shareTarget && /* @__PURE__ */ (0, import_jsx_runtime34.jsx)(ShareActionButton, { target: shareTarget, url: shareUrl }) }) }),
2088
- artist.image && /* @__PURE__ */ (0, import_jsx_runtime34.jsx)(ArtistPhoto, { image: artist.image, name: artist.name }),
2089
- /* @__PURE__ */ (0, import_jsx_runtime34.jsx)(ArtistHeader, { artist }),
2090
- /* @__PURE__ */ (0, import_jsx_runtime34.jsx)(ArtistMembers, { title: "Members", memberships: artist.members, renderArtistLink }),
2091
- /* @__PURE__ */ (0, import_jsx_runtime34.jsx)(ArtistMembers, { title: "Member of", memberships: artist.memberOf, renderArtistLink }),
2092
- artist.tracksCount > 0 && /* @__PURE__ */ (0, import_jsx_runtime34.jsx)(
2164
+ content: /* @__PURE__ */ (0, import_jsx_runtime35.jsxs)(import_jsx_runtime35.Fragment, { children: [
2165
+ shareUrl && /* @__PURE__ */ (0, import_jsx_runtime35.jsx)("div", { className: "genre-action-band border-b border-[var(--color-border)]", children: /* @__PURE__ */ (0, import_jsx_runtime35.jsx)("div", { className: "flex flex-wrap gap-1 px-3.5 py-3", children: shareTarget && /* @__PURE__ */ (0, import_jsx_runtime35.jsx)(ShareActionButton, { target: shareTarget, url: shareUrl }) }) }),
2166
+ artist.image && /* @__PURE__ */ (0, import_jsx_runtime35.jsx)(ArtistPhoto, { image: artist.image, name: artist.name }),
2167
+ /* @__PURE__ */ (0, import_jsx_runtime35.jsx)(ArtistHeader, { artist }),
2168
+ /* @__PURE__ */ (0, import_jsx_runtime35.jsx)(ArtistMembers, { title: "Members", memberships: artist.members, renderArtistLink }),
2169
+ /* @__PURE__ */ (0, import_jsx_runtime35.jsx)(ArtistMembers, { title: "Member of", memberships: artist.memberOf, renderArtistLink }),
2170
+ artist.tracksCount > 0 && /* @__PURE__ */ (0, import_jsx_runtime35.jsx)(
2093
2171
  ArtistSongs,
2094
2172
  {
2095
2173
  artist: { uuid: artist.uuid, name: artist.name },
@@ -2099,7 +2177,7 @@ function useArtistPanel({
2099
2177
  }
2100
2178
  )
2101
2179
  ] }),
2102
- extraContent: artist.albums.length > 0 ? /* @__PURE__ */ (0, import_jsx_runtime34.jsx)(ArtistDiscography, { albums: artist.albums }) : void 0
2180
+ extraContent: artist.albums.length > 0 ? /* @__PURE__ */ (0, import_jsx_runtime35.jsx)(ArtistDiscography, { albums: artist.albums }) : void 0
2103
2181
  };
2104
2182
  }
2105
2183
  }
@@ -2107,48 +2185,48 @@ function useArtistPanel({
2107
2185
 
2108
2186
  // src/genre-tree/song/useSongPanel.tsx
2109
2187
  var import_ui14 = require("@behindthemusictree/ui");
2110
- var import_genre_tree20 = require("@behindthemusictree/app-kit/genre-tree");
2188
+ var import_genre_tree26 = require("@behindthemusictree/app-kit/genre-tree");
2111
2189
 
2112
2190
  // src/genre-tree/song/SongPanelContent.tsx
2113
- var import_react15 = require("react");
2191
+ var import_react16 = require("react");
2114
2192
  var import_lucide_react10 = require("lucide-react");
2115
2193
  var import_ui13 = require("@behindthemusictree/ui");
2116
2194
  var import_player11 = require("@behindthemusictree/app-kit/player");
2117
- var import_genre_tree19 = require("@behindthemusictree/app-kit/genre-tree");
2118
- var import_jsx_runtime35 = require("react/jsx-runtime");
2195
+ var import_genre_tree25 = require("@behindthemusictree/app-kit/genre-tree");
2196
+ var import_jsx_runtime36 = require("react/jsx-runtime");
2119
2197
  var numberFormat = new Intl.NumberFormat("en");
2120
2198
  var dateFormat = new Intl.DateTimeFormat("en", { dateStyle: "medium" });
2121
2199
  var externalLink = "text-[var(--color-primary)] hover:underline focus-visible:underline";
2122
2200
  function SongHistory({ uuid, getBackendBaseUrl }) {
2123
- const [open, setOpen] = (0, import_react15.useState)(false);
2124
- const history = (0, import_genre_tree19.useSongHistory)(uuid, getBackendBaseUrl, open);
2201
+ const [open, setOpen] = (0, import_react16.useState)(false);
2202
+ const history = (0, import_genre_tree25.useSongHistory)(uuid, getBackendBaseUrl, open);
2125
2203
  let body = null;
2126
- if (history.status === "loading") body = /* @__PURE__ */ (0, import_jsx_runtime35.jsx)(import_ui13.Skeleton, { className: "h-10", "aria-busy": "true" });
2127
- else if (history.status === "error") body = /* @__PURE__ */ (0, import_jsx_runtime35.jsx)("p", { className: "text-red-600", children: "Could not load the history." });
2204
+ if (history.status === "loading") body = /* @__PURE__ */ (0, import_jsx_runtime36.jsx)(import_ui13.Skeleton, { className: "h-10", "aria-busy": "true" });
2205
+ else if (history.status === "error") body = /* @__PURE__ */ (0, import_jsx_runtime36.jsx)("p", { className: "text-red-600", children: "Could not load the history." });
2128
2206
  else if (history.status === "success")
2129
- body = history.entries.length === 0 ? /* @__PURE__ */ (0, import_jsx_runtime35.jsx)("p", { className: "text-[var(--color-text-muted)]", children: "No changes yet." }) : /* @__PURE__ */ (0, import_jsx_runtime35.jsx)("ul", { className: "divide-y divide-[var(--color-border)]", children: history.entries.map((entry) => /* @__PURE__ */ (0, import_jsx_runtime35.jsxs)("li", { className: "py-1.5 text-sm", children: [
2130
- /* @__PURE__ */ (0, import_jsx_runtime35.jsx)("span", { className: "font-medium", children: entry.action }),
2131
- entry.oldValue !== null || entry.newValue !== null ? /* @__PURE__ */ (0, import_jsx_runtime35.jsxs)("span", { className: "text-[var(--color-text-muted)]", children: [
2207
+ body = history.entries.length === 0 ? /* @__PURE__ */ (0, import_jsx_runtime36.jsx)("p", { className: "text-[var(--color-text-muted)]", children: "No changes yet." }) : /* @__PURE__ */ (0, import_jsx_runtime36.jsx)("ul", { className: "divide-y divide-[var(--color-border)]", children: history.entries.map((entry) => /* @__PURE__ */ (0, import_jsx_runtime36.jsxs)("li", { className: "py-1.5 text-sm", children: [
2208
+ /* @__PURE__ */ (0, import_jsx_runtime36.jsx)("span", { className: "font-medium", children: entry.action }),
2209
+ entry.oldValue !== null || entry.newValue !== null ? /* @__PURE__ */ (0, import_jsx_runtime36.jsxs)("span", { className: "text-[var(--color-text-muted)]", children: [
2132
2210
  " ",
2133
2211
  "\u2014 ",
2134
2212
  entry.oldValue ?? "none",
2135
2213
  " \u2192 ",
2136
2214
  entry.newValue ?? "none"
2137
2215
  ] }) : null,
2138
- /* @__PURE__ */ (0, import_jsx_runtime35.jsxs)("span", { className: "block text-xs text-[var(--color-text-muted)]", children: [
2216
+ /* @__PURE__ */ (0, import_jsx_runtime36.jsxs)("span", { className: "block text-xs text-[var(--color-text-muted)]", children: [
2139
2217
  entry.actorPseudo ?? "Anonymous",
2140
2218
  " \xB7 ",
2141
2219
  dateFormat.format(new Date(entry.createdOn))
2142
2220
  ] })
2143
2221
  ] }, entry.uuid)) });
2144
- return /* @__PURE__ */ (0, import_jsx_runtime35.jsxs)(
2222
+ return /* @__PURE__ */ (0, import_jsx_runtime36.jsxs)(
2145
2223
  "details",
2146
2224
  {
2147
2225
  onToggle: (event) => setOpen(event.currentTarget.open),
2148
2226
  className: "px-6 pb-2.5 mt-2.5 border-t border-[var(--color-border)]",
2149
2227
  children: [
2150
- /* @__PURE__ */ (0, import_jsx_runtime35.jsx)("summary", { className: "mt-2.5 cursor-pointer text-[0.6875rem] font-semibold uppercase text-[var(--color-text-muted)]", children: "History" }),
2151
- /* @__PURE__ */ (0, import_jsx_runtime35.jsx)("div", { className: "mt-1.5", children: body })
2228
+ /* @__PURE__ */ (0, import_jsx_runtime36.jsx)("summary", { className: "mt-2.5 cursor-pointer text-[0.6875rem] font-semibold uppercase text-[var(--color-text-muted)]", children: "History" }),
2229
+ /* @__PURE__ */ (0, import_jsx_runtime36.jsx)("div", { className: "mt-1.5", children: body })
2152
2230
  ]
2153
2231
  }
2154
2232
  );
@@ -2162,25 +2240,25 @@ function SongPanelContent({
2162
2240
  genrePath,
2163
2241
  onSelectGenre
2164
2242
  }) {
2165
- const { playNewTrackListFromTrackUuid } = (0, import_genre_tree19.useTrackList)();
2243
+ const { playNewTrackListFromTrackUuid } = (0, import_genre_tree25.useTrackList)();
2166
2244
  const { playerTrackObject, playState, handlePlayPauseAction } = (0, import_player11.usePlayer)();
2167
- const unplayableReason = (0, import_genre_tree19.songUnplayableReason)(song);
2245
+ const unplayableReason = (0, import_genre_tree25.songUnplayableReason)(song);
2168
2246
  const isCurrent = playerTrackObject?.track.id === song.uuid;
2169
2247
  const isPlaying = isCurrent && playState === import_player11.PlayStates.PLAYING;
2170
2248
  const shareTarget = { kind: "song", uuid: song.uuid, name: song.title, scope };
2171
- const musicBrainzUrl = (0, import_genre_tree19.musicBrainzRecordingUrl)(song);
2249
+ const musicBrainzUrl = (0, import_genre_tree25.musicBrainzRecordingUrl)(song);
2172
2250
  const fields = [];
2173
2251
  if (song.genre) {
2174
2252
  const path = genrePath.length > 0 ? genrePath : null;
2175
2253
  fields.push({
2176
2254
  label: "Genre",
2177
- value: path ? /* @__PURE__ */ (0, import_jsx_runtime35.jsx)("span", { className: "flex flex-wrap items-center gap-x-1", children: path.map((genrePlaylist, index) => /* @__PURE__ */ (0, import_jsx_runtime35.jsxs)("span", { children: [
2178
- index > 0 && /* @__PURE__ */ (0, import_jsx_runtime35.jsx)("span", { "aria-hidden": "true", children: "\u203A " }),
2179
- /* @__PURE__ */ (0, import_jsx_runtime35.jsx)("button", { type: "button", onClick: () => onSelectGenre(genrePlaylist), className: "hover:underline", children: genrePlaylist.name })
2255
+ value: path ? /* @__PURE__ */ (0, import_jsx_runtime36.jsx)("span", { className: "flex flex-wrap items-center gap-x-1", children: path.map((genrePlaylist, index) => /* @__PURE__ */ (0, import_jsx_runtime36.jsxs)("span", { children: [
2256
+ index > 0 && /* @__PURE__ */ (0, import_jsx_runtime36.jsx)("span", { "aria-hidden": "true", children: "\u203A " }),
2257
+ /* @__PURE__ */ (0, import_jsx_runtime36.jsx)("button", { type: "button", onClick: () => onSelectGenre(genrePlaylist), className: "hover:underline", children: genrePlaylist.name })
2180
2258
  ] }, genrePlaylist.uuid)) }) : song.genre.name
2181
2259
  });
2182
2260
  } else {
2183
- fields.push({ label: "Genre", value: /* @__PURE__ */ (0, import_jsx_runtime35.jsx)("span", { className: "text-[var(--color-text-muted)]", children: "No genre" }) });
2261
+ fields.push({ label: "Genre", value: /* @__PURE__ */ (0, import_jsx_runtime36.jsx)("span", { className: "text-[var(--color-text-muted)]", children: "No genre" }) });
2184
2262
  }
2185
2263
  if (song.firstReleaseYear) fields.push({ label: "First released", value: song.firstReleaseYear });
2186
2264
  const viewCount = song.youtubeVideo?.viewCount;
@@ -2190,11 +2268,11 @@ function SongPanelContent({
2190
2268
  const links = [];
2191
2269
  if (musicBrainzUrl)
2192
2270
  links.push(
2193
- /* @__PURE__ */ (0, import_jsx_runtime35.jsx)("a", { href: musicBrainzUrl, target: "_blank", rel: "noopener noreferrer", className: externalLink, children: "MusicBrainz" }, "mb")
2271
+ /* @__PURE__ */ (0, import_jsx_runtime36.jsx)("a", { href: musicBrainzUrl, target: "_blank", rel: "noopener noreferrer", className: externalLink, children: "MusicBrainz" }, "mb")
2194
2272
  );
2195
2273
  if (song.youtubeVideo)
2196
2274
  links.push(
2197
- /* @__PURE__ */ (0, import_jsx_runtime35.jsx)(
2275
+ /* @__PURE__ */ (0, import_jsx_runtime36.jsx)(
2198
2276
  "a",
2199
2277
  {
2200
2278
  href: `https://www.youtube.com/watch?v=${encodeURIComponent(song.youtubeVideo.id)}`,
@@ -2209,33 +2287,33 @@ function SongPanelContent({
2209
2287
  if (links.length > 0)
2210
2288
  fields.push({
2211
2289
  label: "Links",
2212
- value: /* @__PURE__ */ (0, import_jsx_runtime35.jsx)("span", { className: "flex gap-3", children: links })
2290
+ value: /* @__PURE__ */ (0, import_jsx_runtime36.jsx)("span", { className: "flex gap-3", children: links })
2213
2291
  });
2214
- return /* @__PURE__ */ (0, import_jsx_runtime35.jsxs)(import_jsx_runtime35.Fragment, { children: [
2215
- /* @__PURE__ */ (0, import_jsx_runtime35.jsx)("div", { className: "genre-action-band border-b border-[var(--color-border)]", children: /* @__PURE__ */ (0, import_jsx_runtime35.jsxs)("div", { className: "flex flex-wrap gap-1 px-3.5 py-3", children: [
2216
- unplayableReason ? /* @__PURE__ */ (0, import_jsx_runtime35.jsx)("p", { className: "flex items-center px-2.5 text-sm text-[var(--color-text-muted)]", children: unplayableReason }) : /* @__PURE__ */ (0, import_jsx_runtime35.jsxs)(
2292
+ return /* @__PURE__ */ (0, import_jsx_runtime36.jsxs)(import_jsx_runtime36.Fragment, { children: [
2293
+ /* @__PURE__ */ (0, import_jsx_runtime36.jsx)("div", { className: "genre-action-band border-b border-[var(--color-border)]", children: /* @__PURE__ */ (0, import_jsx_runtime36.jsxs)("div", { className: "flex flex-wrap gap-1 px-3.5 py-3", children: [
2294
+ unplayableReason ? /* @__PURE__ */ (0, import_jsx_runtime36.jsx)("p", { className: "flex items-center px-2.5 text-sm text-[var(--color-text-muted)]", children: unplayableReason }) : /* @__PURE__ */ (0, import_jsx_runtime36.jsxs)(
2217
2295
  "button",
2218
2296
  {
2219
2297
  type: "button",
2220
2298
  onClick: () => isCurrent ? handlePlayPauseAction() : playNewTrackListFromTrackUuid(song, scope),
2221
2299
  className: "group flex flex-col items-center gap-1 w-16 text-xs text-center text-[var(--color-text)] focus-visible:outline-none",
2222
2300
  children: [
2223
- /* @__PURE__ */ (0, import_jsx_runtime35.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_runtime35.jsx)(import_lucide_react10.Pause, { "aria-hidden": "true", className: "w-5 h-5" }) : /* @__PURE__ */ (0, import_jsx_runtime35.jsx)(import_lucide_react10.Play, { "aria-hidden": "true", className: "w-5 h-5" }) }),
2301
+ /* @__PURE__ */ (0, import_jsx_runtime36.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_runtime36.jsx)(import_lucide_react10.Pause, { "aria-hidden": "true", className: "w-5 h-5" }) : /* @__PURE__ */ (0, import_jsx_runtime36.jsx)(import_lucide_react10.Play, { "aria-hidden": "true", className: "w-5 h-5" }) }),
2224
2302
  isPlaying ? "Pause" : "Play"
2225
2303
  ]
2226
2304
  }
2227
2305
  ),
2228
- shareUrl && /* @__PURE__ */ (0, import_jsx_runtime35.jsx)(ShareActionButton, { target: shareTarget, url: shareUrl })
2306
+ shareUrl && /* @__PURE__ */ (0, import_jsx_runtime36.jsx)(ShareActionButton, { target: shareTarget, url: shareUrl })
2229
2307
  ] }) }),
2230
- song.artists && song.artists.length > 0 && /* @__PURE__ */ (0, import_jsx_runtime35.jsx)("p", { className: "px-6 pt-2.5 text-[var(--color-text-muted)]", children: /* @__PURE__ */ (0, import_jsx_runtime35.jsx)(import_genre_tree19.ArtistNames, { artists: song.artists, renderArtistLink }) }),
2231
- /* @__PURE__ */ (0, import_jsx_runtime35.jsx)(import_ui13.FieldList, { items: fields }),
2232
- song.essentialGenres && song.essentialGenres.length > 0 && /* @__PURE__ */ (0, import_jsx_runtime35.jsx)(import_ui13.PanelSection, { title: "Essential in", children: /* @__PURE__ */ (0, import_jsx_runtime35.jsx)("ul", { children: song.essentialGenres.map((genre) => /* @__PURE__ */ (0, import_jsx_runtime35.jsx)("li", { className: "py-0.5", children: genre.name }, genre.uuid)) }) }),
2233
- /* @__PURE__ */ (0, import_jsx_runtime35.jsx)(SongHistory, { uuid: song.uuid, getBackendBaseUrl })
2308
+ song.artists && song.artists.length > 0 && /* @__PURE__ */ (0, import_jsx_runtime36.jsx)("p", { className: "px-6 pt-2.5 text-[var(--color-text-muted)]", children: /* @__PURE__ */ (0, import_jsx_runtime36.jsx)(import_genre_tree25.ArtistNames, { artists: song.artists, renderArtistLink }) }),
2309
+ /* @__PURE__ */ (0, import_jsx_runtime36.jsx)(import_ui13.FieldList, { items: fields }),
2310
+ song.essentialGenres && song.essentialGenres.length > 0 && /* @__PURE__ */ (0, import_jsx_runtime36.jsx)(import_ui13.PanelSection, { title: "Essential in", children: /* @__PURE__ */ (0, import_jsx_runtime36.jsx)("ul", { children: song.essentialGenres.map((genre) => /* @__PURE__ */ (0, import_jsx_runtime36.jsx)("li", { className: "py-0.5", children: genre.name }, genre.uuid)) }) }),
2311
+ /* @__PURE__ */ (0, import_jsx_runtime36.jsx)(SongHistory, { uuid: song.uuid, getBackendBaseUrl })
2234
2312
  ] });
2235
2313
  }
2236
2314
 
2237
2315
  // src/genre-tree/song/useSongPanel.tsx
2238
- var import_jsx_runtime36 = require("react/jsx-runtime");
2316
+ var import_jsx_runtime37 = require("react/jsx-runtime");
2239
2317
  function useSongPanel({
2240
2318
  uuid,
2241
2319
  scope,
@@ -2248,7 +2326,7 @@ function useSongPanel({
2248
2326
  onExpandedChange,
2249
2327
  onClose
2250
2328
  }) {
2251
- const result = (0, import_genre_tree20.useSong)(uuid, getBackendBaseUrl);
2329
+ const result = (0, import_genre_tree26.useSong)(uuid, getBackendBaseUrl);
2252
2330
  const panel = { expanded, onExpandedChange, onClose };
2253
2331
  switch (result.status) {
2254
2332
  case "idle":
@@ -2257,24 +2335,24 @@ function useSongPanel({
2257
2335
  return {
2258
2336
  ...panel,
2259
2337
  title: "Song",
2260
- content: /* @__PURE__ */ (0, import_jsx_runtime36.jsxs)("div", { "aria-busy": "true", className: "flex flex-col gap-2.5 px-6 py-2.5", children: [
2261
- /* @__PURE__ */ (0, import_jsx_runtime36.jsx)(import_ui14.Skeleton, { className: "h-3 w-16" }),
2262
- /* @__PURE__ */ (0, import_jsx_runtime36.jsx)(import_ui14.Skeleton, { className: "h-4 w-32" }),
2263
- /* @__PURE__ */ (0, import_jsx_runtime36.jsx)(import_ui14.Skeleton, { className: "h-3 w-16" }),
2264
- /* @__PURE__ */ (0, import_jsx_runtime36.jsx)(import_ui14.Skeleton, { className: "h-4 w-24" })
2338
+ content: /* @__PURE__ */ (0, import_jsx_runtime37.jsxs)("div", { "aria-busy": "true", className: "flex flex-col gap-2.5 px-6 py-2.5", children: [
2339
+ /* @__PURE__ */ (0, import_jsx_runtime37.jsx)(import_ui14.Skeleton, { className: "h-3 w-16" }),
2340
+ /* @__PURE__ */ (0, import_jsx_runtime37.jsx)(import_ui14.Skeleton, { className: "h-4 w-32" }),
2341
+ /* @__PURE__ */ (0, import_jsx_runtime37.jsx)(import_ui14.Skeleton, { className: "h-3 w-16" }),
2342
+ /* @__PURE__ */ (0, import_jsx_runtime37.jsx)(import_ui14.Skeleton, { className: "h-4 w-24" })
2265
2343
  ] })
2266
2344
  };
2267
2345
  case "not-found":
2268
2346
  return {
2269
2347
  ...panel,
2270
2348
  title: "Song not found",
2271
- content: /* @__PURE__ */ (0, import_jsx_runtime36.jsx)("p", { className: "px-6 py-2.5 text-[var(--color-text-muted)]", children: "This song does not exist." })
2349
+ content: /* @__PURE__ */ (0, import_jsx_runtime37.jsx)("p", { className: "px-6 py-2.5 text-[var(--color-text-muted)]", children: "This song does not exist." })
2272
2350
  };
2273
2351
  case "error":
2274
2352
  return {
2275
2353
  ...panel,
2276
2354
  title: "Song",
2277
- content: /* @__PURE__ */ (0, import_jsx_runtime36.jsx)("p", { className: "px-6 py-2.5 text-red-600", children: "Could not load this song." })
2355
+ content: /* @__PURE__ */ (0, import_jsx_runtime37.jsx)("p", { className: "px-6 py-2.5 text-red-600", children: "Could not load this song." })
2278
2356
  };
2279
2357
  case "success": {
2280
2358
  const { song } = result;
@@ -2282,7 +2360,7 @@ function useSongPanel({
2282
2360
  return {
2283
2361
  ...panel,
2284
2362
  title: song.title,
2285
- content: /* @__PURE__ */ (0, import_jsx_runtime36.jsx)(
2363
+ content: /* @__PURE__ */ (0, import_jsx_runtime37.jsx)(
2286
2364
  SongPanelContent,
2287
2365
  {
2288
2366
  song,
@@ -2302,12 +2380,12 @@ function useSongPanel({
2302
2380
  // src/genre-tree/SizeMetricToggle.tsx
2303
2381
  var import_md = require("react-icons/md");
2304
2382
  var import_ui15 = require("@behindthemusictree/ui");
2305
- var import_jsx_runtime37 = require("react/jsx-runtime");
2383
+ var import_jsx_runtime38 = require("react/jsx-runtime");
2306
2384
  function SizeMetricToggle({
2307
2385
  value,
2308
2386
  onChange
2309
2387
  }) {
2310
- return /* @__PURE__ */ (0, import_jsx_runtime37.jsx)(
2388
+ return /* @__PURE__ */ (0, import_jsx_runtime38.jsx)(
2311
2389
  import_ui15.SegmentedControl,
2312
2390
  {
2313
2391
  variant: "floating",
@@ -2315,15 +2393,15 @@ function SizeMetricToggle({
2315
2393
  value,
2316
2394
  onChange,
2317
2395
  options: [
2318
- { value: "songs", label: /* @__PURE__ */ (0, import_jsx_runtime37.jsx)(import_md.MdMusicNote, { size: 18, "aria-hidden": true }), "aria-label": "Size by songs", title: "Node size by song count" },
2319
- { value: "views", label: /* @__PURE__ */ (0, import_jsx_runtime37.jsx)(import_md.MdVisibility, { size: 18, "aria-hidden": true }), "aria-label": "Size by views", title: "Node size by song views" }
2396
+ { value: "songs", label: /* @__PURE__ */ (0, import_jsx_runtime38.jsx)(import_md.MdMusicNote, { size: 18, "aria-hidden": true }), "aria-label": "Size by songs", title: "Node size by song count" },
2397
+ { value: "views", label: /* @__PURE__ */ (0, import_jsx_runtime38.jsx)(import_md.MdVisibility, { size: 18, "aria-hidden": true }), "aria-label": "Size by views", title: "Node size by song views" }
2320
2398
  ]
2321
2399
  }
2322
2400
  );
2323
2401
  }
2324
2402
 
2325
2403
  // src/genre-tree/GenreTreeView.tsx
2326
- var import_jsx_runtime38 = require("react/jsx-runtime");
2404
+ var import_jsx_runtime39 = require("react/jsx-runtime");
2327
2405
  var NO_POP_ROOT_TITLE = "This genre tree has no 'Mainstream Pop' root yet";
2328
2406
  var REGIONS_CLASS = "[--gtv-center-left:var(--layout-center-left)] [--gtv-center-right:var(--layout-center-right)] [--gtv-center-top:var(--layout-band-top)]";
2329
2407
  var noop = () => {
@@ -2358,6 +2436,7 @@ function GenreTreeView({
2358
2436
  onArtistExpandedChange,
2359
2437
  getShareUrl,
2360
2438
  extraSearchGroups = [],
2439
+ SearchGroups,
2361
2440
  onSearchQueryChange,
2362
2441
  onSearchFocus,
2363
2442
  artistSearch = true,
@@ -2384,21 +2463,21 @@ function GenreTreeView({
2384
2463
  placement = "default",
2385
2464
  ...focus
2386
2465
  }) {
2387
- const getShareUrlRef = (0, import_react16.useRef)(getShareUrl);
2466
+ const getShareUrlRef = (0, import_react17.useRef)(getShareUrl);
2388
2467
  getShareUrlRef.current = getShareUrl;
2389
- const [internalArtistUuid, setInternalArtistUuid] = (0, import_react16.useState)(null);
2468
+ const [internalArtistUuid, setInternalArtistUuid] = (0, import_react17.useState)(null);
2390
2469
  const isArtistControlled = controlledArtistUuid !== void 0;
2391
2470
  const artistUuid = isArtistControlled ? controlledArtistUuid : internalArtistUuid;
2392
- const [internalSongUuid, setInternalSongUuid] = (0, import_react16.useState)(null);
2471
+ const [internalSongUuid, setInternalSongUuid] = (0, import_react17.useState)(null);
2393
2472
  const isSongControlled = controlledSongUuid !== void 0;
2394
2473
  const songUuid = isSongControlled ? controlledSongUuid : internalSongUuid;
2395
2474
  const changeSong = (uuid) => {
2396
2475
  if (!isSongControlled) setInternalSongUuid(uuid);
2397
2476
  onSongUuidChange?.(uuid);
2398
2477
  };
2399
- const [internalSizeMetric, setInternalSizeMetric] = (0, import_react16.useState)("songs");
2478
+ const [internalSizeMetric, setInternalSizeMetric] = (0, import_react17.useState)("songs");
2400
2479
  const sizeMetric = controlledSizeMetric ?? internalSizeMetric;
2401
- const deferredSizeMetric = (0, import_react16.useDeferredValue)(sizeMetric);
2480
+ const deferredSizeMetric = (0, import_react17.useDeferredValue)(sizeMetric);
2402
2481
  const changeSizeMetric = (metric) => {
2403
2482
  if (controlledSizeMetric === void 0) setInternalSizeMetric(metric);
2404
2483
  onSizeMetricChange?.(metric);
@@ -2425,7 +2504,7 @@ function GenreTreeView({
2425
2504
  allowWheelRotation: internalAllowWheelRotation,
2426
2505
  toggleWheelRotation,
2427
2506
  onNodeHover: handleNodeHover
2428
- } = (0, import_genre_tree21.useGenreTreeViewState)({
2507
+ } = (0, import_genre_tree27.useGenreTreeViewState)({
2429
2508
  scope,
2430
2509
  getBackendBaseUrl,
2431
2510
  viewMode: controlledViewMode,
@@ -2435,7 +2514,7 @@ function GenreTreeView({
2435
2514
  artistUuid
2436
2515
  });
2437
2516
  const restoreRevision = viewportRestore?.revision;
2438
- const [stackedRestoreOwner, setStackedRestoreOwner] = (0, import_react16.useState)({ revision: restoreRevision, rootUuid: selectedGenrePlaylist?.root.uuid });
2517
+ const [stackedRestoreOwner, setStackedRestoreOwner] = (0, import_react17.useState)({ revision: restoreRevision, rootUuid: selectedGenrePlaylist?.root.uuid });
2439
2518
  let restoreRootUuid = stackedRestoreOwner.rootUuid;
2440
2519
  if (stackedRestoreOwner.revision !== restoreRevision) {
2441
2520
  restoreRootUuid = selectedGenrePlaylist?.root.uuid;
@@ -2444,14 +2523,14 @@ function GenreTreeView({
2444
2523
  restoreRootUuid = selectedGenrePlaylist.root.uuid;
2445
2524
  setStackedRestoreOwner({ revision: restoreRevision, rootUuid: restoreRootUuid });
2446
2525
  }
2447
- const isGenreUpdating = (0, import_react_query3.useIsMutating)({ mutationKey: import_genre_tree21.genreUpdateMutationKey }) > 0;
2448
- const { mutate: updateGenre } = (0, import_genre_tree21.useUpdateGenre)(scope, getBackendBaseUrl);
2526
+ const isGenreUpdating = (0, import_react_query3.useIsMutating)({ mutationKey: import_genre_tree27.genreUpdateMutationKey }) > 0;
2527
+ const { mutate: updateGenre } = (0, import_genre_tree27.useUpdateGenre)(scope, getBackendBaseUrl);
2449
2528
  const busy = deferredSizeMetric !== sizeMetric || isTreeFetching || isGenreUpdating;
2450
2529
  const isArtistFiltered = artistUuid !== null;
2451
2530
  const canSizeByViews = !isArtistFiltered && hasViewsCount(visibleGenrePlaylists);
2452
- const artistResult = (0, import_genre_tree21.useArtist)(artistUuid, artistSearchBaseUrl ?? getBackendBaseUrl);
2531
+ const artistResult = (0, import_genre_tree27.useArtist)(artistUuid, artistSearchBaseUrl ?? getBackendBaseUrl);
2453
2532
  const filterArtistName = artistResult.status === "success" ? artistResult.artist.name : null;
2454
- const filterArtist = (0, import_react16.useMemo)(
2533
+ const filterArtist = (0, import_react17.useMemo)(
2455
2534
  () => artistUuid !== null && filterArtistName !== null ? { uuid: artistUuid, name: filterArtistName } : null,
2456
2535
  [artistUuid, filterArtistName]
2457
2536
  );
@@ -2466,14 +2545,14 @@ function GenreTreeView({
2466
2545
  if (selectedNodeId !== null) clearSelection();
2467
2546
  changeSong(uuid);
2468
2547
  };
2469
- const openSongRef = (0, import_react16.useRef)(openSong);
2548
+ const openSongRef = (0, import_react17.useRef)(openSong);
2470
2549
  openSongRef.current = openSong;
2471
- const resolvedRenderSongLink = (0, import_react16.useCallback)(
2472
- (song) => renderSongLink ? renderSongLink(song) : /* @__PURE__ */ (0, import_jsx_runtime38.jsx)("button", { type: "button", onClick: () => openSongRef.current(song.uuid), className: "hover:underline", children: song.title }),
2550
+ const resolvedRenderSongLink = (0, import_react17.useCallback)(
2551
+ (song) => renderSongLink ? renderSongLink(song) : /* @__PURE__ */ (0, import_jsx_runtime39.jsx)("button", { type: "button", onClick: () => openSongRef.current(song.uuid), className: "hover:underline", children: song.title }),
2473
2552
  [renderSongLink]
2474
2553
  );
2475
- const [expandedSongUuid, setExpandedSongUuid] = (0, import_react16.useState)(null);
2476
- const [expandedArtistUuid, setExpandedArtistUuid] = (0, import_react16.useState)(null);
2554
+ const [expandedSongUuid, setExpandedSongUuid] = (0, import_react17.useState)(null);
2555
+ const [expandedArtistUuid, setExpandedArtistUuid] = (0, import_react17.useState)(null);
2477
2556
  const artistBaseUrl = artistSearchBaseUrl ?? getBackendBaseUrl;
2478
2557
  const songBaseUrl = getBackendBaseUrl;
2479
2558
  const artistPanel = useArtistPanel({
@@ -2481,7 +2560,7 @@ function GenreTreeView({
2481
2560
  scope,
2482
2561
  getBackendBaseUrl: artistBaseUrl,
2483
2562
  getShareUrl: getShareUrlRef.current,
2484
- renderArtistLink: renderArtistLink ?? ((artist) => artist.isSpecialPurpose ? artist.name : /* @__PURE__ */ (0, import_jsx_runtime38.jsx)("button", { type: "button", onClick: () => openArtist(artist.uuid), className: "hover:underline", children: artist.name })),
2563
+ renderArtistLink: renderArtistLink ?? ((artist) => artist.isSpecialPurpose ? artist.name : /* @__PURE__ */ (0, import_jsx_runtime39.jsx)("button", { type: "button", onClick: () => openArtist(artist.uuid), className: "hover:underline", children: artist.name })),
2485
2564
  expanded: controlledArtistExpanded ?? (artistUuid !== null && expandedArtistUuid === artistUuid),
2486
2565
  onExpandedChange: (on) => {
2487
2566
  if (controlledArtistExpanded === void 0) setExpandedArtistUuid(on ? artistUuid : null);
@@ -2490,7 +2569,7 @@ function GenreTreeView({
2490
2569
  renderSongLink: resolvedRenderSongLink,
2491
2570
  onClose: () => changeArtist(null)
2492
2571
  });
2493
- const resolvedRenderArtistLink = renderArtistLink ?? ((artist) => artist.isSpecialPurpose ? artist.name : /* @__PURE__ */ (0, import_jsx_runtime38.jsx)("button", { type: "button", onClick: () => openArtist(artist.uuid), className: "hover:underline", children: artist.name }));
2572
+ const resolvedRenderArtistLink = renderArtistLink ?? ((artist) => artist.isSpecialPurpose ? artist.name : /* @__PURE__ */ (0, import_jsx_runtime39.jsx)("button", { type: "button", onClick: () => openArtist(artist.uuid), className: "hover:underline", children: artist.name }));
2494
2573
  const getGenrePath = (genre) => {
2495
2574
  const path = [];
2496
2575
  let current = visibleGenrePlaylists.find((playlist) => playlist.criteria?.uuid === genre.uuid);
@@ -2522,8 +2601,8 @@ function GenreTreeView({
2522
2601
  const builtInSongPanel = overlayPanel || hasSelectedGenreNode ? void 0 : songPanel;
2523
2602
  const builtInArtistPanel = overlayPanel || hasSelectedGenreNode || builtInSongPanel ? void 0 : artistPanel;
2524
2603
  const builtInPanel = builtInSongPanel ?? builtInArtistPanel;
2525
- const [uncontrolledCollapsed, setUncontrolledCollapsed] = (0, import_react16.useState)(false);
2526
- const [uncontrolledExpanded, setUncontrolledExpanded] = (0, import_react16.useState)(false);
2604
+ const [uncontrolledCollapsed, setUncontrolledCollapsed] = (0, import_react17.useState)(false);
2605
+ const [uncontrolledExpanded, setUncontrolledExpanded] = (0, import_react17.useState)(false);
2527
2606
  const isCollapsed = collapsed ?? uncontrolledCollapsed;
2528
2607
  const isExpanded = expanded ?? uncontrolledExpanded;
2529
2608
  const changeCollapsed = (next) => {
@@ -2534,28 +2613,28 @@ function GenreTreeView({
2534
2613
  setUncontrolledExpanded(next);
2535
2614
  onExpandedChange?.(next);
2536
2615
  };
2537
- (0, import_react16.useEffect)(() => {
2616
+ (0, import_react17.useEffect)(() => {
2538
2617
  setUncontrolledCollapsed(false);
2539
2618
  }, [selectedNodeId, artistUuid, songUuid]);
2540
- const [isWheelRevealed, setIsWheelRevealed] = (0, import_react16.useState)(false);
2619
+ const [isWheelRevealed, setIsWheelRevealed] = (0, import_react17.useState)(false);
2541
2620
  const usesHandoff = viewMode === "wheel" || viewMode === "pop-core";
2542
2621
  const isTreeReady = !isLoading && (!usesHandoff || isWheelRevealed);
2543
- const [resolveStyle, setResolveStyle] = (0, import_react16.useState)(null);
2544
- const handleNodeStyleResolverChange = (0, import_react16.useCallback)(
2622
+ const [resolveStyle, setResolveStyle] = (0, import_react17.useState)(null);
2623
+ const handleNodeStyleResolverChange = (0, import_react17.useCallback)(
2545
2624
  (resolver) => setResolveStyle(() => resolver),
2546
2625
  []
2547
2626
  );
2548
- const panelRef = (0, import_react16.useRef)(null);
2627
+ const panelRef = (0, import_react17.useRef)(null);
2549
2628
  const obscuredInset = (0, import_ui16.useObscuredInset)(panelRef);
2550
2629
  const hostPanel = { obscuredInset, onNodeStyleResolverChange: handleNodeStyleResolverChange };
2551
2630
  const nodeMetric = isArtistFiltered || !canSizeByViews ? "songs" : deferredSizeMetric;
2552
- const treeNodes = (0, import_react16.useMemo)(() => toTreeNodes(visibleGenrePlaylists, nodeMetric), [visibleGenrePlaylists, nodeMetric]);
2631
+ const treeNodes = (0, import_react17.useMemo)(() => toTreeNodes(visibleGenrePlaylists, nodeMetric), [visibleGenrePlaylists, nodeMetric]);
2553
2632
  const relations = (0, import_genre_tree_view4.useGenreNodeRelations)(
2554
2633
  treeNodes,
2555
2634
  !overlayPanel && hasSelectedGenreNode ? selectedNodeId : null,
2556
2635
  resolveStyle
2557
2636
  );
2558
- const [selectionHistory, setSelectionHistory] = (0, import_react16.useState)({
2637
+ const [selectionHistory, setSelectionHistory] = (0, import_react17.useState)({
2559
2638
  current: selectedNodeId,
2560
2639
  previous: null
2561
2640
  });
@@ -2568,28 +2647,6 @@ function GenreTreeView({
2568
2647
  const previousPlaylist = selectionHistory.previous !== null && selectionHistory.previous !== selectedNodeId ? genrePlaylistByNodeId.get(selectionHistory.previous) : void 0;
2569
2648
  const handlePrevious = previousPlaylist && visibleGenrePlaylists.includes(previousPlaylist) ? () => handleGenreSearchSelect(previousPlaylist) : void 0;
2570
2649
  const { showPopup, hidePopup } = (0, import_popup5.usePopup)();
2571
- const [searchText, setSearchText] = (0, import_react16.useState)("");
2572
- const artistGroup = useArtistSearchGroup(
2573
- artistBaseUrl,
2574
- searchText,
2575
- (artist) => {
2576
- openArtist(artist.uuid);
2577
- onArtistSelect?.(artist);
2578
- },
2579
- Boolean(artistSearch)
2580
- );
2581
- const songGroup = useSongSearchGroup(
2582
- songBaseUrl,
2583
- searchText,
2584
- (song) => {
2585
- openSong(song.uuid);
2586
- onSongSelect?.(song);
2587
- },
2588
- Boolean(songSearch)
2589
- );
2590
- const extraGroups = [...extraSearchGroups, artistGroup, songGroup].filter(
2591
- (group) => group !== void 0
2592
- );
2593
2650
  const handleSelectedNodeChange = (node) => {
2594
2651
  if (node !== null && songUuid !== null) changeSong(null);
2595
2652
  selectNode(node);
@@ -2605,7 +2662,7 @@ function GenreTreeView({
2605
2662
  const handleGenreSearchZoomIn = () => {
2606
2663
  if (selectedUuid) focus.onFocusGenreChange?.(selectedUuid, "zoom-in");
2607
2664
  };
2608
- const zoomInRef = (0, import_react16.useRef)(void 0);
2665
+ const zoomInRef = (0, import_react17.useRef)(void 0);
2609
2666
  zoomInRef.current = canZoomIn ? handleGenreSearchZoomIn : void 0;
2610
2667
  const selectGenreFromPanel = (nodeId) => {
2611
2668
  const genrePlaylist = genrePlaylistByNodeId.get(nodeId);
@@ -2613,7 +2670,7 @@ function GenreTreeView({
2613
2670
  };
2614
2671
  const renderGenreBody = (relation) => {
2615
2672
  const { node } = relation;
2616
- const family = /* @__PURE__ */ (0, import_jsx_runtime38.jsx)(
2673
+ const family = /* @__PURE__ */ (0, import_jsx_runtime39.jsx)(
2617
2674
  import_genre_tree_view4.GenreNodeFamily,
2618
2675
  {
2619
2676
  node,
@@ -2624,10 +2681,10 @@ function GenreTreeView({
2624
2681
  onSelectNode: selectGenreFromPanel
2625
2682
  }
2626
2683
  );
2627
- const ancestors = /* @__PURE__ */ (0, import_jsx_runtime38.jsx)(import_genre_tree_view4.GenreNodeAncestors, { ancestorNodes: relation.ancestorNodes, onSelectNode: selectGenreFromPanel });
2684
+ const ancestors = /* @__PURE__ */ (0, import_jsx_runtime39.jsx)(import_genre_tree_view4.GenreNodeAncestors, { ancestorNodes: relation.ancestorNodes, onSelectNode: selectGenreFromPanel });
2628
2685
  const genrePlaylist = genrePlaylistByNodeId.get(node.id);
2629
2686
  const criteria = genrePlaylist?.criteria;
2630
- if (!genrePlaylist || !criteria) return { primary: /* @__PURE__ */ (0, import_jsx_runtime38.jsxs)(import_jsx_runtime38.Fragment, { children: [
2687
+ if (!genrePlaylist || !criteria) return { primary: /* @__PURE__ */ (0, import_jsx_runtime39.jsxs)(import_jsx_runtime39.Fragment, { children: [
2631
2688
  family,
2632
2689
  ancestors
2633
2690
  ] }) };
@@ -2647,16 +2704,17 @@ function GenreTreeView({
2647
2704
  (sibling) => sibling.parent?.uuid === parentPlaylist.uuid && sibling.uuid !== genrePlaylist.uuid && sibling.criteria?.side === "pop"
2648
2705
  ) : void 0;
2649
2706
  return {
2650
- primary: /* @__PURE__ */ (0, import_jsx_runtime38.jsxs)(import_jsx_runtime38.Fragment, { children: [
2651
- /* @__PURE__ */ (0, import_jsx_runtime38.jsx)(
2707
+ primary: /* @__PURE__ */ (0, import_jsx_runtime39.jsxs)(import_jsx_runtime39.Fragment, { children: [
2708
+ /* @__PURE__ */ (0, import_jsx_runtime39.jsx)(
2652
2709
  GenreInfoPanelBody,
2653
2710
  {
2654
- family: /* @__PURE__ */ (0, import_jsx_runtime38.jsxs)(import_jsx_runtime38.Fragment, { children: [
2711
+ family: /* @__PURE__ */ (0, import_jsx_runtime39.jsxs)(import_jsx_runtime39.Fragment, { children: [
2655
2712
  family,
2656
2713
  ancestors
2657
2714
  ] }),
2658
2715
  genreUuid,
2659
2716
  genrePlaylist,
2717
+ catalogue: genrePlaylist.catalogue,
2660
2718
  wikidataId: criteria.wikidataId,
2661
2719
  scope,
2662
2720
  getBackendBaseUrl,
@@ -2669,7 +2727,7 @@ function GenreTreeView({
2669
2727
  onAddChild: readOnly ? void 0 : () => handleGenreCreationAction(criteria),
2670
2728
  onRename: readOnly ? void 0 : () => handleGenreRenameAction(criteria),
2671
2729
  onChangeParent: readOnly ? void 0 : () => showPopup(
2672
- /* @__PURE__ */ (0, import_jsx_runtime38.jsx)(
2730
+ /* @__PURE__ */ (0, import_jsx_runtime39.jsx)(
2673
2731
  ChangeParentPopup,
2674
2732
  {
2675
2733
  genrePlaylist: { ...genrePlaylist, criteria },
@@ -2744,19 +2802,19 @@ function GenreTreeView({
2744
2802
  edge: activePanel.expanded ? { label: "Shrink panel", onClick: () => activePanel.onExpandedChange?.(false) } : isCollapsed ? null : { label: "Hide panel", onClick: () => changeCollapsed(true) },
2745
2803
  center: activePanel.expanded ? null : isCollapsed ? { label: "Show panel", onClick: () => changeCollapsed(false) } : activePanel.onExpandedChange ? { label: "Extend panel", onClick: () => activePanel.onExpandedChange?.(true) } : null
2746
2804
  } : void 0;
2747
- const expandButton = placement === "default" && activePanel?.onExpandedChange ? /* @__PURE__ */ (0, import_jsx_runtime38.jsx)(
2805
+ const expandButton = placement === "default" && activePanel?.onExpandedChange ? /* @__PURE__ */ (0, import_jsx_runtime39.jsx)(
2748
2806
  import_ui16.IconButton,
2749
2807
  {
2750
2808
  "aria-label": activePanel.expanded ? "Shrink panel" : "Extend panel",
2751
2809
  style: activePanel.headerAccent && { color: "inherit" },
2752
2810
  onClick: () => activePanel.onExpandedChange?.(!activePanel.expanded),
2753
- children: activePanel.expanded ? /* @__PURE__ */ (0, import_jsx_runtime38.jsx)(import_lucide_react12.Minimize2, { "aria-hidden": "true", className: "h-4 w-4" }) : /* @__PURE__ */ (0, import_jsx_runtime38.jsx)(import_lucide_react12.Maximize2, { "aria-hidden": "true", className: "h-4 w-4" })
2811
+ children: activePanel.expanded ? /* @__PURE__ */ (0, import_jsx_runtime39.jsx)(import_lucide_react12.Minimize2, { "aria-hidden": "true", className: "h-4 w-4" }) : /* @__PURE__ */ (0, import_jsx_runtime39.jsx)(import_lucide_react12.Maximize2, { "aria-hidden": "true", className: "h-4 w-4" })
2754
2812
  }
2755
2813
  ) : null;
2756
2814
  const renderArtistFilter = () => {
2757
2815
  if (isLoading || !filterArtist) return null;
2758
- return /* @__PURE__ */ (0, import_jsx_runtime38.jsxs)("span", { className: "inline-flex items-center overflow-hidden rounded-full bg-[var(--color-neutral-100)] text-sm", children: [
2759
- /* @__PURE__ */ (0, import_jsx_runtime38.jsx)(
2816
+ return /* @__PURE__ */ (0, import_jsx_runtime39.jsxs)("span", { className: "inline-flex items-center overflow-hidden rounded-full bg-[var(--color-neutral-100)] text-sm", children: [
2817
+ /* @__PURE__ */ (0, import_jsx_runtime39.jsx)(
2760
2818
  "button",
2761
2819
  {
2762
2820
  type: "button",
@@ -2768,7 +2826,7 @@ function GenreTreeView({
2768
2826
  children: filterArtist.name
2769
2827
  }
2770
2828
  ),
2771
- /* @__PURE__ */ (0, import_jsx_runtime38.jsx)(
2829
+ /* @__PURE__ */ (0, import_jsx_runtime39.jsx)(
2772
2830
  "button",
2773
2831
  {
2774
2832
  type: "button",
@@ -2780,26 +2838,36 @@ function GenreTreeView({
2780
2838
  )
2781
2839
  ] });
2782
2840
  };
2783
- const actions = /* @__PURE__ */ (0, import_jsx_runtime38.jsxs)(import_jsx_runtime38.Fragment, { children: [
2784
- !isLoading && /* @__PURE__ */ (0, import_jsx_runtime38.jsx)(
2785
- GenreSearch,
2841
+ const actions = /* @__PURE__ */ (0, import_jsx_runtime39.jsxs)(import_jsx_runtime39.Fragment, { children: [
2842
+ !isLoading && /* @__PURE__ */ (0, import_jsx_runtime39.jsx)(
2843
+ GenreTreeSearch,
2786
2844
  {
2845
+ artistBaseUrl,
2846
+ songBaseUrl,
2847
+ artistSearch: Boolean(artistSearch),
2848
+ songSearch: Boolean(songSearch),
2849
+ onArtistSelect: (artist) => {
2850
+ openArtist(artist.uuid);
2851
+ onArtistSelect?.(artist);
2852
+ },
2853
+ onSongSelect: (song) => {
2854
+ openSong(song.uuid);
2855
+ onSongSelect?.(song);
2856
+ },
2857
+ SearchGroups,
2787
2858
  genrePlaylists: visibleGenrePlaylists,
2788
2859
  onSelect: handleGenreSearchSelect,
2789
2860
  selectedName: selectedGenrePlaylist?.name ?? (typeof builtInPanel?.title === "string" ? builtInPanel.title : null),
2790
2861
  onClear: builtInSongPanel ? () => changeSong(null) : builtInArtistPanel ? () => changeArtist(null) : handleGenreSearchClear,
2791
2862
  onPrevious: handlePrevious,
2792
- onQueryChange: (text) => {
2793
- setSearchText(text);
2794
- onSearchQueryChange?.(text);
2795
- },
2863
+ onQueryChange: onSearchQueryChange,
2796
2864
  onFocus: onSearchFocus,
2797
- extraGroups
2865
+ extraGroups: extraSearchGroups
2798
2866
  }
2799
2867
  ),
2800
- !isLoading && filterArtist && /* @__PURE__ */ (0, import_jsx_runtime38.jsx)("div", { className: "hidden w-full justify-center max-md:flex", children: renderArtistFilter() }),
2801
- /* @__PURE__ */ (0, import_jsx_runtime38.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: [
2802
- !isLoading && !isControlled && /* @__PURE__ */ (0, import_jsx_runtime38.jsx)(
2868
+ !isLoading && filterArtist && /* @__PURE__ */ (0, import_jsx_runtime39.jsx)("div", { className: "hidden w-full justify-center max-md:flex", children: renderArtistFilter() }),
2869
+ /* @__PURE__ */ (0, import_jsx_runtime39.jsxs)("div", { className: "contents max-md:flex max-md:w-full max-md:items-center max-md:gap-2 max-md:overflow-x-auto max-md:[scrollbar-width:none] max-md:[&>*]:shrink-0", children: [
2870
+ !isLoading && !isControlled && /* @__PURE__ */ (0, import_jsx_runtime39.jsx)(
2803
2871
  import_ui16.SegmentedControl,
2804
2872
  {
2805
2873
  "aria-label": "Tree view mode",
@@ -2813,13 +2881,13 @@ function GenreTreeView({
2813
2881
  ]
2814
2882
  }
2815
2883
  ),
2816
- !isLoading && showDisplayOptions && /* @__PURE__ */ (0, import_jsx_runtime38.jsx)(
2884
+ !isLoading && showDisplayOptions && /* @__PURE__ */ (0, import_jsx_runtime39.jsx)(
2817
2885
  "div",
2818
2886
  {
2819
2887
  className: "flex items-center gap-1",
2820
2888
  role: "group",
2821
2889
  "aria-label": "Tree display options",
2822
- children: !isRotationControlled && viewMode !== "stacked" && viewMode !== "outline" && /* @__PURE__ */ (0, import_jsx_runtime38.jsx)(
2890
+ children: !isRotationControlled && viewMode !== "stacked" && viewMode !== "outline" && /* @__PURE__ */ (0, import_jsx_runtime39.jsx)(
2823
2891
  import_ui16.Button,
2824
2892
  {
2825
2893
  variant: allowWheelRotation ? "default" : "outline",
@@ -2830,7 +2898,7 @@ function GenreTreeView({
2830
2898
  )
2831
2899
  }
2832
2900
  ),
2833
- !isLoading && !readOnly && showAddRoot && /* @__PURE__ */ (0, import_jsx_runtime38.jsx)(
2901
+ !isLoading && !readOnly && showAddRoot && /* @__PURE__ */ (0, import_jsx_runtime39.jsx)(
2834
2902
  import_ui16.IconTextButton,
2835
2903
  {
2836
2904
  icon: import_lucide_react11.Plus,
@@ -2840,34 +2908,34 @@ function GenreTreeView({
2840
2908
  )
2841
2909
  ] })
2842
2910
  ] });
2843
- return /* @__PURE__ */ (0, import_jsx_runtime38.jsxs)(
2911
+ return /* @__PURE__ */ (0, import_jsx_runtime39.jsxs)(
2844
2912
  "div",
2845
2913
  {
2846
2914
  "data-left-panel": isPanelShown && !isCollapsed ? void 0 : "hidden",
2847
2915
  className: ["genre-tree-view relative flex flex-col h-full", placement === "regions" && REGIONS_CLASS, className2].filter(Boolean).join(" "),
2848
2916
  children: [
2849
- /* @__PURE__ */ (0, import_jsx_runtime38.jsx)(
2917
+ /* @__PURE__ */ (0, import_jsx_runtime39.jsx)(
2850
2918
  "div",
2851
2919
  {
2852
2920
  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"}`,
2853
2921
  children: actions
2854
2922
  }
2855
2923
  ),
2856
- !isLoading && filterArtist && /* @__PURE__ */ (0, import_jsx_runtime38.jsx)(
2924
+ !isLoading && filterArtist && /* @__PURE__ */ (0, import_jsx_runtime39.jsx)(
2857
2925
  "div",
2858
2926
  {
2859
2927
  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"}`,
2860
2928
  children: renderArtistFilter()
2861
2929
  }
2862
2930
  ),
2863
- /* @__PURE__ */ (0, import_jsx_runtime38.jsx)("div", { className: "content-container flex-1 min-h-0 flex flex-row gap-4", children: /* @__PURE__ */ (0, import_jsx_runtime38.jsxs)("div", { className: "tree-view-container relative flex-1 min-w-0 flex flex-col h-full", children: [
2864
- !isLoading && viewMode === "outline" && floatingControlsLeading && /* @__PURE__ */ (0, import_jsx_runtime38.jsx)("div", { className: "absolute bottom-3 right-3 z-10", children: floatingControlsLeading }),
2865
- isLoading ? /* @__PURE__ */ (0, import_jsx_runtime38.jsx)(import_genre_tree_view4.GenreTreeViewSkeleton, { viewMode }) : viewMode === "wheel" ? (
2931
+ /* @__PURE__ */ (0, import_jsx_runtime39.jsx)("div", { className: "content-container flex-1 min-h-0 flex flex-row gap-4", children: /* @__PURE__ */ (0, import_jsx_runtime39.jsxs)("div", { className: "tree-view-container relative flex-1 min-w-0 flex flex-col h-full", children: [
2932
+ !isLoading && viewMode === "outline" && floatingControlsLeading && /* @__PURE__ */ (0, import_jsx_runtime39.jsx)("div", { className: "absolute bottom-3 right-3 z-10", children: floatingControlsLeading }),
2933
+ isLoading ? /* @__PURE__ */ (0, import_jsx_runtime39.jsx)(import_genre_tree_view4.GenreTreeViewSkeleton, { viewMode }) : viewMode === "wheel" ? (
2866
2934
  // The info panel's top edge aligns with the tree's; the search bar floats `--layout-panel-inset` below it,
2867
2935
  // and the header inset (the bar's height plus that inset) extends the header's fill behind the bar,
2868
2936
  // Google Maps style; the 8px bottom padding mirrors the inset above the bar, and the hidden title's height is zeroed.
2869
2937
  // The panel's own Zoom in button is hidden: the search bar carries it.
2870
- /* @__PURE__ */ (0, import_jsx_runtime38.jsx)("div", { className: "tree-container search-overlay relative flex-1 w-full min-h-0 ", children: /* @__PURE__ */ (0, import_jsx_runtime38.jsx)(GenreTreeWheelHandoff, { skeleton: /* @__PURE__ */ (0, import_jsx_runtime38.jsx)(import_genre_tree_view4.GenreTreeWheelSkeleton, {}), onRevealedChange: setIsWheelRevealed, children: /* @__PURE__ */ (0, import_jsx_runtime38.jsx)(
2938
+ /* @__PURE__ */ (0, import_jsx_runtime39.jsx)("div", { className: "tree-container search-overlay relative flex-1 w-full min-h-0 ", children: /* @__PURE__ */ (0, import_jsx_runtime39.jsx)(GenreTreeWheelHandoff, { skeleton: /* @__PURE__ */ (0, import_jsx_runtime39.jsx)(import_genre_tree_view4.GenreTreeWheelSkeleton, {}), onRevealedChange: setIsWheelRevealed, children: /* @__PURE__ */ (0, import_jsx_runtime39.jsx)(
2871
2939
  GenrePlaylistTreeWheel,
2872
2940
  {
2873
2941
  scope,
@@ -2884,9 +2952,9 @@ function GenreTreeView({
2884
2952
  allowWheelRotation,
2885
2953
  sizeMetric: isArtistFiltered || !canSizeByViews ? "songs" : deferredSizeMetric,
2886
2954
  busy,
2887
- floatingControlsLeading: floatingControlsLeading || canSizeByViews ? /* @__PURE__ */ (0, import_jsx_runtime38.jsxs)("div", { className: "flex items-center gap-2", children: [
2955
+ floatingControlsLeading: floatingControlsLeading || canSizeByViews ? /* @__PURE__ */ (0, import_jsx_runtime39.jsxs)("div", { className: "flex items-center gap-2", children: [
2888
2956
  floatingControlsLeading,
2889
- canSizeByViews && /* @__PURE__ */ (0, import_jsx_runtime38.jsx)(SizeMetricToggle, { value: sizeMetric, onChange: changeSizeMetric })
2957
+ canSizeByViews && /* @__PURE__ */ (0, import_jsx_runtime39.jsx)(SizeMetricToggle, { value: sizeMetric, onChange: changeSizeMetric })
2890
2958
  ] }) : void 0,
2891
2959
  defaultViewport,
2892
2960
  viewportRestore,
@@ -2895,7 +2963,7 @@ function GenreTreeView({
2895
2963
  ) }) })
2896
2964
  ) : viewMode === "pop-core" ? (
2897
2965
  // Same as the wheel: the header inset extends its fill behind the floating search bar, Google Maps style.
2898
- /* @__PURE__ */ (0, import_jsx_runtime38.jsx)("div", { className: "tree-container search-overlay relative flex-1 w-full min-h-0 ", children: /* @__PURE__ */ (0, import_jsx_runtime38.jsx)(GenreTreeWheelHandoff, { skeleton: /* @__PURE__ */ (0, import_jsx_runtime38.jsx)(import_genre_tree_view4.GenreTreeWheelSkeleton, {}), onRevealedChange: setIsWheelRevealed, children: /* @__PURE__ */ (0, import_jsx_runtime38.jsx)(
2966
+ /* @__PURE__ */ (0, import_jsx_runtime39.jsx)("div", { className: "tree-container search-overlay relative flex-1 w-full min-h-0 ", children: /* @__PURE__ */ (0, import_jsx_runtime39.jsx)(GenreTreeWheelHandoff, { skeleton: /* @__PURE__ */ (0, import_jsx_runtime39.jsx)(import_genre_tree_view4.GenreTreeWheelSkeleton, {}), onRevealedChange: setIsWheelRevealed, children: /* @__PURE__ */ (0, import_jsx_runtime39.jsx)(
2899
2967
  GenrePlaylistTreeWheelRadialPopCore,
2900
2968
  {
2901
2969
  scope,
@@ -2912,9 +2980,9 @@ function GenreTreeView({
2912
2980
  allowWheelRotation,
2913
2981
  sizeMetric: isArtistFiltered || !canSizeByViews ? "songs" : deferredSizeMetric,
2914
2982
  busy,
2915
- floatingControlsLeading: floatingControlsLeading || canSizeByViews ? /* @__PURE__ */ (0, import_jsx_runtime38.jsxs)("div", { className: "flex items-center gap-2", children: [
2983
+ floatingControlsLeading: floatingControlsLeading || canSizeByViews ? /* @__PURE__ */ (0, import_jsx_runtime39.jsxs)("div", { className: "flex items-center gap-2", children: [
2916
2984
  floatingControlsLeading,
2917
- canSizeByViews && /* @__PURE__ */ (0, import_jsx_runtime38.jsx)(SizeMetricToggle, { value: sizeMetric, onChange: changeSizeMetric })
2985
+ canSizeByViews && /* @__PURE__ */ (0, import_jsx_runtime39.jsx)(SizeMetricToggle, { value: sizeMetric, onChange: changeSizeMetric })
2918
2986
  ] }) : void 0,
2919
2987
  defaultViewport,
2920
2988
  viewportRestore,
@@ -2922,7 +2990,7 @@ function GenreTreeView({
2922
2990
  ...focus
2923
2991
  }
2924
2992
  ) }) })
2925
- ) : viewMode === "outline" ? /* @__PURE__ */ (0, import_jsx_runtime38.jsx)("div", { className: "tree-container flex-1 min-h-0 w-full overflow-y-auto relative", children: /* @__PURE__ */ (0, import_jsx_runtime38.jsx)(
2993
+ ) : viewMode === "outline" ? /* @__PURE__ */ (0, import_jsx_runtime39.jsx)("div", { className: "tree-container flex-1 min-h-0 w-full overflow-y-auto relative", children: /* @__PURE__ */ (0, import_jsx_runtime39.jsx)(
2926
2994
  GenrePlaylistTreeWheelRadialPopCore,
2927
2995
  {
2928
2996
  outline: true,
@@ -2938,13 +3006,13 @@ function GenreTreeView({
2938
3006
  selectedNodeId,
2939
3007
  readOnly
2940
3008
  }
2941
- ) }) : /* @__PURE__ */ (0, import_jsx_runtime38.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(
3009
+ ) }) : /* @__PURE__ */ (0, import_jsx_runtime39.jsx)("div", { className: "tree-container relative flex flex-col gap-4 w-full pt-14 overflow-x-auto overflow-y-auto text-[var(--color-text)]", children: Object.entries(groupedGenrePlaylistsByRoot).map(
2942
3010
  ([uuid, genrePlaylistTreePerRoot]) => {
2943
- return /* @__PURE__ */ (0, import_jsx_runtime38.jsx)(
3011
+ return /* @__PURE__ */ (0, import_jsx_runtime39.jsx)(
2944
3012
  "div",
2945
3013
  {
2946
3014
  className: "tree-per-root-container relative shrink-0 h-[28rem] mt-2 mr-16 p-2 bg-[var(--color-neutral-100)] rounded-lg",
2947
- children: /* @__PURE__ */ (0, import_jsx_runtime38.jsx)("div", { className: "graph-container relative z-10 h-full", children: /* @__PURE__ */ (0, import_jsx_runtime38.jsx)(
3015
+ children: /* @__PURE__ */ (0, import_jsx_runtime39.jsx)("div", { className: "graph-container relative z-10 h-full", children: /* @__PURE__ */ (0, import_jsx_runtime39.jsx)(
2948
3016
  GenrePlaylistTreePerRoot,
2949
3017
  {
2950
3018
  scope,
@@ -2970,7 +3038,7 @@ function GenreTreeView({
2970
3038
  );
2971
3039
  }
2972
3040
  ) }),
2973
- /* @__PURE__ */ (0, import_jsx_runtime38.jsx)(
3041
+ /* @__PURE__ */ (0, import_jsx_runtime39.jsx)(
2974
3042
  import_ui16.DetailPanel,
2975
3043
  {
2976
3044
  ref: panelRef,
@@ -2987,7 +3055,7 @@ function GenreTreeView({
2987
3055
  aside: activePanel?.aside,
2988
3056
  edgeTab: detailTab,
2989
3057
  headerClassName: activePanel?.compact ? "pt-[calc(var(--layout-search-h)+var(--layout-panel-inset)+0.5rem)]" : void 0,
2990
- children: activePanel?.aside && !activePanel.expanded ? /* @__PURE__ */ (0, import_jsx_runtime38.jsxs)(import_jsx_runtime38.Fragment, { children: [
3058
+ children: activePanel?.aside && !activePanel.expanded ? /* @__PURE__ */ (0, import_jsx_runtime39.jsxs)(import_jsx_runtime39.Fragment, { children: [
2991
3059
  activePanel.content,
2992
3060
  activePanel.aside
2993
3061
  ] }) : activePanel?.content
@@ -3000,12 +3068,12 @@ function GenreTreeView({
3000
3068
  }
3001
3069
 
3002
3070
  // src/genre-tree/GenreDetailPanel.tsx
3003
- var import_react17 = require("react");
3071
+ var import_react18 = require("react");
3004
3072
  var import_genre_tree_view5 = require("@behindthemusictree/genre-tree-view");
3005
3073
  var import_ui17 = require("@behindthemusictree/ui");
3006
3074
  var import_lucide_react13 = require("lucide-react");
3007
- var import_genre_tree22 = require("@behindthemusictree/app-kit/genre-tree");
3008
- var import_jsx_runtime39 = require("react/jsx-runtime");
3075
+ var import_genre_tree28 = require("@behindthemusictree/app-kit/genre-tree");
3076
+ var import_jsx_runtime40 = require("react/jsx-runtime");
3009
3077
  var RELATIVE_ITEM_COUNT = 1;
3010
3078
  function toNode(genre, parentId) {
3011
3079
  return {
@@ -3032,9 +3100,9 @@ function GenreDetailPanel({
3032
3100
  edgeTab,
3033
3101
  extras
3034
3102
  }) {
3035
- const { data: detail, isPending, isError } = (0, import_genre_tree22.useFetchGenreDetail)(genreUuid, scope, getBackendBaseUrl);
3103
+ const { data: detail, isPending, isError } = (0, import_genre_tree28.useFetchGenreDetail)(genreUuid, scope, getBackendBaseUrl);
3036
3104
  const playback = useGenrePlaylistPlayback({ scope, genrePlaylists: [] });
3037
- const view = (0, import_react17.useMemo)(() => {
3105
+ const view = (0, import_react18.useMemo)(() => {
3038
3106
  if (!detail) return null;
3039
3107
  const rootColor = (0, import_genre_tree_view5.getGenreTreeColor)(detail.root.wikidataId);
3040
3108
  const relative = (genre, parentId) => ({
@@ -3048,7 +3116,7 @@ function GenreDetailPanel({
3048
3116
  id: detail.uuid,
3049
3117
  parentId: detail.parent?.uuid ?? null,
3050
3118
  name: detail.name,
3051
- itemCount: detail.tracksCount,
3119
+ itemCount: scope === "reference" ? (0, import_genre_tree28.genreMusicCount)(import_genre_tree28.GenrePlaylistSummarySchema.parse(detail.criteriaPlaylist)) : detail.tracksCount,
3052
3120
  wikidataId: detail.wikidataId
3053
3121
  },
3054
3122
  fill: rootColor,
@@ -3058,19 +3126,19 @@ function GenreDetailPanel({
3058
3126
  ({ ascendant }, index) => relative(ascendant, index === 0 ? null : ancestors2[index - 1].ascendant.uuid)
3059
3127
  )
3060
3128
  };
3061
- }, [detail]);
3129
+ }, [detail, scope]);
3062
3130
  if (isError) {
3063
- return /* @__PURE__ */ (0, import_jsx_runtime39.jsx)("div", { role: "alert", className: "p-4 text-sm text-[var(--color-text)]", children: "This genre could not be loaded." });
3131
+ return /* @__PURE__ */ (0, import_jsx_runtime40.jsx)("div", { role: "alert", className: "p-4 text-sm text-[var(--color-text)]", children: "This genre could not be loaded." });
3064
3132
  }
3065
3133
  if (isPending || !detail || !view) {
3066
- return /* @__PURE__ */ (0, import_jsx_runtime39.jsxs)("div", { className: "flex flex-col gap-3 p-4", "aria-busy": "true", "data-testid": "genre-detail-panel-skeleton", children: [
3067
- /* @__PURE__ */ (0, import_jsx_runtime39.jsx)(import_ui17.Skeleton, { className: "h-6 w-2/3" }),
3068
- /* @__PURE__ */ (0, import_jsx_runtime39.jsx)(import_ui17.Skeleton, { className: "h-4 w-full" }),
3069
- /* @__PURE__ */ (0, import_jsx_runtime39.jsx)(import_ui17.Skeleton, { className: "h-4 w-3/4" })
3134
+ return /* @__PURE__ */ (0, import_jsx_runtime40.jsxs)("div", { className: "flex flex-col gap-3 p-4", "aria-busy": "true", "data-testid": "genre-detail-panel-skeleton", children: [
3135
+ /* @__PURE__ */ (0, import_jsx_runtime40.jsx)(import_ui17.Skeleton, { className: "h-6 w-2/3" }),
3136
+ /* @__PURE__ */ (0, import_jsx_runtime40.jsx)(import_ui17.Skeleton, { className: "h-4 w-full" }),
3137
+ /* @__PURE__ */ (0, import_jsx_runtime40.jsx)(import_ui17.Skeleton, { className: "h-4 w-3/4" })
3070
3138
  ] });
3071
3139
  }
3072
3140
  const selectGenre = (nodeId) => onSelectGenre(nodeId);
3073
- const family = /* @__PURE__ */ (0, import_jsx_runtime39.jsx)(
3141
+ const family = /* @__PURE__ */ (0, import_jsx_runtime40.jsx)(
3074
3142
  import_genre_tree_view5.GenreNodeFamily,
3075
3143
  {
3076
3144
  node: view.node,
@@ -3081,20 +3149,20 @@ function GenreDetailPanel({
3081
3149
  onSelectNode: selectGenre
3082
3150
  }
3083
3151
  );
3084
- const ancestors = /* @__PURE__ */ (0, import_jsx_runtime39.jsx)(import_genre_tree_view5.GenreNodeAncestors, { ancestorNodes: view.ancestorNodes, onSelectNode: selectGenre });
3085
- return /* @__PURE__ */ (0, import_jsx_runtime39.jsxs)(
3152
+ const ancestors = /* @__PURE__ */ (0, import_jsx_runtime40.jsx)(import_genre_tree_view5.GenreNodeAncestors, { ancestorNodes: view.ancestorNodes, onSelectNode: selectGenre });
3153
+ return /* @__PURE__ */ (0, import_jsx_runtime40.jsxs)(
3086
3154
  import_ui17.DetailPanel,
3087
3155
  {
3088
3156
  side: "left",
3089
3157
  title: view.node.name,
3090
3158
  headerAccent: { background: view.fill, color: import_genre_tree_view5.ACCENT_TEXT_COLOR },
3091
- headerActions: onExpandedChange ? /* @__PURE__ */ (0, import_jsx_runtime39.jsx)(
3159
+ headerActions: onExpandedChange ? /* @__PURE__ */ (0, import_jsx_runtime40.jsx)(
3092
3160
  import_ui17.IconButton,
3093
3161
  {
3094
3162
  "aria-label": expanded ? "Shrink panel" : "Extend panel",
3095
3163
  style: { color: "inherit" },
3096
3164
  onClick: () => onExpandedChange(!expanded),
3097
- children: expanded ? /* @__PURE__ */ (0, import_jsx_runtime39.jsx)(import_lucide_react13.Minimize2, { "aria-hidden": "true", className: "h-4 w-4" }) : /* @__PURE__ */ (0, import_jsx_runtime39.jsx)(import_lucide_react13.Maximize2, { "aria-hidden": "true", className: "h-4 w-4" })
3165
+ children: expanded ? /* @__PURE__ */ (0, import_jsx_runtime40.jsx)(import_lucide_react13.Minimize2, { "aria-hidden": "true", className: "h-4 w-4" }) : /* @__PURE__ */ (0, import_jsx_runtime40.jsx)(import_lucide_react13.Maximize2, { "aria-hidden": "true", className: "h-4 w-4" })
3098
3166
  }
3099
3167
  ) : void 0,
3100
3168
  onClose,
@@ -3103,15 +3171,16 @@ function GenreDetailPanel({
3103
3171
  onExpandedChange,
3104
3172
  edgeTab,
3105
3173
  children: [
3106
- /* @__PURE__ */ (0, import_jsx_runtime39.jsx)(
3174
+ /* @__PURE__ */ (0, import_jsx_runtime40.jsx)(
3107
3175
  GenreInfoPanelBody,
3108
3176
  {
3109
- family: /* @__PURE__ */ (0, import_jsx_runtime39.jsxs)(import_jsx_runtime39.Fragment, { children: [
3177
+ family: /* @__PURE__ */ (0, import_jsx_runtime40.jsxs)(import_jsx_runtime40.Fragment, { children: [
3110
3178
  family,
3111
3179
  ancestors
3112
3180
  ] }),
3113
3181
  genreUuid: detail.uuid,
3114
- genrePlaylist: { ...detail.criteriaPlaylist, tracksCount: detail.tracksCount },
3182
+ genrePlaylist: scope === "reference" ? import_genre_tree28.GenrePlaylistSummarySchema.parse(detail.criteriaPlaylist) : { ...detail.criteriaPlaylist, tracksCount: detail.tracksCount },
3183
+ catalogue: detail.catalogue,
3115
3184
  wikidataId: detail.wikidataId,
3116
3185
  scope,
3117
3186
  getBackendBaseUrl,
@@ -3130,13 +3199,13 @@ function GenreDetailPanel({
3130
3199
  var import_genre_tree_view6 = require("@behindthemusictree/genre-tree-view");
3131
3200
 
3132
3201
  // src/genre-tree/track-list-sidebar/TrackListSidebar.tsx
3133
- var import_react18 = require("react");
3202
+ var import_react19 = require("react");
3134
3203
  var import_lucide_react14 = require("lucide-react");
3135
- var import_genre_tree23 = require("@behindthemusictree/app-kit/genre-tree");
3136
- var import_genre_tree24 = require("@behindthemusictree/app-kit/genre-tree");
3137
- var import_track_noun3 = require("@behindthemusictree/app-kit/track-noun");
3138
- var import_jsx_runtime40 = require("react/jsx-runtime");
3139
- var useBrowserLayoutEffect = typeof window === "undefined" ? import_react18.useEffect : import_react18.useLayoutEffect;
3204
+ var import_genre_tree29 = require("@behindthemusictree/app-kit/genre-tree");
3205
+ var import_genre_tree30 = require("@behindthemusictree/app-kit/genre-tree");
3206
+ var import_track_noun4 = require("@behindthemusictree/app-kit/track-noun");
3207
+ var import_jsx_runtime41 = require("react/jsx-runtime");
3208
+ var useBrowserLayoutEffect = typeof window === "undefined" ? import_react19.useEffect : import_react19.useLayoutEffect;
3140
3209
  function TrackListSidebar({
3141
3210
  className: className2 = "",
3142
3211
  renderDuration,
@@ -3148,18 +3217,18 @@ function TrackListSidebar({
3148
3217
  renderArtistLink,
3149
3218
  renderSongLink
3150
3219
  }) {
3151
- const { trackList, loadMore, autoAdvanceScrollRequest } = (0, import_genre_tree23.useTrackList)();
3152
- const trackNoun = (0, import_track_noun3.useTrackNoun)();
3153
- const trackListRef = (0, import_react18.useRef)(null);
3154
- const previousOriginRef = (0, import_react18.useRef)(trackList?.origin ?? null);
3155
- const handledAutoAdvanceRequestIdRef = (0, import_react18.useRef)(null);
3156
- const shouldFollowPlaybackRef = (0, import_react18.useRef)(true);
3157
- const programmaticScrollTopRef = (0, import_react18.useRef)(null);
3158
- const userScrollIntentRef = (0, import_react18.useRef)(false);
3159
- const userScrollIntentTimerRef = (0, import_react18.useRef)(null);
3160
- const loadMoreSentinelRef = (0, import_react18.useRef)(null);
3220
+ const { trackList, loadMore, autoAdvanceScrollRequest } = (0, import_genre_tree29.useTrackList)();
3221
+ const trackNoun = (0, import_track_noun4.useTrackNoun)();
3222
+ const trackListRef = (0, import_react19.useRef)(null);
3223
+ const previousOriginRef = (0, import_react19.useRef)(trackList?.origin ?? null);
3224
+ const handledAutoAdvanceRequestIdRef = (0, import_react19.useRef)(null);
3225
+ const shouldFollowPlaybackRef = (0, import_react19.useRef)(true);
3226
+ const programmaticScrollTopRef = (0, import_react19.useRef)(null);
3227
+ const userScrollIntentRef = (0, import_react19.useRef)(false);
3228
+ const userScrollIntentTimerRef = (0, import_react19.useRef)(null);
3229
+ const loadMoreSentinelRef = (0, import_react19.useRef)(null);
3161
3230
  const hasMore = !!trackList && trackList.nextPage !== null;
3162
- (0, import_react18.useEffect)(() => {
3231
+ (0, import_react19.useEffect)(() => {
3163
3232
  const sentinel = loadMoreSentinelRef.current;
3164
3233
  if (!hasMore || !sentinel) return;
3165
3234
  const observer = new IntersectionObserver((entries) => {
@@ -3179,11 +3248,11 @@ function TrackListSidebar({
3179
3248
  programmaticScrollTopRef.current = null;
3180
3249
  if (trackListRef.current) trackListRef.current.scrollTop = 0;
3181
3250
  }, [trackList?.origin]);
3182
- (0, import_react18.useEffect)(() => {
3251
+ (0, import_react19.useEffect)(() => {
3183
3252
  const list = trackListRef.current;
3184
3253
  if (!isCollapsed && list?.scrollTop === 0) shouldFollowPlaybackRef.current = true;
3185
3254
  }, [isCollapsed]);
3186
- (0, import_react18.useEffect)(
3255
+ (0, import_react19.useEffect)(
3187
3256
  () => () => {
3188
3257
  if (userScrollIntentTimerRef.current !== null) clearTimeout(userScrollIntentTimerRef.current);
3189
3258
  },
@@ -3237,25 +3306,25 @@ function TrackListSidebar({
3237
3306
  }
3238
3307
  };
3239
3308
  const positionClasses = layout === "inline" ? "relative w-full flex h-full flex-col" : "fixed right-0 w-[50rem] flex flex-col";
3240
- return trackList ? /* @__PURE__ */ (0, import_jsx_runtime40.jsxs)(
3309
+ return trackList ? /* @__PURE__ */ (0, import_jsx_runtime41.jsxs)(
3241
3310
  "div",
3242
3311
  {
3243
3312
  "data-theme": "dark",
3244
3313
  className: `track-list-sidebar ${positionClasses} rounded-2xl bg-[var(--color-surface)] pb-1 ${className2}`,
3245
3314
  style: layout === "fixed" ? { top: "63.5px", maxHeight: "calc(100vh - 63.5px - 79px)" } : void 0,
3246
3315
  children: [
3247
- /* @__PURE__ */ (0, import_jsx_runtime40.jsxs)("div", { className: "header flex h-16 items-center px-4 py-2 text-[var(--color-text)]", children: [
3248
- /* @__PURE__ */ (0, import_jsx_runtime40.jsxs)("div", { className: "min-w-0 flex flex-1 flex-col justify-center", children: [
3249
- /* @__PURE__ */ (0, import_jsx_runtime40.jsxs)("div", { className: "origin flex text-xl", children: [
3250
- /* @__PURE__ */ (0, import_jsx_runtime40.jsx)("div", { className: "from h-auto flex flex-col justify-center items-center mr-2", children: "From" }),
3251
- /* @__PURE__ */ (0, import_jsx_runtime40.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_runtime40.jsx)("div", { className: "name max-w-full truncate", children: trackList.origin.label }) })
3316
+ /* @__PURE__ */ (0, import_jsx_runtime41.jsxs)("div", { className: "header flex h-16 items-center px-4 py-2 text-[var(--color-text)]", children: [
3317
+ /* @__PURE__ */ (0, import_jsx_runtime41.jsxs)("div", { className: "min-w-0 flex flex-1 flex-col justify-center", children: [
3318
+ /* @__PURE__ */ (0, import_jsx_runtime41.jsxs)("div", { className: "origin flex text-xl", children: [
3319
+ /* @__PURE__ */ (0, import_jsx_runtime41.jsx)("div", { className: "from h-auto flex flex-col justify-center items-center mr-2", children: "From" }),
3320
+ /* @__PURE__ */ (0, import_jsx_runtime41.jsx)("div", { className: "name-container flex flex-col justify-center items-center text-[var(--color-text)] font-bold pr-2 max-w-[27.5rem]", children: /* @__PURE__ */ (0, import_jsx_runtime41.jsx)("div", { className: "name max-w-full truncate", children: trackList.origin.label }) })
3252
3321
  ] }),
3253
- /* @__PURE__ */ (0, import_jsx_runtime40.jsxs)("div", { className: "info flex items-center text-sm text-[var(--color-text-muted)]", children: [
3254
- 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 `,
3322
+ /* @__PURE__ */ (0, import_jsx_runtime41.jsxs)("div", { className: "info flex items-center text-sm text-[var(--color-text-muted)]", children: [
3323
+ 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 `,
3255
3324
  `${trackList.total} ${trackList.total > 1 ? trackNoun.plural : trackNoun.singular}`
3256
3325
  ] })
3257
3326
  ] }),
3258
- (isCollapsed && onExpand || !isCollapsed && onMinimize) && /* @__PURE__ */ (0, import_jsx_runtime40.jsx)(
3327
+ (isCollapsed && onExpand || !isCollapsed && onMinimize) && /* @__PURE__ */ (0, import_jsx_runtime41.jsx)(
3259
3328
  "button",
3260
3329
  {
3261
3330
  type: "button",
@@ -3264,11 +3333,11 @@ function TrackListSidebar({
3264
3333
  "aria-expanded": !isCollapsed,
3265
3334
  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"}`,
3266
3335
  onClick: isCollapsed ? onExpand : onMinimize,
3267
- children: /* @__PURE__ */ (0, import_jsx_runtime40.jsx)(import_lucide_react14.ListMusic, { "aria-hidden": "true", className: "h-5 w-5" })
3336
+ children: /* @__PURE__ */ (0, import_jsx_runtime41.jsx)(import_lucide_react14.ListMusic, { "aria-hidden": "true", className: "h-5 w-5" })
3268
3337
  }
3269
3338
  )
3270
3339
  ] }),
3271
- !isCollapsed && /* @__PURE__ */ (0, import_jsx_runtime40.jsxs)(
3340
+ !isCollapsed && /* @__PURE__ */ (0, import_jsx_runtime41.jsxs)(
3272
3341
  "ul",
3273
3342
  {
3274
3343
  ref: trackListRef,
@@ -3278,7 +3347,7 @@ function TrackListSidebar({
3278
3347
  onPointerDown: markUserScrollIntent,
3279
3348
  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",
3280
3349
  children: [
3281
- trackList.tracks.map((track, index) => /* @__PURE__ */ (0, import_jsx_runtime40.jsx)("li", { "data-track-uuid": track.uuid, children: /* @__PURE__ */ (0, import_jsx_runtime40.jsx)(
3350
+ trackList.tracks.map((track, index) => /* @__PURE__ */ (0, import_jsx_runtime41.jsx)("li", { "data-track-uuid": track.uuid, children: /* @__PURE__ */ (0, import_jsx_runtime41.jsx)(
3282
3351
  TrackItem,
3283
3352
  {
3284
3353
  track,
@@ -3289,7 +3358,7 @@ function TrackListSidebar({
3289
3358
  renderSongLink
3290
3359
  }
3291
3360
  ) }, track.uuid)),
3292
- hasMore && /* @__PURE__ */ (0, import_jsx_runtime40.jsx)("li", { ref: loadMoreSentinelRef, "aria-hidden": "true", className: "h-px" })
3361
+ hasMore && /* @__PURE__ */ (0, import_jsx_runtime41.jsx)("li", { ref: loadMoreSentinelRef, "aria-hidden": "true", className: "h-px" })
3293
3362
  ]
3294
3363
  }
3295
3364
  )
@@ -3299,17 +3368,17 @@ function TrackListSidebar({
3299
3368
  }
3300
3369
 
3301
3370
  // src/genre-tree/PlayerPanel.tsx
3302
- var import_react19 = require("react");
3371
+ var import_react20 = require("react");
3303
3372
  var import_tailwind_merge = require("tailwind-merge");
3304
3373
  var import_player13 = require("@behindthemusictree/app-kit/player");
3305
- var import_genre_tree26 = require("@behindthemusictree/app-kit/genre-tree");
3374
+ var import_genre_tree32 = require("@behindthemusictree/app-kit/genre-tree");
3306
3375
  var import_ui18 = require("@behindthemusictree/ui");
3307
3376
 
3308
3377
  // src/genre-tree/PlayerControls.tsx
3309
3378
  var import_lucide_react15 = require("lucide-react");
3310
- var import_genre_tree25 = require("@behindthemusictree/app-kit/genre-tree");
3379
+ var import_genre_tree31 = require("@behindthemusictree/app-kit/genre-tree");
3311
3380
  var import_player12 = require("@behindthemusictree/app-kit/player");
3312
- var import_jsx_runtime41 = require("react/jsx-runtime");
3381
+ var import_jsx_runtime42 = require("react/jsx-runtime");
3313
3382
  function PlayerControls() {
3314
3383
  const {
3315
3384
  duration,
@@ -3321,22 +3390,22 @@ function PlayerControls() {
3321
3390
  setVolume,
3322
3391
  volume
3323
3392
  } = (0, import_player12.usePlayer)();
3324
- const { hasNextTrack, hasPreviousTrack, playNextTrack, playPreviousTrack } = (0, import_genre_tree25.useTrackList)();
3393
+ const { hasNextTrack, hasPreviousTrack, playNextTrack, playPreviousTrack } = (0, import_genre_tree31.useTrackList)();
3325
3394
  const currentTime = (0, import_player12.useCurrentTime)();
3326
3395
  const isBusy = isLoading || playState === import_player12.PlayStates.LOADING;
3327
3396
  const isReady = !!playerTrackObject?.isReady && !isBusy && !playerTrackObject.loadError;
3328
3397
  const safeDuration = Number.isFinite(duration) && duration > 0 ? duration : 0;
3329
3398
  const safeCurrentTime = Number.isFinite(currentTime) ? Math.max(0, Math.min(currentTime, safeDuration)) : 0;
3330
3399
  const isPlaying = playState === import_player12.PlayStates.PLAYING;
3331
- return /* @__PURE__ */ (0, import_jsx_runtime41.jsxs)(
3400
+ return /* @__PURE__ */ (0, import_jsx_runtime42.jsxs)(
3332
3401
  "section",
3333
3402
  {
3334
3403
  "aria-label": "Player controls",
3335
3404
  className: "player-controls flex shrink-0 flex-col gap-2 border-b border-zinc-800 px-3 py-2 text-zinc-200",
3336
3405
  children: [
3337
- /* @__PURE__ */ (0, import_jsx_runtime41.jsxs)("div", { className: "flex min-w-0 items-center gap-2 text-xs tabular-nums text-zinc-400", children: [
3338
- /* @__PURE__ */ (0, import_jsx_runtime41.jsx)("span", { "aria-label": "Elapsed time", children: (0, import_genre_tree25.formatTime)(safeCurrentTime) }),
3339
- /* @__PURE__ */ (0, import_jsx_runtime41.jsx)(
3406
+ /* @__PURE__ */ (0, import_jsx_runtime42.jsxs)("div", { className: "flex min-w-0 items-center gap-2 text-xs tabular-nums text-zinc-400", children: [
3407
+ /* @__PURE__ */ (0, import_jsx_runtime42.jsx)("span", { "aria-label": "Elapsed time", children: (0, import_genre_tree31.formatTime)(safeCurrentTime) }),
3408
+ /* @__PURE__ */ (0, import_jsx_runtime42.jsx)(
3340
3409
  "input",
3341
3410
  {
3342
3411
  "aria-label": "Seek",
@@ -3350,11 +3419,11 @@ function PlayerControls() {
3350
3419
  onChange: (event) => seekTo(Number(event.currentTarget.value))
3351
3420
  }
3352
3421
  ),
3353
- /* @__PURE__ */ (0, import_jsx_runtime41.jsx)("span", { "aria-label": "Duration", children: (0, import_genre_tree25.formatTime)(safeDuration) })
3422
+ /* @__PURE__ */ (0, import_jsx_runtime42.jsx)("span", { "aria-label": "Duration", children: (0, import_genre_tree31.formatTime)(safeDuration) })
3354
3423
  ] }),
3355
- /* @__PURE__ */ (0, import_jsx_runtime41.jsxs)("div", { className: "grid min-h-9 grid-cols-[1fr_auto_1fr] items-center gap-3", children: [
3356
- /* @__PURE__ */ (0, import_jsx_runtime41.jsxs)("div", { className: "col-start-2 flex items-center justify-center gap-2", children: [
3357
- /* @__PURE__ */ (0, import_jsx_runtime41.jsx)(
3424
+ /* @__PURE__ */ (0, import_jsx_runtime42.jsxs)("div", { className: "grid min-h-9 grid-cols-[1fr_auto_1fr] items-center gap-3", children: [
3425
+ /* @__PURE__ */ (0, import_jsx_runtime42.jsxs)("div", { className: "col-start-2 flex items-center justify-center gap-2", children: [
3426
+ /* @__PURE__ */ (0, import_jsx_runtime42.jsx)(
3358
3427
  "button",
3359
3428
  {
3360
3429
  type: "button",
@@ -3363,10 +3432,10 @@ function PlayerControls() {
3363
3432
  disabled: !isReady || !hasPreviousTrack,
3364
3433
  onClick: playPreviousTrack,
3365
3434
  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",
3366
- children: /* @__PURE__ */ (0, import_jsx_runtime41.jsx)(import_lucide_react15.SkipBack, { "aria-hidden": "true", className: "h-4 w-4" })
3435
+ children: /* @__PURE__ */ (0, import_jsx_runtime42.jsx)(import_lucide_react15.SkipBack, { "aria-hidden": "true", className: "h-4 w-4" })
3367
3436
  }
3368
3437
  ),
3369
- /* @__PURE__ */ (0, import_jsx_runtime41.jsx)(
3438
+ /* @__PURE__ */ (0, import_jsx_runtime42.jsx)(
3370
3439
  "button",
3371
3440
  {
3372
3441
  type: "button",
@@ -3375,10 +3444,10 @@ function PlayerControls() {
3375
3444
  disabled: !isReady,
3376
3445
  onClick: handlePlayPauseAction,
3377
3446
  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",
3378
- children: isPlaying ? /* @__PURE__ */ (0, import_jsx_runtime41.jsx)(import_lucide_react15.Pause, { "aria-hidden": "true", className: "h-4 w-4", fill: "currentColor" }) : /* @__PURE__ */ (0, import_jsx_runtime41.jsx)(import_lucide_react15.Play, { "aria-hidden": "true", className: "h-4 w-4", fill: "currentColor" })
3447
+ children: isPlaying ? /* @__PURE__ */ (0, import_jsx_runtime42.jsx)(import_lucide_react15.Pause, { "aria-hidden": "true", className: "h-4 w-4", fill: "currentColor" }) : /* @__PURE__ */ (0, import_jsx_runtime42.jsx)(import_lucide_react15.Play, { "aria-hidden": "true", className: "h-4 w-4", fill: "currentColor" })
3379
3448
  }
3380
3449
  ),
3381
- /* @__PURE__ */ (0, import_jsx_runtime41.jsx)(
3450
+ /* @__PURE__ */ (0, import_jsx_runtime42.jsx)(
3382
3451
  "button",
3383
3452
  {
3384
3453
  type: "button",
@@ -3387,14 +3456,14 @@ function PlayerControls() {
3387
3456
  disabled: !isReady || !hasNextTrack,
3388
3457
  onClick: playNextTrack,
3389
3458
  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",
3390
- children: /* @__PURE__ */ (0, import_jsx_runtime41.jsx)(import_lucide_react15.SkipForward, { "aria-hidden": "true", className: "h-4 w-4" })
3459
+ children: /* @__PURE__ */ (0, import_jsx_runtime42.jsx)(import_lucide_react15.SkipForward, { "aria-hidden": "true", className: "h-4 w-4" })
3391
3460
  }
3392
3461
  )
3393
3462
  ] }),
3394
- /* @__PURE__ */ (0, import_jsx_runtime41.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: [
3395
- volume === 0 ? /* @__PURE__ */ (0, import_jsx_runtime41.jsx)(import_lucide_react15.VolumeX, { "aria-hidden": "true", className: "h-[0.7rem] w-[0.7rem] shrink-0" }) : /* @__PURE__ */ (0, import_jsx_runtime41.jsx)(import_lucide_react15.Volume2, { "aria-hidden": "true", className: "h-[0.7rem] w-[0.7rem] shrink-0" }),
3396
- /* @__PURE__ */ (0, import_jsx_runtime41.jsx)("span", { className: "sr-only", children: "Volume" }),
3397
- /* @__PURE__ */ (0, import_jsx_runtime41.jsx)(
3463
+ /* @__PURE__ */ (0, import_jsx_runtime42.jsxs)("label", { className: "col-start-3 flex w-[4.2rem] shrink-0 items-center gap-[0.35rem] justify-self-end text-zinc-400 sm:w-[4.9rem]", children: [
3464
+ volume === 0 ? /* @__PURE__ */ (0, import_jsx_runtime42.jsx)(import_lucide_react15.VolumeX, { "aria-hidden": "true", className: "h-[0.7rem] w-[0.7rem] shrink-0" }) : /* @__PURE__ */ (0, import_jsx_runtime42.jsx)(import_lucide_react15.Volume2, { "aria-hidden": "true", className: "h-[0.7rem] w-[0.7rem] shrink-0" }),
3465
+ /* @__PURE__ */ (0, import_jsx_runtime42.jsx)("span", { className: "sr-only", children: "Volume" }),
3466
+ /* @__PURE__ */ (0, import_jsx_runtime42.jsx)(
3398
3467
  "input",
3399
3468
  {
3400
3469
  "aria-label": "Volume",
@@ -3416,7 +3485,7 @@ function PlayerControls() {
3416
3485
  }
3417
3486
 
3418
3487
  // src/genre-tree/PlayerPanel.tsx
3419
- var import_jsx_runtime42 = require("react/jsx-runtime");
3488
+ var import_jsx_runtime43 = require("react/jsx-runtime");
3420
3489
  var PANEL_ID = "player-panel";
3421
3490
  var DEFAULT_EXTENDED_WIDTH = "calc(100vw - var(--layout-center-left) - var(--layout-gutter))";
3422
3491
  function PlayerPanel({
@@ -3429,13 +3498,13 @@ function PlayerPanel({
3429
3498
  renderSongLink
3430
3499
  }) {
3431
3500
  const { playerTrackObject, retryPlayback } = (0, import_player13.usePlayer)();
3432
- const { toggleTrackListSidebar, isTrackListSidebarVisible } = (0, import_genre_tree26.useTrackListSidebarVisibility)();
3433
- const [lastLoadedTrackTitle, setLastLoadedTrackTitle] = (0, import_react19.useState)("");
3501
+ const { toggleTrackListSidebar, isTrackListSidebarVisible } = (0, import_genre_tree32.useTrackListSidebarVisibility)();
3502
+ const [lastLoadedTrackTitle, setLastLoadedTrackTitle] = (0, import_react20.useState)("");
3434
3503
  const loadedTrackTitle = playerTrackObject?.track.title;
3435
- (0, import_react19.useEffect)(() => {
3504
+ (0, import_react20.useEffect)(() => {
3436
3505
  if (loadedTrackTitle) setLastLoadedTrackTitle(loadedTrackTitle);
3437
3506
  }, [loadedTrackTitle]);
3438
- const [uncontrolledExtended, setUncontrolledExtended] = (0, import_react19.useState)(false);
3507
+ const [uncontrolledExtended, setUncontrolledExtended] = (0, import_react20.useState)(false);
3439
3508
  const isExtendRequested = expanded ?? uncontrolledExtended;
3440
3509
  const setIsExtendRequested = (next) => {
3441
3510
  setUncontrolledExtended(next);
@@ -3456,7 +3525,7 @@ function PlayerPanel({
3456
3525
  onClick: () => setIsExtendRequested(true),
3457
3526
  expanded: false
3458
3527
  } : null;
3459
- return /* @__PURE__ */ (0, import_jsx_runtime42.jsxs)(
3528
+ return /* @__PURE__ */ (0, import_jsx_runtime43.jsxs)(
3460
3529
  "div",
3461
3530
  {
3462
3531
  id: PANEL_ID,
@@ -3469,7 +3538,7 @@ function PlayerPanel({
3469
3538
  ),
3470
3539
  style: isExtended ? { width: extendedWidth } : void 0,
3471
3540
  children: [
3472
- /* @__PURE__ */ (0, import_jsx_runtime42.jsx)(
3541
+ /* @__PURE__ */ (0, import_jsx_runtime43.jsx)(
3473
3542
  import_ui18.PanelEdgeTab,
3474
3543
  {
3475
3544
  side: "right",
@@ -3479,7 +3548,7 @@ function PlayerPanel({
3479
3548
  className: "absolute right-full top-1/2 -translate-y-1/2"
3480
3549
  }
3481
3550
  ),
3482
- /* @__PURE__ */ (0, import_jsx_runtime42.jsxs)(
3551
+ /* @__PURE__ */ (0, import_jsx_runtime43.jsxs)(
3483
3552
  "div",
3484
3553
  {
3485
3554
  className: (0, import_tailwind_merge.twMerge)(
@@ -3487,20 +3556,20 @@ function PlayerPanel({
3487
3556
  isExtended ? "flex-row" : "flex-col"
3488
3557
  ),
3489
3558
  children: [
3490
- /* @__PURE__ */ (0, import_jsx_runtime42.jsxs)("div", { className: "flex w-[var(--layout-right-w)] shrink-0 flex-col", children: [
3491
- /* @__PURE__ */ (0, import_jsx_runtime42.jsx)("div", { className: "flex shrink-0 items-center justify-between gap-2 border-b border-zinc-800 p-2", children: /* @__PURE__ */ (0, import_jsx_runtime42.jsx)("span", { className: "min-w-0 truncate text-sm font-semibold", children: loadedTrackTitle || lastLoadedTrackTitle }) }),
3492
- /* @__PURE__ */ (0, import_jsx_runtime42.jsxs)("div", { className: "relative w-full shrink-0", children: [
3493
- /* @__PURE__ */ (0, import_jsx_runtime42.jsx)(import_player13.PlayerVideoSurface, { className: "aspect-video w-full bg-black" }),
3494
- playerTrackObject?.loadError && /* @__PURE__ */ (0, import_jsx_runtime42.jsxs)("span", { className: "absolute bottom-2 left-2 right-2 text-xs text-red-400", children: [
3559
+ /* @__PURE__ */ (0, import_jsx_runtime43.jsxs)("div", { className: "flex w-[var(--layout-right-w)] shrink-0 flex-col", children: [
3560
+ /* @__PURE__ */ (0, import_jsx_runtime43.jsx)("div", { className: "flex shrink-0 items-center justify-between gap-2 border-b border-zinc-800 p-2", children: /* @__PURE__ */ (0, import_jsx_runtime43.jsx)("span", { className: "min-w-0 truncate text-sm font-semibold", children: loadedTrackTitle || lastLoadedTrackTitle }) }),
3561
+ /* @__PURE__ */ (0, import_jsx_runtime43.jsxs)("div", { className: "relative w-full shrink-0", children: [
3562
+ /* @__PURE__ */ (0, import_jsx_runtime43.jsx)(import_player13.PlayerVideoSurface, { className: "aspect-video w-full bg-black" }),
3563
+ playerTrackObject?.loadError && /* @__PURE__ */ (0, import_jsx_runtime43.jsxs)("span", { className: "absolute bottom-2 left-2 right-2 text-xs text-red-400", children: [
3495
3564
  playerTrackObject.loadError,
3496
- /* @__PURE__ */ (0, import_jsx_runtime42.jsx)("button", { type: "button", className: "ml-2 underline", onClick: () => {
3565
+ /* @__PURE__ */ (0, import_jsx_runtime43.jsx)("button", { type: "button", className: "ml-2 underline", onClick: () => {
3497
3566
  void retryPlayback();
3498
3567
  }, children: "Retry" })
3499
3568
  ] })
3500
3569
  ] }),
3501
- /* @__PURE__ */ (0, import_jsx_runtime42.jsx)(PlayerControls, {})
3570
+ /* @__PURE__ */ (0, import_jsx_runtime43.jsx)(PlayerControls, {})
3502
3571
  ] }),
3503
- /* @__PURE__ */ (0, import_jsx_runtime42.jsx)(
3572
+ /* @__PURE__ */ (0, import_jsx_runtime43.jsx)(
3504
3573
  TrackListSidebar,
3505
3574
  {
3506
3575
  layout: "inline",
@@ -3526,33 +3595,33 @@ function PlayerPanel({
3526
3595
  // src/genre-tree/Rating.tsx
3527
3596
  var import_uuid = require("uuid");
3528
3597
  var import_ai = require("react-icons/ai");
3529
- var import_genre_tree27 = require("@behindthemusictree/app-kit/genre-tree");
3530
- var import_jsx_runtime43 = require("react/jsx-runtime");
3598
+ var import_genre_tree33 = require("@behindthemusictree/app-kit/genre-tree");
3599
+ var import_jsx_runtime44 = require("react/jsx-runtime");
3531
3600
  function Rating({ rating, handleChange }) {
3532
3601
  const instanceId = (0, import_uuid.v4)();
3533
- return /* @__PURE__ */ (0, import_jsx_runtime43.jsx)("div", { className: "flex items-center", children: [...Array(6)].map((_, i) => /* @__PURE__ */ (0, import_jsx_runtime43.jsxs)("div", { className: `relative ${i === 0 ? "mr-4" : "mr-1"}`, children: [
3534
- /* @__PURE__ */ (0, import_jsx_runtime43.jsx)(
3602
+ return /* @__PURE__ */ (0, import_jsx_runtime44.jsx)("div", { className: "flex items-center", children: [...Array(6)].map((_, i) => /* @__PURE__ */ (0, import_jsx_runtime44.jsxs)("div", { className: `relative ${i === 0 ? "mr-4" : "mr-1"}`, children: [
3603
+ /* @__PURE__ */ (0, import_jsx_runtime44.jsx)(
3535
3604
  "input",
3536
3605
  {
3537
3606
  id: `star-${instanceId}-${i}`,
3538
3607
  className: "w-6 h-6 opacity-0",
3539
3608
  type: "radio",
3540
3609
  name: "rating",
3541
- value: i === 0 ? import_genre_tree27.FORM_RATING_NULL_VALUE : i,
3610
+ value: i === 0 ? import_genre_tree33.FORM_RATING_NULL_VALUE : i,
3542
3611
  checked: rating === (i === 0 ? null : i),
3543
3612
  onChange: handleChange
3544
3613
  }
3545
3614
  ),
3546
- /* @__PURE__ */ (0, import_jsx_runtime43.jsx)("label", { htmlFor: `star-${instanceId}-${i}`, className: "cursor-pointer", children: i === 0 ? /* @__PURE__ */ (0, import_jsx_runtime43.jsxs)("div", { className: "flex", children: [
3547
- /* @__PURE__ */ (0, import_jsx_runtime43.jsx)(
3615
+ /* @__PURE__ */ (0, import_jsx_runtime44.jsx)("label", { htmlFor: `star-${instanceId}-${i}`, className: "cursor-pointer", children: i === 0 ? /* @__PURE__ */ (0, import_jsx_runtime44.jsxs)("div", { className: "flex", children: [
3616
+ /* @__PURE__ */ (0, import_jsx_runtime44.jsx)(
3548
3617
  import_ai.AiOutlineStar,
3549
3618
  {
3550
- 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)]"}`
3619
+ 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)]"}`
3551
3620
  }
3552
3621
  ),
3553
- /* @__PURE__ */ (0, import_jsx_runtime43.jsx)("span", { className: "flex-grow w-8" }),
3622
+ /* @__PURE__ */ (0, import_jsx_runtime44.jsx)("span", { className: "flex-grow w-8" }),
3554
3623
  " "
3555
- ] }) : /* @__PURE__ */ (0, import_jsx_runtime43.jsx)(
3624
+ ] }) : /* @__PURE__ */ (0, import_jsx_runtime44.jsx)(
3556
3625
  import_ai.AiFillStar,
3557
3626
  {
3558
3627
  className: `w-7 h-7 absolute top-0 left-0 ${(rating ?? -1) >= i ? "text-[var(--color-accent-rating)]" : "text-[var(--color-text-muted)]"}`