@behindthemusictree/app-kit-ui 34.1.0 → 35.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -46,13 +46,13 @@ __export(genre_tree_exports, {
46
46
  module.exports = __toCommonJS(genre_tree_exports);
47
47
 
48
48
  // src/genre-tree/GenreTreeView.tsx
49
- var import_react17 = require("react");
49
+ var import_react18 = require("react");
50
50
  var import_react_query3 = require("@tanstack/react-query");
51
- var import_lucide_react11 = require("lucide-react");
52
51
  var import_lucide_react12 = require("lucide-react");
53
- var import_ui16 = require("@behindthemusictree/ui");
54
- var import_genre_tree_view4 = require("@behindthemusictree/genre-tree-view");
55
- var import_genre_tree27 = require("@behindthemusictree/app-kit/genre-tree");
52
+ var import_lucide_react13 = require("lucide-react");
53
+ var import_ui17 = require("@behindthemusictree/ui");
54
+ var import_genre_tree_view5 = require("@behindthemusictree/genre-tree-view");
55
+ var import_genre_tree28 = 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
@@ -915,6 +915,7 @@ function GenreTreeSearch({
915
915
  artistSearch,
916
916
  songSearch,
917
917
  SearchGroups,
918
+ onSelectGenre,
918
919
  extraGroups = [],
919
920
  onQueryChange,
920
921
  onFocus,
@@ -940,9 +941,16 @@ function GenreTreeSearch({
940
941
  extraGroups: [...extraGroups, ...hostGroups, artistGroup, songGroup].filter((group) => group !== void 0)
941
942
  }
942
943
  );
943
- return SearchGroups ? /* @__PURE__ */ (0, import_jsx_runtime14.jsx)(SearchGroups, { query: deferredQuery, focused, children: render }) : render([]);
944
+ return SearchGroups ? /* @__PURE__ */ (0, import_jsx_runtime14.jsx)(SearchGroups, { query: deferredQuery, focused, onSelectGenre, children: render }) : render([]);
944
945
  }
945
946
 
947
+ // src/genre-tree/GenreDetailPanel.tsx
948
+ var import_react14 = require("react");
949
+ var import_genre_tree_view4 = require("@behindthemusictree/genre-tree-view");
950
+ var import_ui7 = require("@behindthemusictree/ui");
951
+ var import_lucide_react8 = require("lucide-react");
952
+ var import_genre_tree19 = require("@behindthemusictree/app-kit/genre-tree");
953
+
946
954
  // src/genre-tree/GenreInfoPanelBody.tsx
947
955
  var import_genre_tree18 = require("@behindthemusictree/app-kit/genre-tree");
948
956
  var import_track_noun3 = require("@behindthemusictree/app-kit/track-noun");
@@ -1391,6 +1399,7 @@ var nextCursor = (next) => {
1391
1399
  function GenrePlaylistTracks({
1392
1400
  genreUuid,
1393
1401
  genrePlaylist,
1402
+ genreName: selectedGenreName,
1394
1403
  selection,
1395
1404
  scope,
1396
1405
  getBackendBaseUrl,
@@ -1406,13 +1415,17 @@ function GenrePlaylistTracks({
1406
1415
  const { playerTrackObject, playState, handlePlayPauseAction } = (0, import_player4.usePlayer)();
1407
1416
  const { showPopup } = (0, import_popup4.usePopup)();
1408
1417
  const trackNoun = (0, import_track_noun.useTrackNoun)();
1409
- const { uuid } = genrePlaylist;
1418
+ const uuid = genrePlaylist?.uuid;
1419
+ const genreName = selectedGenreName ?? genrePlaylist?.name ?? "Genre";
1410
1420
  const { fetch } = (0, import_transport3.useFetchWrapper)(getBackendBaseUrl);
1411
1421
  const playback = (0, import_transport3.usePlaybackContext)();
1412
1422
  const { data, isPending, isError, refetch, hasNextPage, isFetchingNextPage, fetchNextPage } = (0, import_react_query.useInfiniteQuery)({
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] : []],
1423
+ queryKey: [...scope === "me" ? uuid ? import_genre_tree12.genrePlaylistQueryKeys.me.tracks(uuid) : ["missing-personal-genre-playlist", genreUuid] : 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] : []],
1414
1424
  queryFn: async ({ pageParam }) => {
1415
- if (scope === "me") return fetchGenrePlaylistTracksPage(uuid, scope, Number(pageParam));
1425
+ if (scope === "me") {
1426
+ if (!uuid) throw new Error("Personal genre has no playlist");
1427
+ return fetchGenrePlaylistTracksPage(uuid, scope, Number(pageParam));
1428
+ }
1416
1429
  const response = await fetch(
1417
1430
  import_genre_tree13.libraryEndpoints.reference.songs.list(),
1418
1431
  true,
@@ -1425,7 +1438,7 @@ function GenrePlaylistTracks({
1425
1438
  },
1426
1439
  initialPageParam: scope === "me" ? 1 : "",
1427
1440
  getNextPageParam: (lastPage) => lastPage.nextPage ?? void 0,
1428
- enabled: playback.ready && (scope !== "me" || sessionRestored && !!session?.accessToken)
1441
+ enabled: playback.ready && (scope !== "me" || uuid !== void 0 && sessionRestored && !!session?.accessToken)
1429
1442
  });
1430
1443
  const [sentinel, setSentinel] = (0, import_react12.useState)(null);
1431
1444
  (0, import_react12.useEffect)(() => {
@@ -1447,7 +1460,7 @@ function GenrePlaylistTracks({
1447
1460
  ordering: "title",
1448
1461
  genreUuid,
1449
1462
  ...selection && { selection }
1450
- }) : scope === "reference" ? playNewTrackListFromGenreCatalogue({ uuid: genreUuid, name: genrePlaylist.name }, scope, { startSongUuid, ...selection && { selection } }) : playNewTrackListFromGenrePlaylist(genrePlaylist, scope, { startSongUuid });
1463
+ }) : scope === "reference" ? playNewTrackListFromGenreCatalogue({ uuid: genreUuid, name: genreName }, scope, { startSongUuid, ...selection && { selection } }) : genrePlaylist ? playNewTrackListFromGenrePlaylist(genrePlaylist, scope, { startSongUuid }) : Promise.reject(new Error("Personal genre has no playlist"));
1451
1464
  started.catch((error) => {
1452
1465
  console.error("Failed to play genre playlist:", error);
1453
1466
  showPopup(/* @__PURE__ */ (0, import_jsx_runtime23.jsx)(InternalErrorPopup, { errorCode: import_transport6.ErrorCode.CLIENT_UNKNOWN }));
@@ -1455,13 +1468,13 @@ function GenrePlaylistTracks({
1455
1468
  };
1456
1469
  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: [
1457
1470
  renderTracksToolbar?.(),
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: [
1471
+ scope === "me" && !genrePlaylist ? /* @__PURE__ */ (0, import_jsx_runtime23.jsx)("p", { children: "\u2014" }) : 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: [
1459
1472
  "Couldn't load ",
1460
1473
  trackNoun.plural,
1461
1474
  ".",
1462
1475
  " ",
1463
1476
  /* @__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: [
1477
+ ] }) : 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": `${genreName} ${trackNoun.plural}`, children: [
1465
1478
  tracks.map((track, index) => {
1466
1479
  const label = track.artists?.length ? `${track.title} \u2014 ${track.artists.map((artist2) => artist2.name).join(", ")}` : track.title;
1467
1480
  const isCurrent = playerTrackObject?.track.id === track.uuid;
@@ -1528,6 +1541,7 @@ var import_jsx_runtime24 = require("react/jsx-runtime");
1528
1541
  function GenreDetailExtras({
1529
1542
  genreUuid,
1530
1543
  genrePlaylist,
1544
+ genreName,
1531
1545
  selection,
1532
1546
  wikidataId,
1533
1547
  scope,
@@ -1595,6 +1609,7 @@ function GenreDetailExtras({
1595
1609
  {
1596
1610
  genreUuid,
1597
1611
  genrePlaylist,
1612
+ genreName,
1598
1613
  selection,
1599
1614
  scope,
1600
1615
  getBackendBaseUrl,
@@ -1636,6 +1651,7 @@ function GenreInfoPanelBody({
1636
1651
  genreUuid,
1637
1652
  genrePlaylist,
1638
1653
  catalogue,
1654
+ countsContext,
1639
1655
  wikidataId,
1640
1656
  scope,
1641
1657
  getBackendBaseUrl,
@@ -1644,7 +1660,7 @@ function GenreInfoPanelBody({
1644
1660
  extras
1645
1661
  }) {
1646
1662
  const noun = (0, import_track_noun3.useTrackNoun)();
1647
- const summary = scope === "reference" ? import_genre_tree18.GenreCatalogueCountsSchema.parse(genrePlaylist) : void 0;
1663
+ const summary = scope === "reference" ? import_genre_tree18.GenreCatalogueCountsSchema.parse(genrePlaylist ?? catalogue) : void 0;
1648
1664
  const selection = summary ? (0, import_genre_tree18.defaultGenreMusicSelection)(summary) : void 0;
1649
1665
  const describeCounts = (counts) => {
1650
1666
  const scopes = counts.genreAndSubgenresTracksCount;
@@ -1652,17 +1668,19 @@ function GenreInfoPanelBody({
1652
1668
  };
1653
1669
  return /* @__PURE__ */ (0, import_jsx_runtime25.jsxs)(import_jsx_runtime25.Fragment, { children: [
1654
1670
  toolbar,
1655
- /* @__PURE__ */ (0, import_jsx_runtime25.jsx)(GenreNodeActionBand, { ...band, genrePlaylist, playback }),
1671
+ genrePlaylist ? /* @__PURE__ */ (0, import_jsx_runtime25.jsx)(GenreNodeActionBand, { ...band, genrePlaylist, playback }) : /* @__PURE__ */ (0, import_jsx_runtime25.jsx)(GenreActionBand, { ...band }),
1656
1672
  summary && catalogue && /* @__PURE__ */ (0, import_jsx_runtime25.jsxs)("div", { className: "px-3.5 py-3 text-sm text-[var(--color-text)]", children: [
1657
1673
  /* @__PURE__ */ (0, import_jsx_runtime25.jsxs)("p", { children: [
1658
1674
  "Catalogue ",
1659
1675
  noun.plural,
1676
+ countsContext ? ` (${countsContext})` : "",
1660
1677
  ": ",
1661
1678
  describeCounts(catalogue)
1662
1679
  ] }),
1663
- /* @__PURE__ */ (0, import_jsx_runtime25.jsxs)("p", { children: [
1680
+ genrePlaylist && /* @__PURE__ */ (0, import_jsx_runtime25.jsxs)("p", { children: [
1664
1681
  "Playlist ",
1665
1682
  noun.plural,
1683
+ countsContext ? ` (${countsContext})` : "",
1666
1684
  ": ",
1667
1685
  describeCounts(summary)
1668
1686
  ] })
@@ -1675,6 +1693,7 @@ function GenreInfoPanelBody({
1675
1693
  family,
1676
1694
  genreUuid,
1677
1695
  genrePlaylist,
1696
+ genreName: band.node.name,
1678
1697
  selection,
1679
1698
  wikidataId,
1680
1699
  scope,
@@ -1684,10 +1703,160 @@ function GenreInfoPanelBody({
1684
1703
  ] });
1685
1704
  }
1686
1705
 
1687
- // src/genre-tree/ChangeParentPopup.tsx
1688
- var import_react14 = require("react");
1689
- var import_genre_tree19 = require("@behindthemusictree/app-kit/genre-tree");
1706
+ // src/genre-tree/GenreDetailPanel.tsx
1690
1707
  var import_jsx_runtime26 = require("react/jsx-runtime");
1708
+ var RELATIVE_ITEM_COUNT = 1;
1709
+ function toNode(genre, parentId) {
1710
+ return {
1711
+ id: genre.uuid,
1712
+ parentId,
1713
+ name: genre.name,
1714
+ itemCount: RELATIVE_ITEM_COUNT,
1715
+ side: genre.side ?? void 0,
1716
+ wikidataId: genre.wikidataId
1717
+ };
1718
+ }
1719
+ function useGenreDetailContent({
1720
+ genreUuid,
1721
+ scope,
1722
+ getBackendBaseUrl,
1723
+ onSelectGenre,
1724
+ extras,
1725
+ band,
1726
+ playback: hostPlayback,
1727
+ trailing,
1728
+ genrePlaylist: hostPlaylist,
1729
+ expanded
1730
+ }) {
1731
+ const { data: detail, isPending, isError } = (0, import_genre_tree19.useFetchGenreDetail)(genreUuid, scope, getBackendBaseUrl);
1732
+ const defaultPlayback = useGenrePlaylistPlayback({ scope, genrePlaylists: [], artist: extras?.artist });
1733
+ const playback = hostPlayback ?? defaultPlayback;
1734
+ const view = (0, import_react14.useMemo)(() => {
1735
+ if (!detail) return null;
1736
+ const rootColor = (0, import_genre_tree_view4.getGenreTreeColor)(detail.root.wikidataId);
1737
+ const relative = (genre, parentId) => ({
1738
+ node: toNode(genre, parentId),
1739
+ fill: (0, import_genre_tree_view4.tintSurface)(rootColor),
1740
+ textColor: import_genre_tree_view4.TEXT_COLOR
1741
+ });
1742
+ const ancestors2 = [...detail.ascendants].sort((a, b) => b.degree - a.degree);
1743
+ return {
1744
+ node: {
1745
+ id: detail.uuid,
1746
+ parentId: detail.parent?.uuid ?? null,
1747
+ name: detail.name,
1748
+ itemCount: scope === "reference" ? (0, import_genre_tree19.genreMusicCount)(detail.criteriaPlaylist ? import_genre_tree19.GenrePlaylistSummarySchema.parse(detail.criteriaPlaylist) : import_genre_tree19.GenreCatalogueCountsSchema.parse(detail.catalogue)) : detail.tracksCount,
1749
+ wikidataId: detail.wikidataId
1750
+ },
1751
+ fill: rootColor,
1752
+ parentNode: detail.parent ? relative(detail.parent, null) : null,
1753
+ childNodes: detail.children.map((child) => relative(child, detail.uuid)),
1754
+ ancestorNodes: ancestors2.map(
1755
+ ({ ascendant }, index) => relative(ascendant, index === 0 ? null : ancestors2[index - 1].ascendant.uuid)
1756
+ )
1757
+ };
1758
+ }, [detail, scope]);
1759
+ if (isError) return {
1760
+ title: "Genre unavailable",
1761
+ content: /* @__PURE__ */ (0, import_jsx_runtime26.jsx)("p", { role: "alert", className: "p-4 text-sm text-[var(--color-text)]", children: "This genre could not be loaded." })
1762
+ };
1763
+ if (isPending || !detail || !view) return {
1764
+ title: "Loading genre\u2026",
1765
+ content: /* @__PURE__ */ (0, import_jsx_runtime26.jsxs)("div", { className: "flex flex-col gap-3 p-4", "aria-busy": "true", "data-testid": "genre-detail-panel-skeleton", children: [
1766
+ /* @__PURE__ */ (0, import_jsx_runtime26.jsx)(import_ui7.Skeleton, { className: "h-6 w-2/3" }),
1767
+ /* @__PURE__ */ (0, import_jsx_runtime26.jsx)(import_ui7.Skeleton, { className: "h-4 w-full" }),
1768
+ /* @__PURE__ */ (0, import_jsx_runtime26.jsx)(import_ui7.Skeleton, { className: "h-4 w-3/4" })
1769
+ ] })
1770
+ };
1771
+ const selectGenre = (nodeId) => onSelectGenre(import_genre_tree19.GenreUuidSchema.parse(nodeId));
1772
+ const family = /* @__PURE__ */ (0, import_jsx_runtime26.jsx)(
1773
+ import_genre_tree_view4.GenreNodeFamily,
1774
+ {
1775
+ node: view.node,
1776
+ fill: view.fill,
1777
+ textColor: import_genre_tree_view4.ACCENT_TEXT_COLOR,
1778
+ parentNode: view.parentNode,
1779
+ childNodes: view.childNodes,
1780
+ onSelectNode: selectGenre
1781
+ }
1782
+ );
1783
+ const ancestors = /* @__PURE__ */ (0, import_jsx_runtime26.jsx)(import_genre_tree_view4.GenreNodeAncestors, { ancestorNodes: view.ancestorNodes, onSelectNode: selectGenre });
1784
+ return {
1785
+ title: detail.name,
1786
+ headerAccent: { background: view.fill, color: import_genre_tree_view4.ACCENT_TEXT_COLOR },
1787
+ aside: expanded ? ancestors : void 0,
1788
+ content: /* @__PURE__ */ (0, import_jsx_runtime26.jsxs)(import_jsx_runtime26.Fragment, { children: [
1789
+ /* @__PURE__ */ (0, import_jsx_runtime26.jsx)(
1790
+ GenreInfoPanelBody,
1791
+ {
1792
+ family: /* @__PURE__ */ (0, import_jsx_runtime26.jsxs)(import_jsx_runtime26.Fragment, { children: [
1793
+ family,
1794
+ !expanded && ancestors
1795
+ ] }),
1796
+ genreUuid: detail.uuid,
1797
+ genrePlaylist: hostPlaylist ?? (detail.criteriaPlaylist === null ? null : scope === "reference" ? { ...import_genre_tree19.GenrePlaylistSummarySchema.parse(detail.criteriaPlaylist), criteria: import_genre_tree19.CriteriaMinimumSchema.parse(detail) } : { ...detail.criteriaPlaylist, criteria: import_genre_tree19.CriteriaMinimumSchema.parse(detail), tracksCount: detail.tracksCount }),
1798
+ catalogue: detail.catalogue,
1799
+ countsContext: !hostPlaylist && extras?.artist ? "all artists" : void 0,
1800
+ wikidataId: detail.wikidataId,
1801
+ scope,
1802
+ getBackendBaseUrl,
1803
+ playback,
1804
+ band: { node: view.node, ...band, shareTarget: band?.shareTarget?.kind === "genre" ? { ...band.shareTarget, name: detail.name } : band?.shareTarget },
1805
+ extras
1806
+ }
1807
+ ),
1808
+ trailing
1809
+ ] })
1810
+ };
1811
+ }
1812
+ function GenreDetailPanel({
1813
+ genreUuid,
1814
+ scope,
1815
+ getBackendBaseUrl,
1816
+ onClose,
1817
+ onSelectGenre,
1818
+ onZoomIn,
1819
+ shareUrl,
1820
+ shareTarget,
1821
+ collapsed,
1822
+ expanded,
1823
+ onExpandedChange,
1824
+ edgeTab,
1825
+ extras,
1826
+ headerClassName
1827
+ }) {
1828
+ const panel = useGenreDetailContent({
1829
+ genreUuid,
1830
+ scope,
1831
+ getBackendBaseUrl,
1832
+ onSelectGenre,
1833
+ extras,
1834
+ expanded,
1835
+ band: { onZoomIn, shareUrl, shareTarget: shareUrl ? shareTarget : void 0 }
1836
+ });
1837
+ return /* @__PURE__ */ (0, import_jsx_runtime26.jsx)(
1838
+ import_ui7.DetailPanel,
1839
+ {
1840
+ side: "left",
1841
+ title: panel.title,
1842
+ headerAccent: panel.headerAccent,
1843
+ onClose,
1844
+ collapsed,
1845
+ expanded,
1846
+ onExpandedChange,
1847
+ edgeTab,
1848
+ aside: panel.aside,
1849
+ headerClassName,
1850
+ headerActions: onExpandedChange ? /* @__PURE__ */ (0, import_jsx_runtime26.jsx)(import_ui7.IconButton, { "aria-label": expanded ? "Shrink panel" : "Extend panel", onClick: () => onExpandedChange(!expanded), children: expanded ? /* @__PURE__ */ (0, import_jsx_runtime26.jsx)(import_lucide_react8.Minimize2, { "aria-hidden": "true", className: "h-4 w-4" }) : /* @__PURE__ */ (0, import_jsx_runtime26.jsx)(import_lucide_react8.Maximize2, { "aria-hidden": "true", className: "h-4 w-4" }) }) : void 0,
1851
+ children: panel.content
1852
+ }
1853
+ );
1854
+ }
1855
+
1856
+ // src/genre-tree/ChangeParentPopup.tsx
1857
+ var import_react15 = require("react");
1858
+ var import_genre_tree20 = require("@behindthemusictree/app-kit/genre-tree");
1859
+ var import_jsx_runtime27 = require("react/jsx-runtime");
1691
1860
  function ChangeParentPopup({
1692
1861
  genrePlaylist,
1693
1862
  genrePlaylists,
@@ -1696,9 +1865,9 @@ function ChangeParentPopup({
1696
1865
  onClose,
1697
1866
  onParentChanged
1698
1867
  }) {
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),
1868
+ const { mutate, isPending, formErrors } = (0, import_genre_tree20.useUpdateGenre)(scope, getBackendBaseUrl);
1869
+ const candidates = (0, import_react15.useMemo)(
1870
+ () => (0, import_genre_tree20.getReparentCandidates)(genrePlaylists, genrePlaylist.uuid),
1702
1871
  [genrePlaylists, genrePlaylist.uuid]
1703
1872
  );
1704
1873
  const handleSelect = (newParent) => {
@@ -1715,19 +1884,19 @@ function ChangeParentPopup({
1715
1884
  }
1716
1885
  );
1717
1886
  };
1718
- (0, import_react14.useEffect)(() => {
1887
+ (0, import_react15.useEffect)(() => {
1719
1888
  const handleKeyDown = (event) => {
1720
1889
  if (event.key === "Escape") onClose();
1721
1890
  };
1722
1891
  window.addEventListener("keydown", handleKeyDown);
1723
1892
  return () => window.removeEventListener("keydown", handleKeyDown);
1724
1893
  }, [onClose]);
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: [
1894
+ return /* @__PURE__ */ (0, import_jsx_runtime27.jsxs)(BasePopup_default, { title: `Change parent of ${genrePlaylist.name}`, onClose, showCancelButton: true, fitContent: true, children: [
1895
+ /* @__PURE__ */ (0, import_jsx_runtime27.jsxs)("p", { className: "mb-3 text-sm text-[var(--color-text-muted)]", children: [
1727
1896
  "Current parent: ",
1728
1897
  genrePlaylist.parent?.name ?? "none"
1729
1898
  ] }),
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)(
1899
+ /* @__PURE__ */ (0, import_jsx_runtime27.jsx)("div", { "aria-busy": isPending, className: isPending ? "pointer-events-none opacity-60" : void 0, children: /* @__PURE__ */ (0, import_jsx_runtime27.jsx)(
1731
1900
  GenreSearch,
1732
1901
  {
1733
1902
  genrePlaylists: candidates,
@@ -1735,24 +1904,24 @@ function ChangeParentPopup({
1735
1904
  placeholder: "Search a new parent\u2026"
1736
1905
  }
1737
1906
  ) }),
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(" ") })
1907
+ formErrors.length > 0 && /* @__PURE__ */ (0, import_jsx_runtime27.jsx)("p", { role: "alert", className: "mt-3 text-sm text-[var(--color-red-500)]", children: formErrors.map(({ message }) => message).join(" ") })
1739
1908
  ] });
1740
1909
  }
1741
1910
 
1742
1911
  // src/genre-tree/artist/useArtistPanel.tsx
1743
- var import_ui12 = require("@behindthemusictree/ui");
1744
- var import_genre_tree24 = require("@behindthemusictree/app-kit/genre-tree");
1912
+ var import_ui13 = require("@behindthemusictree/ui");
1913
+ var import_genre_tree25 = require("@behindthemusictree/app-kit/genre-tree");
1745
1914
 
1746
1915
  // src/genre-tree/artist/ArtistHeader.tsx
1747
- var import_ui8 = require("@behindthemusictree/ui");
1916
+ var import_ui9 = require("@behindthemusictree/ui");
1748
1917
 
1749
1918
  // src/genre-tree/artist/ArtistLinks.tsx
1750
- var import_lucide_react8 = require("lucide-react");
1919
+ var import_lucide_react9 = require("lucide-react");
1751
1920
  var import_si3 = require("react-icons/si");
1752
- var import_ui7 = require("@behindthemusictree/ui");
1753
- var import_jsx_runtime27 = require("react/jsx-runtime");
1921
+ var import_ui8 = require("@behindthemusictree/ui");
1922
+ var import_jsx_runtime28 = require("react/jsx-runtime");
1754
1923
  var LINK_KINDS = {
1755
- official: { label: "Official website", Icon: import_lucide_react8.Globe },
1924
+ official: { label: "Official website", Icon: import_lucide_react9.Globe },
1756
1925
  wikipedia: { label: "Wikipedia", Icon: import_si3.SiWikipedia },
1757
1926
  wikidata: { label: "Wikidata", Icon: import_si3.SiWikidata },
1758
1927
  spotify: { label: "Spotify", Icon: import_si3.SiSpotify },
@@ -1760,7 +1929,7 @@ var LINK_KINDS = {
1760
1929
  bandcamp: { label: "Bandcamp", Icon: import_si3.SiBandcamp },
1761
1930
  soundcloud: { label: "SoundCloud", Icon: import_si3.SiSoundcloud },
1762
1931
  discogs: { label: "Discogs", Icon: import_si3.SiDiscogs },
1763
- social: { label: "Social", Icon: import_lucide_react8.Share2 }
1932
+ social: { label: "Social", Icon: import_lucide_react9.Share2 }
1764
1933
  };
1765
1934
  function socialLabel(url) {
1766
1935
  try {
@@ -1787,7 +1956,7 @@ function artistLinks(artist) {
1787
1956
  return links;
1788
1957
  }
1789
1958
  function ArtistLinks({ links }) {
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)) });
1959
+ return /* @__PURE__ */ (0, import_jsx_runtime28.jsx)("ul", { className: "flex flex-wrap gap-1.5", children: links.map(({ key, url, Icon, label }) => /* @__PURE__ */ (0, import_jsx_runtime28.jsx)("li", { children: /* @__PURE__ */ (0, import_jsx_runtime28.jsx)(import_ui8.Chip, { href: url, icon: /* @__PURE__ */ (0, import_jsx_runtime28.jsx)(Icon, { className: "h-3.5 w-3.5 shrink-0", "aria-hidden": "true" }), children: label }) }, key)) });
1791
1960
  }
1792
1961
 
1793
1962
  // src/genre-tree/artist/formatYears.ts
@@ -1798,7 +1967,7 @@ function formatYears(beginYear, endYear, ended) {
1798
1967
  }
1799
1968
 
1800
1969
  // src/genre-tree/artist/ArtistHeader.tsx
1801
- var import_jsx_runtime28 = require("react/jsx-runtime");
1970
+ var import_jsx_runtime29 = require("react/jsx-runtime");
1802
1971
  var countryNames = new Intl.DisplayNames(["en"], { type: "region" });
1803
1972
  function ArtistHeader({ artist }) {
1804
1973
  const fields = [];
@@ -1808,35 +1977,35 @@ function ArtistHeader({ artist }) {
1808
1977
  const years = formatYears(artist.beginYear, artist.endYear, artist.ended);
1809
1978
  if (years) fields.push({ label: "Active", value: years });
1810
1979
  const links = artistLinks(artist);
1811
- if (links.length > 0) fields.push({ label: "Links", value: /* @__PURE__ */ (0, import_jsx_runtime28.jsx)(ArtistLinks, { links }) });
1980
+ if (links.length > 0) fields.push({ label: "Links", value: /* @__PURE__ */ (0, import_jsx_runtime29.jsx)(ArtistLinks, { links }) });
1812
1981
  if (fields.length === 0) return null;
1813
- return /* @__PURE__ */ (0, import_jsx_runtime28.jsx)(import_ui8.FieldList, { items: fields });
1982
+ return /* @__PURE__ */ (0, import_jsx_runtime29.jsx)(import_ui9.FieldList, { items: fields });
1814
1983
  }
1815
1984
 
1816
1985
  // src/genre-tree/artist/ArtistPhoto.tsx
1817
- var import_jsx_runtime29 = require("react/jsx-runtime");
1986
+ var import_jsx_runtime30 = require("react/jsx-runtime");
1818
1987
  function ExternalLink({ href, children }) {
1819
- return /* @__PURE__ */ (0, import_jsx_runtime29.jsx)("a", { href, target: "_blank", rel: "noopener noreferrer", className: "underline-offset-2 hover:underline", children });
1988
+ return /* @__PURE__ */ (0, import_jsx_runtime30.jsx)("a", { href, target: "_blank", rel: "noopener noreferrer", className: "underline-offset-2 hover:underline", children });
1820
1989
  }
1821
1990
  function ArtistPhoto({ image, name }) {
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: [
1991
+ return /* @__PURE__ */ (0, import_jsx_runtime30.jsxs)("figure", { className: "flex flex-col gap-1.5 px-6 py-2.5", children: [
1992
+ /* @__PURE__ */ (0, import_jsx_runtime30.jsx)("img", { src: image.url, alt: name, className: "h-64 w-full object-contain bg-[var(--color-surface-muted)] rounded" }),
1993
+ /* @__PURE__ */ (0, import_jsx_runtime30.jsxs)("figcaption", { className: "text-xs text-[var(--color-text-muted)]", children: [
1825
1994
  "Photo: ",
1826
- /* @__PURE__ */ (0, import_jsx_runtime29.jsx)(ExternalLink, { href: image.pageUrl, children: image.author ?? "Wikimedia Commons" }),
1995
+ /* @__PURE__ */ (0, import_jsx_runtime30.jsx)(ExternalLink, { href: image.pageUrl, children: image.author ?? "Wikimedia Commons" }),
1827
1996
  ",",
1828
1997
  " ",
1829
- image.licenseUrl ? /* @__PURE__ */ (0, import_jsx_runtime29.jsx)(ExternalLink, { href: image.licenseUrl, children: image.license }) : image.license
1998
+ image.licenseUrl ? /* @__PURE__ */ (0, import_jsx_runtime30.jsx)(ExternalLink, { href: image.licenseUrl, children: image.license }) : image.license
1830
1999
  ] })
1831
2000
  ] });
1832
2001
  }
1833
2002
 
1834
2003
  // src/genre-tree/artist/ArtistMembers.tsx
1835
- var import_ui9 = require("@behindthemusictree/ui");
1836
- var import_jsx_runtime30 = require("react/jsx-runtime");
2004
+ var import_ui10 = require("@behindthemusictree/ui");
2005
+ var import_jsx_runtime31 = require("react/jsx-runtime");
1837
2006
  function MembershipList({ title, memberships, renderArtistLink }) {
1838
2007
  if (memberships.length === 0) return null;
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) => {
2008
+ return /* @__PURE__ */ (0, import_jsx_runtime31.jsx)(import_ui10.PanelGroup, { title, children: /* @__PURE__ */ (0, import_jsx_runtime31.jsx)("ul", { children: memberships.map((membership, index) => {
1840
2009
  const details = [
1841
2010
  formatYears(membership.beginYear, membership.endYear, membership.ended),
1842
2011
  ...membership.roles
@@ -1844,9 +2013,9 @@ function MembershipList({ title, memberships, renderArtistLink }) {
1844
2013
  const detailsText = details.filter(Boolean).join(" \xB7 ");
1845
2014
  return (
1846
2015
  // A member can have several periods in the same group.
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: [
2016
+ /* @__PURE__ */ (0, import_jsx_runtime31.jsxs)("li", { className: "truncate py-0.5", title: detailsText || void 0, children: [
2017
+ /* @__PURE__ */ (0, import_jsx_runtime31.jsx)("span", { className: "font-medium text-[var(--color-text)]", children: renderArtistLink(membership.artist) }),
2018
+ detailsText && /* @__PURE__ */ (0, import_jsx_runtime31.jsxs)("span", { className: "text-[var(--color-text-muted)]", children: [
1850
2019
  " \u2014 ",
1851
2020
  detailsText
1852
2021
  ] })
@@ -1857,9 +2026,9 @@ function MembershipList({ title, memberships, renderArtistLink }) {
1857
2026
  function ArtistMembers({ title, memberships, renderArtistLink }) {
1858
2027
  if (memberships.length === 0) return null;
1859
2028
  const isCurrent = (membership) => !membership.ended && membership.endYear === null;
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)(
2029
+ return /* @__PURE__ */ (0, import_jsx_runtime31.jsxs)(import_ui10.PanelSection, { title, children: [
2030
+ /* @__PURE__ */ (0, import_jsx_runtime31.jsx)(MembershipList, { title: "Current", memberships: memberships.filter(isCurrent), renderArtistLink }),
2031
+ /* @__PURE__ */ (0, import_jsx_runtime31.jsx)(
1863
2032
  MembershipList,
1864
2033
  {
1865
2034
  title: "Past",
@@ -1871,8 +2040,8 @@ function ArtistMembers({ title, memberships, renderArtistLink }) {
1871
2040
  }
1872
2041
 
1873
2042
  // src/genre-tree/artist/ArtistDiscography.tsx
1874
- var import_ui10 = require("@behindthemusictree/ui");
1875
- var import_jsx_runtime31 = require("react/jsx-runtime");
2043
+ var import_ui11 = require("@behindthemusictree/ui");
2044
+ var import_jsx_runtime32 = require("react/jsx-runtime");
1876
2045
  var GROUPS = ["Albums", "EPs", "Singles", "Live", "Compilations", "Other"];
1877
2046
  var PRIMARY_GROUPS = { album: "Albums", ep: "EPs", single: "Singles" };
1878
2047
  function discographyGroup({ primaryType, secondaryTypes }) {
@@ -1892,34 +2061,34 @@ function byYear(a, b) {
1892
2061
  function ArtistDiscography({ albums }) {
1893
2062
  if (albums.length === 0) return null;
1894
2063
  const sorted = [...albums].sort(byYear);
1895
- return /* @__PURE__ */ (0, import_jsx_runtime31.jsx)(import_ui10.PanelSection, { title: "Discography", children: GROUPS.map((group) => {
2064
+ return /* @__PURE__ */ (0, import_jsx_runtime32.jsx)(import_ui11.PanelSection, { title: "Discography", children: GROUPS.map((group) => {
1896
2065
  const groupAlbums = sorted.filter((album) => discographyGroup(album) === group);
1897
2066
  if (groupAlbums.length === 0) return null;
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 })
2067
+ return /* @__PURE__ */ (0, import_jsx_runtime32.jsx)(import_ui11.PanelGroup, { title: group, children: /* @__PURE__ */ (0, import_jsx_runtime32.jsx)("ul", { children: groupAlbums.map((album) => /* @__PURE__ */ (0, import_jsx_runtime32.jsxs)("li", { className: "grid grid-cols-[2.5rem_minmax(0,1fr)] gap-2 py-0.5", children: [
2068
+ /* @__PURE__ */ (0, import_jsx_runtime32.jsx)("span", { className: "tabular-nums text-[var(--color-text-muted)]", children: album.year ?? "\u2014" }),
2069
+ /* @__PURE__ */ (0, import_jsx_runtime32.jsx)("span", { className: "truncate text-[var(--color-text)]", title: album.name, children: album.name })
1901
2070
  ] }, album.uuid)) }) }, group);
1902
2071
  }) });
1903
2072
  }
1904
2073
 
1905
2074
  // src/genre-tree/artist/ArtistSongs.tsx
1906
- var import_react15 = require("react");
2075
+ var import_react16 = require("react");
1907
2076
  var import_react_query2 = require("@tanstack/react-query");
1908
- var import_lucide_react9 = require("lucide-react");
1909
- var import_ui11 = require("@behindthemusictree/ui");
2077
+ var import_lucide_react10 = require("lucide-react");
2078
+ var import_ui12 = require("@behindthemusictree/ui");
1910
2079
  var import_player10 = require("@behindthemusictree/app-kit/player");
1911
- var import_genre_tree23 = require("@behindthemusictree/app-kit/genre-tree");
2080
+ var import_genre_tree24 = require("@behindthemusictree/app-kit/genre-tree");
1912
2081
  var import_transport7 = require("@behindthemusictree/app-kit/transport");
1913
2082
 
1914
2083
  // src/genre-tree/track-list-sidebar/TrackItem.tsx
1915
2084
  var import_player9 = require("@behindthemusictree/app-kit/player");
1916
- var import_genre_tree20 = require("@behindthemusictree/app-kit/genre-tree");
2085
+ var import_genre_tree21 = require("@behindthemusictree/app-kit/genre-tree");
1917
2086
 
1918
2087
  // src/genre-tree/TrackPositionPlayPause.tsx
1919
2088
  var import_fa3 = require("react-icons/fa");
1920
2089
  var import_player7 = require("@behindthemusictree/app-kit/player");
1921
2090
  var import_player8 = require("@behindthemusictree/app-kit/player");
1922
- var import_jsx_runtime32 = require("react/jsx-runtime");
2091
+ var import_jsx_runtime33 = require("react/jsx-runtime");
1923
2092
  function TrackPositionPlayPause({
1924
2093
  position,
1925
2094
  uuid,
@@ -1933,14 +2102,14 @@ function TrackPositionPlayPause({
1933
2102
  const isPlaying = isThisTrack && isConfirmedPlayable && playState === import_player8.PlayStates.PLAYING;
1934
2103
  const isPaused = isThisTrack && isConfirmedPlayable && playState === import_player8.PlayStates.PAUSED;
1935
2104
  const isCurrentTrackActive = isPlaying || isPaused;
1936
- return /* @__PURE__ */ (0, import_jsx_runtime32.jsxs)(
2105
+ return /* @__PURE__ */ (0, import_jsx_runtime33.jsxs)(
1937
2106
  "div",
1938
2107
  {
1939
2108
  className: "track-position-play-pause relative flex items-center justify-center gap-2 text-lg w-16 cursor-pointer",
1940
2109
  style: { minWidth: "4rem", maxWidth: "4rem" },
1941
2110
  onClick: handlePlayPauseClick,
1942
2111
  children: [
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)(
2112
+ /* @__PURE__ */ (0, import_jsx_runtime33.jsx)("div", { className: "track-position-play-pause__status group-hover:hidden flex items-center justify-center", children: isAwaitingPlayback ? /* @__PURE__ */ (0, import_jsx_runtime33.jsx)("span", { className: "track-position-play-pause__spinner", role: "status", "aria-label": "Loading track" }) : isCurrentTrackActive ? /* @__PURE__ */ (0, import_jsx_runtime33.jsxs)(
1944
2113
  "span",
1945
2114
  {
1946
2115
  className: "track-position-play-pause__bars",
@@ -1948,18 +2117,18 @@ function TrackPositionPlayPause({
1948
2117
  role: "img",
1949
2118
  "aria-label": isPlaying ? "Playing" : "Paused",
1950
2119
  children: [
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" })
2120
+ /* @__PURE__ */ (0, import_jsx_runtime33.jsx)("span", { className: "track-position-play-pause__bar" }),
2121
+ /* @__PURE__ */ (0, import_jsx_runtime33.jsx)("span", { className: "track-position-play-pause__bar" }),
2122
+ /* @__PURE__ */ (0, import_jsx_runtime33.jsx)("span", { className: "track-position-play-pause__bar" })
1954
2123
  ]
1955
2124
  }
1956
2125
  ) : position }),
1957
- /* @__PURE__ */ (0, import_jsx_runtime32.jsx)(
2126
+ /* @__PURE__ */ (0, import_jsx_runtime33.jsx)(
1958
2127
  "div",
1959
2128
  {
1960
2129
  className: `hidden group-hover:flex items-center justify-center ${isCurrentTrackActive ? "" : "absolute inset-0"}`,
1961
2130
  "aria-hidden": "true",
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, {})
2131
+ children: isAwaitingPlayback ? /* @__PURE__ */ (0, import_jsx_runtime33.jsx)("span", { className: "track-position-play-pause__spinner" }) : isPlaying ? /* @__PURE__ */ (0, import_jsx_runtime33.jsx)(import_fa3.FaPause, {}) : /* @__PURE__ */ (0, import_jsx_runtime33.jsx)(import_fa3.FaPlay, {})
1963
2132
  }
1964
2133
  )
1965
2134
  ]
@@ -1968,9 +2137,9 @@ function TrackPositionPlayPause({
1968
2137
  }
1969
2138
 
1970
2139
  // src/genre-tree/track-list-sidebar/TrackItem.tsx
1971
- var import_genre_tree21 = require("@behindthemusictree/app-kit/genre-tree");
1972
2140
  var import_genre_tree22 = require("@behindthemusictree/app-kit/genre-tree");
1973
- var import_jsx_runtime33 = require("react/jsx-runtime");
2141
+ var import_genre_tree23 = require("@behindthemusictree/app-kit/genre-tree");
2142
+ var import_jsx_runtime34 = require("react/jsx-runtime");
1974
2143
  function TrackItem({
1975
2144
  track,
1976
2145
  position,
@@ -1980,11 +2149,11 @@ function TrackItem({
1980
2149
  renderSongLink
1981
2150
  }) {
1982
2151
  const { handlePlayPauseAction, playerTrackObject, loadTrackForPlayer } = (0, import_player9.usePlayer)();
1983
- const { trackList, selectedTrack, toTrackAtPosition, runtimeUnplayableTrackIds } = (0, import_genre_tree20.useTrackList)();
2152
+ const { trackList, selectedTrack, toTrackAtPosition, runtimeUnplayableTrackIds } = (0, import_genre_tree21.useTrackList)();
1984
2153
  const scope = trackList?.origin?.scope ?? null;
1985
2154
  const isCurrentTrack = playerTrackObject?.track.id === track.uuid;
1986
2155
  const hasPlaybackError = isCurrentTrack && !!playerTrackObject?.loadError;
1987
- const isUnplayable = hasPlaybackError || runtimeUnplayableTrackIds?.has(track.uuid) === true || (0, import_genre_tree21.isSong)(track) && (0, import_genre_tree21.songUnplayableReason)(track) !== null;
2156
+ const isUnplayable = hasPlaybackError || runtimeUnplayableTrackIds?.has(track.uuid) === true || (0, import_genre_tree22.isSong)(track) && (0, import_genre_tree22.songUnplayableReason)(track) !== null;
1988
2157
  const handlePlayPauseClick = (event) => {
1989
2158
  event.stopPropagation();
1990
2159
  if (isCurrentTrack) {
@@ -1994,12 +2163,12 @@ function TrackItem({
1994
2163
  loadTrackForPlayer(track.uuid);
1995
2164
  }
1996
2165
  };
1997
- return /* @__PURE__ */ (0, import_jsx_runtime33.jsxs)(
2166
+ return /* @__PURE__ */ (0, import_jsx_runtime34.jsxs)(
1998
2167
  "div",
1999
2168
  {
2000
2169
  className: `track-item flex h-14 text-[var(--color-text)] ${isUnplayable ? "opacity-50" : "hover:bg-[var(--color-surface-muted)] group"}`,
2001
2170
  children: [
2002
- isUnplayable ? /* @__PURE__ */ (0, import_jsx_runtime33.jsx)(
2171
+ isUnplayable ? /* @__PURE__ */ (0, import_jsx_runtime34.jsx)(
2003
2172
  "div",
2004
2173
  {
2005
2174
  className: "flex items-center justify-center text-lg w-16",
@@ -2007,7 +2176,7 @@ function TrackItem({
2007
2176
  "aria-disabled": "true",
2008
2177
  children: position
2009
2178
  }
2010
- ) : /* @__PURE__ */ (0, import_jsx_runtime33.jsx)(
2179
+ ) : /* @__PURE__ */ (0, import_jsx_runtime34.jsx)(
2011
2180
  TrackPositionPlayPause,
2012
2181
  {
2013
2182
  position,
@@ -2016,12 +2185,12 @@ function TrackItem({
2016
2185
  isSelected: selectedTrack?.uuid === track.uuid
2017
2186
  }
2018
2187
  ),
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 }) }) : ""
2188
+ /* @__PURE__ */ (0, import_jsx_runtime34.jsxs)("div", { className: "title-artist-container flex flex-col items-start justify-center min-w-0 flex-1", children: [
2189
+ /* @__PURE__ */ (0, import_jsx_runtime34.jsx)("div", { className: "flex max-w-full items-center gap-2", children: /* @__PURE__ */ (0, import_jsx_runtime34.jsx)("div", { className: "title text-lg font-bold text-[var(--color-text)] max-w-full truncate", children: renderSongLink ? renderSongLink(track) : (0, import_genre_tree22.isSong)(track) ? /* @__PURE__ */ (0, import_jsx_runtime34.jsx)(MusicBrainzRecordingLink, { song: track, className: "hover:underline focus-visible:underline", children: track.title }) : track.title }) }),
2190
+ track.artists && track.artists.length > 0 ? /* @__PURE__ */ (0, import_jsx_runtime34.jsx)("div", { className: "artist text-base max-w-full truncate text-[var(--color-text-muted)]", children: /* @__PURE__ */ (0, import_jsx_runtime34.jsx)(import_genre_tree23.ArtistNames, { artists: track.artists, renderArtistLink }) }) : ""
2022
2191
  ] }),
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)(
2192
+ /* @__PURE__ */ (0, import_jsx_runtime34.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_runtime34.jsx)("div", { className: "genre-name max-w-full whitespace-normal break-words p-1 text-xs font-bold", children: track.genre.name }) : "" }),
2193
+ renderDuration && /* @__PURE__ */ (0, import_jsx_runtime34.jsx)(
2025
2194
  "div",
2026
2195
  {
2027
2196
  className: "duration flex text-base w-16 items-center justify-center",
@@ -2029,28 +2198,28 @@ function TrackItem({
2029
2198
  children: renderDuration(track)
2030
2199
  }
2031
2200
  ),
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) })
2201
+ renderActions && /* @__PURE__ */ (0, import_jsx_runtime34.jsx)("div", { className: "edit flex text-base w-6 items-center justify-center mr-2 cursor-pointer", children: renderActions(track) })
2033
2202
  ]
2034
2203
  }
2035
2204
  );
2036
2205
  }
2037
2206
 
2038
2207
  // src/genre-tree/artist/ArtistSongs.tsx
2039
- var import_jsx_runtime34 = require("react/jsx-runtime");
2040
- var SongPageSchema2 = (0, import_transport7.PaginatedResponseSchema)(import_genre_tree23.SongDetailedSchema);
2208
+ var import_jsx_runtime35 = require("react/jsx-runtime");
2209
+ var SongPageSchema2 = (0, import_transport7.PaginatedResponseSchema)(import_genre_tree24.SongDetailedSchema);
2041
2210
  var SORTS = [
2042
2211
  { ordering: "title", label: "Title" },
2043
2212
  { ordering: "-albumYear", label: "Year" }
2044
2213
  ];
2045
2214
  function ArtistSongs({ artist, getBackendBaseUrl, renderArtistLink, renderSongLink }) {
2046
- const [page, setPage] = (0, import_react15.useState)(1);
2047
- const [ordering, setOrdering] = (0, import_react15.useState)("title");
2215
+ const [page, setPage] = (0, import_react16.useState)(1);
2216
+ const [ordering, setOrdering] = (0, import_react16.useState)("title");
2048
2217
  const { fetch } = (0, import_transport7.useFetchWrapper)(getBackendBaseUrl);
2049
- const { playNewTrackListFromArtist } = (0, import_genre_tree23.useTrackList)();
2218
+ const { playNewTrackListFromArtist } = (0, import_genre_tree24.useTrackList)();
2050
2219
  const { playerTrackObject } = (0, import_player10.usePlayer)();
2051
2220
  const { data, isPending, isError } = (0, import_transport7.useQueryWithParse)({
2052
2221
  queryKey: ["referenceSongs", "artist", artist.uuid, ordering, page],
2053
- queryFn: () => fetch(import_genre_tree23.libraryEndpoints.reference.songs.list(), true, false, void 0, {
2222
+ queryFn: () => fetch(import_genre_tree24.libraryEndpoints.reference.songs.list(), true, false, void 0, {
2054
2223
  artist: artist.uuid,
2055
2224
  ordering,
2056
2225
  page
@@ -2065,25 +2234,25 @@ function ArtistSongs({ artist, getBackendBaseUrl, renderArtistLink, renderSongLi
2065
2234
  });
2066
2235
  const handleRowClickCapture = (event, song) => {
2067
2236
  if (!(event.target instanceof Element) || !event.target.closest(".track-position-play-pause")) return;
2068
- if (playerTrackObject?.track.id === song.uuid || (0, import_genre_tree23.songUnplayableReason)(song) !== null) return;
2237
+ if (playerTrackObject?.track.id === song.uuid || (0, import_genre_tree24.songUnplayableReason)(song) !== null) return;
2069
2238
  event.stopPropagation();
2070
2239
  void play(song.uuid);
2071
2240
  };
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)(
2241
+ return /* @__PURE__ */ (0, import_jsx_runtime35.jsx)(import_ui12.PanelSection, { title: "Songs", children: isPending ? /* @__PURE__ */ (0, import_jsx_runtime35.jsx)(import_ui12.Skeleton, { className: "h-14", "aria-busy": "true" }) : isError ? /* @__PURE__ */ (0, import_jsx_runtime35.jsx)("p", { className: "text-red-600", children: "Could not load songs." }) : data.results.length === 0 ? /* @__PURE__ */ (0, import_jsx_runtime35.jsx)("p", { className: "text-[var(--color-text-muted)]", children: "No songs yet." }) : /* @__PURE__ */ (0, import_jsx_runtime35.jsxs)(import_jsx_runtime35.Fragment, { children: [
2242
+ /* @__PURE__ */ (0, import_jsx_runtime35.jsxs)("div", { className: "flex flex-wrap items-center justify-between gap-2 mb-2", children: [
2243
+ /* @__PURE__ */ (0, import_jsx_runtime35.jsxs)(
2075
2244
  "button",
2076
2245
  {
2077
2246
  type: "button",
2078
2247
  onClick: () => void play(),
2079
2248
  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",
2080
2249
  children: [
2081
- /* @__PURE__ */ (0, import_jsx_runtime34.jsx)(import_lucide_react9.Play, { className: "h-3.5 w-3.5", "aria-hidden": "true" }),
2250
+ /* @__PURE__ */ (0, import_jsx_runtime35.jsx)(import_lucide_react10.Play, { className: "h-3.5 w-3.5", "aria-hidden": "true" }),
2082
2251
  "Play all"
2083
2252
  ]
2084
2253
  }
2085
2254
  ),
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)(
2255
+ /* @__PURE__ */ (0, import_jsx_runtime35.jsx)("div", { role: "group", "aria-label": "Sort songs by", className: "flex gap-1 text-xs", children: SORTS.map((sort) => /* @__PURE__ */ (0, import_jsx_runtime35.jsx)(
2087
2256
  "button",
2088
2257
  {
2089
2258
  type: "button",
@@ -2098,7 +2267,7 @@ function ArtistSongs({ artist, getBackendBaseUrl, renderArtistLink, renderSongLi
2098
2267
  sort.ordering
2099
2268
  )) })
2100
2269
  ] }),
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)(
2270
+ /* @__PURE__ */ (0, import_jsx_runtime35.jsx)("ul", { className: "overflow-hidden mb-2 bg-zinc-950 rounded-lg", children: data.results.map((song, index) => /* @__PURE__ */ (0, import_jsx_runtime35.jsx)("li", { onClickCapture: (event) => handleRowClickCapture(event, song), children: /* @__PURE__ */ (0, import_jsx_runtime35.jsx)(
2102
2271
  TrackItem,
2103
2272
  {
2104
2273
  track: song,
@@ -2107,12 +2276,12 @@ function ArtistSongs({ artist, getBackendBaseUrl, renderArtistLink, renderSongLi
2107
2276
  renderSongLink
2108
2277
  }
2109
2278
  ) }, song.uuid)) }),
2110
- /* @__PURE__ */ (0, import_jsx_runtime34.jsx)(import_ui11.Pagination, { currentPage: page, totalPages: data.totalPages, onPageChange: setPage })
2279
+ /* @__PURE__ */ (0, import_jsx_runtime35.jsx)(import_ui12.Pagination, { currentPage: page, totalPages: data.totalPages, onPageChange: setPage })
2111
2280
  ] }) });
2112
2281
  }
2113
2282
 
2114
2283
  // src/genre-tree/artist/useArtistPanel.tsx
2115
- var import_jsx_runtime35 = require("react/jsx-runtime");
2284
+ var import_jsx_runtime36 = require("react/jsx-runtime");
2116
2285
  function useArtistPanel({
2117
2286
  uuid,
2118
2287
  scope,
@@ -2124,7 +2293,7 @@ function useArtistPanel({
2124
2293
  onExpandedChange,
2125
2294
  onClose
2126
2295
  }) {
2127
- const result = (0, import_genre_tree24.useArtist)(uuid, getBackendBaseUrl);
2296
+ const result = (0, import_genre_tree25.useArtist)(uuid, getBackendBaseUrl);
2128
2297
  const panel = { expanded, onExpandedChange, onClose };
2129
2298
  switch (result.status) {
2130
2299
  case "idle":
@@ -2133,26 +2302,26 @@ function useArtistPanel({
2133
2302
  return {
2134
2303
  ...panel,
2135
2304
  title: "Artist",
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" })
2305
+ content: /* @__PURE__ */ (0, import_jsx_runtime36.jsxs)("div", { "aria-busy": "true", className: "flex flex-col gap-2.5 px-6 py-2.5", children: [
2306
+ /* @__PURE__ */ (0, import_jsx_runtime36.jsx)(import_ui13.Skeleton, { className: "h-3 w-16" }),
2307
+ /* @__PURE__ */ (0, import_jsx_runtime36.jsx)(import_ui13.Skeleton, { className: "h-4 w-32" }),
2308
+ /* @__PURE__ */ (0, import_jsx_runtime36.jsx)(import_ui13.Skeleton, { className: "h-3 w-16" }),
2309
+ /* @__PURE__ */ (0, import_jsx_runtime36.jsx)(import_ui13.Skeleton, { className: "h-4 w-24" }),
2310
+ /* @__PURE__ */ (0, import_jsx_runtime36.jsx)(import_ui13.Skeleton, { className: "h-16 mt-2.5" }),
2311
+ /* @__PURE__ */ (0, import_jsx_runtime36.jsx)(import_ui13.Skeleton, { className: "h-16" })
2143
2312
  ] })
2144
2313
  };
2145
2314
  case "not-found":
2146
2315
  return {
2147
2316
  ...panel,
2148
2317
  title: "Artist not found",
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." })
2318
+ content: /* @__PURE__ */ (0, import_jsx_runtime36.jsx)("p", { className: "px-6 py-2.5 text-[var(--color-text-muted)]", children: "This artist does not exist." })
2150
2319
  };
2151
2320
  case "error":
2152
2321
  return {
2153
2322
  ...panel,
2154
2323
  title: "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." })
2324
+ content: /* @__PURE__ */ (0, import_jsx_runtime36.jsx)("p", { className: "px-6 py-2.5 text-red-600", children: "Could not load this artist." })
2156
2325
  };
2157
2326
  case "success": {
2158
2327
  const { artist } = result;
@@ -2161,13 +2330,13 @@ function useArtistPanel({
2161
2330
  return {
2162
2331
  ...panel,
2163
2332
  title: artist.name,
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)(
2333
+ content: /* @__PURE__ */ (0, import_jsx_runtime36.jsxs)(import_jsx_runtime36.Fragment, { children: [
2334
+ shareUrl && /* @__PURE__ */ (0, import_jsx_runtime36.jsx)("div", { className: "genre-action-band border-b border-[var(--color-border)]", children: /* @__PURE__ */ (0, import_jsx_runtime36.jsx)("div", { className: "flex flex-wrap gap-1 px-3.5 py-3", children: shareTarget && /* @__PURE__ */ (0, import_jsx_runtime36.jsx)(ShareActionButton, { target: shareTarget, url: shareUrl }) }) }),
2335
+ artist.image && /* @__PURE__ */ (0, import_jsx_runtime36.jsx)(ArtistPhoto, { image: artist.image, name: artist.name }),
2336
+ /* @__PURE__ */ (0, import_jsx_runtime36.jsx)(ArtistHeader, { artist }),
2337
+ /* @__PURE__ */ (0, import_jsx_runtime36.jsx)(ArtistMembers, { title: "Members", memberships: artist.members, renderArtistLink }),
2338
+ /* @__PURE__ */ (0, import_jsx_runtime36.jsx)(ArtistMembers, { title: "Member of", memberships: artist.memberOf, renderArtistLink }),
2339
+ artist.tracksCount > 0 && /* @__PURE__ */ (0, import_jsx_runtime36.jsx)(
2171
2340
  ArtistSongs,
2172
2341
  {
2173
2342
  artist: { uuid: artist.uuid, name: artist.name },
@@ -2177,56 +2346,56 @@ function useArtistPanel({
2177
2346
  }
2178
2347
  )
2179
2348
  ] }),
2180
- extraContent: artist.albums.length > 0 ? /* @__PURE__ */ (0, import_jsx_runtime35.jsx)(ArtistDiscography, { albums: artist.albums }) : void 0
2349
+ extraContent: artist.albums.length > 0 ? /* @__PURE__ */ (0, import_jsx_runtime36.jsx)(ArtistDiscography, { albums: artist.albums }) : void 0
2181
2350
  };
2182
2351
  }
2183
2352
  }
2184
2353
  }
2185
2354
 
2186
2355
  // src/genre-tree/song/useSongPanel.tsx
2187
- var import_ui14 = require("@behindthemusictree/ui");
2188
- var import_genre_tree26 = require("@behindthemusictree/app-kit/genre-tree");
2356
+ var import_ui15 = require("@behindthemusictree/ui");
2357
+ var import_genre_tree27 = require("@behindthemusictree/app-kit/genre-tree");
2189
2358
 
2190
2359
  // src/genre-tree/song/SongPanelContent.tsx
2191
- var import_react16 = require("react");
2192
- var import_lucide_react10 = require("lucide-react");
2193
- var import_ui13 = require("@behindthemusictree/ui");
2360
+ var import_react17 = require("react");
2361
+ var import_lucide_react11 = require("lucide-react");
2362
+ var import_ui14 = require("@behindthemusictree/ui");
2194
2363
  var import_player11 = require("@behindthemusictree/app-kit/player");
2195
- var import_genre_tree25 = require("@behindthemusictree/app-kit/genre-tree");
2196
- var import_jsx_runtime36 = require("react/jsx-runtime");
2364
+ var import_genre_tree26 = require("@behindthemusictree/app-kit/genre-tree");
2365
+ var import_jsx_runtime37 = require("react/jsx-runtime");
2197
2366
  var numberFormat = new Intl.NumberFormat("en");
2198
2367
  var dateFormat = new Intl.DateTimeFormat("en", { dateStyle: "medium" });
2199
2368
  var externalLink = "text-[var(--color-primary)] hover:underline focus-visible:underline";
2200
2369
  function SongHistory({ uuid, getBackendBaseUrl }) {
2201
- const [open, setOpen] = (0, import_react16.useState)(false);
2202
- const history = (0, import_genre_tree25.useSongHistory)(uuid, getBackendBaseUrl, open);
2370
+ const [open, setOpen] = (0, import_react17.useState)(false);
2371
+ const history = (0, import_genre_tree26.useSongHistory)(uuid, getBackendBaseUrl, open);
2203
2372
  let body = null;
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." });
2373
+ if (history.status === "loading") body = /* @__PURE__ */ (0, import_jsx_runtime37.jsx)(import_ui14.Skeleton, { className: "h-10", "aria-busy": "true" });
2374
+ else if (history.status === "error") body = /* @__PURE__ */ (0, import_jsx_runtime37.jsx)("p", { className: "text-red-600", children: "Could not load the history." });
2206
2375
  else if (history.status === "success")
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: [
2376
+ body = history.entries.length === 0 ? /* @__PURE__ */ (0, import_jsx_runtime37.jsx)("p", { className: "text-[var(--color-text-muted)]", children: "No changes yet." }) : /* @__PURE__ */ (0, import_jsx_runtime37.jsx)("ul", { className: "divide-y divide-[var(--color-border)]", children: history.entries.map((entry) => /* @__PURE__ */ (0, import_jsx_runtime37.jsxs)("li", { className: "py-1.5 text-sm", children: [
2377
+ /* @__PURE__ */ (0, import_jsx_runtime37.jsx)("span", { className: "font-medium", children: entry.action }),
2378
+ entry.oldValue !== null || entry.newValue !== null ? /* @__PURE__ */ (0, import_jsx_runtime37.jsxs)("span", { className: "text-[var(--color-text-muted)]", children: [
2210
2379
  " ",
2211
2380
  "\u2014 ",
2212
2381
  entry.oldValue ?? "none",
2213
2382
  " \u2192 ",
2214
2383
  entry.newValue ?? "none"
2215
2384
  ] }) : null,
2216
- /* @__PURE__ */ (0, import_jsx_runtime36.jsxs)("span", { className: "block text-xs text-[var(--color-text-muted)]", children: [
2385
+ /* @__PURE__ */ (0, import_jsx_runtime37.jsxs)("span", { className: "block text-xs text-[var(--color-text-muted)]", children: [
2217
2386
  entry.actorPseudo ?? "Anonymous",
2218
2387
  " \xB7 ",
2219
2388
  dateFormat.format(new Date(entry.createdOn))
2220
2389
  ] })
2221
2390
  ] }, entry.uuid)) });
2222
- return /* @__PURE__ */ (0, import_jsx_runtime36.jsxs)(
2391
+ return /* @__PURE__ */ (0, import_jsx_runtime37.jsxs)(
2223
2392
  "details",
2224
2393
  {
2225
2394
  onToggle: (event) => setOpen(event.currentTarget.open),
2226
2395
  className: "px-6 pb-2.5 mt-2.5 border-t border-[var(--color-border)]",
2227
2396
  children: [
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 })
2397
+ /* @__PURE__ */ (0, import_jsx_runtime37.jsx)("summary", { className: "mt-2.5 cursor-pointer text-[0.6875rem] font-semibold uppercase text-[var(--color-text-muted)]", children: "History" }),
2398
+ /* @__PURE__ */ (0, import_jsx_runtime37.jsx)("div", { className: "mt-1.5", children: body })
2230
2399
  ]
2231
2400
  }
2232
2401
  );
@@ -2240,25 +2409,25 @@ function SongPanelContent({
2240
2409
  genrePath,
2241
2410
  onSelectGenre
2242
2411
  }) {
2243
- const { playNewTrackListFromTrackUuid } = (0, import_genre_tree25.useTrackList)();
2412
+ const { playNewTrackListFromTrackUuid } = (0, import_genre_tree26.useTrackList)();
2244
2413
  const { playerTrackObject, playState, handlePlayPauseAction } = (0, import_player11.usePlayer)();
2245
- const unplayableReason = (0, import_genre_tree25.songUnplayableReason)(song);
2414
+ const unplayableReason = (0, import_genre_tree26.songUnplayableReason)(song);
2246
2415
  const isCurrent = playerTrackObject?.track.id === song.uuid;
2247
2416
  const isPlaying = isCurrent && playState === import_player11.PlayStates.PLAYING;
2248
2417
  const shareTarget = { kind: "song", uuid: song.uuid, name: song.title, scope };
2249
- const musicBrainzUrl = (0, import_genre_tree25.musicBrainzRecordingUrl)(song);
2418
+ const musicBrainzUrl = (0, import_genre_tree26.musicBrainzRecordingUrl)(song);
2250
2419
  const fields = [];
2251
2420
  if (song.genre) {
2252
2421
  const path = genrePath.length > 0 ? genrePath : null;
2253
2422
  fields.push({
2254
2423
  label: "Genre",
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 })
2424
+ value: path ? /* @__PURE__ */ (0, import_jsx_runtime37.jsx)("span", { className: "flex flex-wrap items-center gap-x-1", children: path.map((genrePlaylist, index) => /* @__PURE__ */ (0, import_jsx_runtime37.jsxs)("span", { children: [
2425
+ index > 0 && /* @__PURE__ */ (0, import_jsx_runtime37.jsx)("span", { "aria-hidden": "true", children: "\u203A " }),
2426
+ /* @__PURE__ */ (0, import_jsx_runtime37.jsx)("button", { type: "button", onClick: () => onSelectGenre(genrePlaylist), className: "hover:underline", children: genrePlaylist.name })
2258
2427
  ] }, genrePlaylist.uuid)) }) : song.genre.name
2259
2428
  });
2260
2429
  } else {
2261
- fields.push({ label: "Genre", value: /* @__PURE__ */ (0, import_jsx_runtime36.jsx)("span", { className: "text-[var(--color-text-muted)]", children: "No genre" }) });
2430
+ fields.push({ label: "Genre", value: /* @__PURE__ */ (0, import_jsx_runtime37.jsx)("span", { className: "text-[var(--color-text-muted)]", children: "No genre" }) });
2262
2431
  }
2263
2432
  if (song.firstReleaseYear) fields.push({ label: "First released", value: song.firstReleaseYear });
2264
2433
  const viewCount = song.youtubeVideo?.viewCount;
@@ -2268,11 +2437,11 @@ function SongPanelContent({
2268
2437
  const links = [];
2269
2438
  if (musicBrainzUrl)
2270
2439
  links.push(
2271
- /* @__PURE__ */ (0, import_jsx_runtime36.jsx)("a", { href: musicBrainzUrl, target: "_blank", rel: "noopener noreferrer", className: externalLink, children: "MusicBrainz" }, "mb")
2440
+ /* @__PURE__ */ (0, import_jsx_runtime37.jsx)("a", { href: musicBrainzUrl, target: "_blank", rel: "noopener noreferrer", className: externalLink, children: "MusicBrainz" }, "mb")
2272
2441
  );
2273
2442
  if (song.youtubeVideo)
2274
2443
  links.push(
2275
- /* @__PURE__ */ (0, import_jsx_runtime36.jsx)(
2444
+ /* @__PURE__ */ (0, import_jsx_runtime37.jsx)(
2276
2445
  "a",
2277
2446
  {
2278
2447
  href: `https://www.youtube.com/watch?v=${encodeURIComponent(song.youtubeVideo.id)}`,
@@ -2287,33 +2456,33 @@ function SongPanelContent({
2287
2456
  if (links.length > 0)
2288
2457
  fields.push({
2289
2458
  label: "Links",
2290
- value: /* @__PURE__ */ (0, import_jsx_runtime36.jsx)("span", { className: "flex gap-3", children: links })
2459
+ value: /* @__PURE__ */ (0, import_jsx_runtime37.jsx)("span", { className: "flex gap-3", children: links })
2291
2460
  });
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)(
2461
+ return /* @__PURE__ */ (0, import_jsx_runtime37.jsxs)(import_jsx_runtime37.Fragment, { children: [
2462
+ /* @__PURE__ */ (0, import_jsx_runtime37.jsx)("div", { className: "genre-action-band border-b border-[var(--color-border)]", children: /* @__PURE__ */ (0, import_jsx_runtime37.jsxs)("div", { className: "flex flex-wrap gap-1 px-3.5 py-3", children: [
2463
+ unplayableReason ? /* @__PURE__ */ (0, import_jsx_runtime37.jsx)("p", { className: "flex items-center px-2.5 text-sm text-[var(--color-text-muted)]", children: unplayableReason }) : /* @__PURE__ */ (0, import_jsx_runtime37.jsxs)(
2295
2464
  "button",
2296
2465
  {
2297
2466
  type: "button",
2298
2467
  onClick: () => isCurrent ? handlePlayPauseAction() : playNewTrackListFromTrackUuid(song, scope),
2299
2468
  className: "group flex flex-col items-center gap-1 w-16 text-xs text-center text-[var(--color-text)] focus-visible:outline-none",
2300
2469
  children: [
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" }) }),
2470
+ /* @__PURE__ */ (0, import_jsx_runtime37.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_runtime37.jsx)(import_lucide_react11.Pause, { "aria-hidden": "true", className: "w-5 h-5" }) : /* @__PURE__ */ (0, import_jsx_runtime37.jsx)(import_lucide_react11.Play, { "aria-hidden": "true", className: "w-5 h-5" }) }),
2302
2471
  isPlaying ? "Pause" : "Play"
2303
2472
  ]
2304
2473
  }
2305
2474
  ),
2306
- shareUrl && /* @__PURE__ */ (0, import_jsx_runtime36.jsx)(ShareActionButton, { target: shareTarget, url: shareUrl })
2475
+ shareUrl && /* @__PURE__ */ (0, import_jsx_runtime37.jsx)(ShareActionButton, { target: shareTarget, url: shareUrl })
2307
2476
  ] }) }),
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 })
2477
+ song.artists && song.artists.length > 0 && /* @__PURE__ */ (0, import_jsx_runtime37.jsx)("p", { className: "px-6 pt-2.5 text-[var(--color-text-muted)]", children: /* @__PURE__ */ (0, import_jsx_runtime37.jsx)(import_genre_tree26.ArtistNames, { artists: song.artists, renderArtistLink }) }),
2478
+ /* @__PURE__ */ (0, import_jsx_runtime37.jsx)(import_ui14.FieldList, { items: fields }),
2479
+ song.essentialGenres && song.essentialGenres.length > 0 && /* @__PURE__ */ (0, import_jsx_runtime37.jsx)(import_ui14.PanelSection, { title: "Essential in", children: /* @__PURE__ */ (0, import_jsx_runtime37.jsx)("ul", { children: song.essentialGenres.map((genre) => /* @__PURE__ */ (0, import_jsx_runtime37.jsx)("li", { className: "py-0.5", children: genre.name }, genre.uuid)) }) }),
2480
+ /* @__PURE__ */ (0, import_jsx_runtime37.jsx)(SongHistory, { uuid: song.uuid, getBackendBaseUrl })
2312
2481
  ] });
2313
2482
  }
2314
2483
 
2315
2484
  // src/genre-tree/song/useSongPanel.tsx
2316
- var import_jsx_runtime37 = require("react/jsx-runtime");
2485
+ var import_jsx_runtime38 = require("react/jsx-runtime");
2317
2486
  function useSongPanel({
2318
2487
  uuid,
2319
2488
  scope,
@@ -2326,7 +2495,7 @@ function useSongPanel({
2326
2495
  onExpandedChange,
2327
2496
  onClose
2328
2497
  }) {
2329
- const result = (0, import_genre_tree26.useSong)(uuid, getBackendBaseUrl);
2498
+ const result = (0, import_genre_tree27.useSong)(uuid, getBackendBaseUrl);
2330
2499
  const panel = { expanded, onExpandedChange, onClose };
2331
2500
  switch (result.status) {
2332
2501
  case "idle":
@@ -2335,24 +2504,24 @@ function useSongPanel({
2335
2504
  return {
2336
2505
  ...panel,
2337
2506
  title: "Song",
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" })
2507
+ content: /* @__PURE__ */ (0, import_jsx_runtime38.jsxs)("div", { "aria-busy": "true", className: "flex flex-col gap-2.5 px-6 py-2.5", children: [
2508
+ /* @__PURE__ */ (0, import_jsx_runtime38.jsx)(import_ui15.Skeleton, { className: "h-3 w-16" }),
2509
+ /* @__PURE__ */ (0, import_jsx_runtime38.jsx)(import_ui15.Skeleton, { className: "h-4 w-32" }),
2510
+ /* @__PURE__ */ (0, import_jsx_runtime38.jsx)(import_ui15.Skeleton, { className: "h-3 w-16" }),
2511
+ /* @__PURE__ */ (0, import_jsx_runtime38.jsx)(import_ui15.Skeleton, { className: "h-4 w-24" })
2343
2512
  ] })
2344
2513
  };
2345
2514
  case "not-found":
2346
2515
  return {
2347
2516
  ...panel,
2348
2517
  title: "Song not found",
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." })
2518
+ content: /* @__PURE__ */ (0, import_jsx_runtime38.jsx)("p", { className: "px-6 py-2.5 text-[var(--color-text-muted)]", children: "This song does not exist." })
2350
2519
  };
2351
2520
  case "error":
2352
2521
  return {
2353
2522
  ...panel,
2354
2523
  title: "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." })
2524
+ content: /* @__PURE__ */ (0, import_jsx_runtime38.jsx)("p", { className: "px-6 py-2.5 text-red-600", children: "Could not load this song." })
2356
2525
  };
2357
2526
  case "success": {
2358
2527
  const { song } = result;
@@ -2360,7 +2529,7 @@ function useSongPanel({
2360
2529
  return {
2361
2530
  ...panel,
2362
2531
  title: song.title,
2363
- content: /* @__PURE__ */ (0, import_jsx_runtime37.jsx)(
2532
+ content: /* @__PURE__ */ (0, import_jsx_runtime38.jsx)(
2364
2533
  SongPanelContent,
2365
2534
  {
2366
2535
  song,
@@ -2379,29 +2548,29 @@ function useSongPanel({
2379
2548
 
2380
2549
  // src/genre-tree/SizeMetricToggle.tsx
2381
2550
  var import_md = require("react-icons/md");
2382
- var import_ui15 = require("@behindthemusictree/ui");
2383
- var import_jsx_runtime38 = require("react/jsx-runtime");
2551
+ var import_ui16 = require("@behindthemusictree/ui");
2552
+ var import_jsx_runtime39 = require("react/jsx-runtime");
2384
2553
  function SizeMetricToggle({
2385
2554
  value,
2386
2555
  onChange
2387
2556
  }) {
2388
- return /* @__PURE__ */ (0, import_jsx_runtime38.jsx)(
2389
- import_ui15.SegmentedControl,
2557
+ return /* @__PURE__ */ (0, import_jsx_runtime39.jsx)(
2558
+ import_ui16.SegmentedControl,
2390
2559
  {
2391
2560
  variant: "floating",
2392
2561
  "aria-label": "Size nodes by",
2393
2562
  value,
2394
2563
  onChange,
2395
2564
  options: [
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" }
2565
+ { value: "songs", label: /* @__PURE__ */ (0, import_jsx_runtime39.jsx)(import_md.MdMusicNote, { size: 18, "aria-hidden": true }), "aria-label": "Size by songs", title: "Node size by song count" },
2566
+ { value: "views", label: /* @__PURE__ */ (0, import_jsx_runtime39.jsx)(import_md.MdVisibility, { size: 18, "aria-hidden": true }), "aria-label": "Size by views", title: "Node size by song views" }
2398
2567
  ]
2399
2568
  }
2400
2569
  );
2401
2570
  }
2402
2571
 
2403
2572
  // src/genre-tree/GenreTreeView.tsx
2404
- var import_jsx_runtime39 = require("react/jsx-runtime");
2573
+ var import_jsx_runtime40 = require("react/jsx-runtime");
2405
2574
  var NO_POP_ROOT_TITLE = "This genre tree has no 'Mainstream Pop' root yet";
2406
2575
  var REGIONS_CLASS = "[--gtv-center-left:var(--layout-center-left)] [--gtv-center-right:var(--layout-center-right)] [--gtv-center-top:var(--layout-band-top)]";
2407
2576
  var noop = () => {
@@ -2414,8 +2583,8 @@ function GenreTreeView({
2414
2583
  getBackendBaseUrl,
2415
2584
  additionalActions,
2416
2585
  viewMode: controlledViewMode,
2417
- selectedNodeId: controlledSelectedNodeId,
2418
- onSelectedNodeIdChange,
2586
+ selectedGenreUuid: controlledSelectedGenreUuid,
2587
+ onSelectedGenreUuidChange,
2419
2588
  defaultViewport,
2420
2589
  viewportRestore,
2421
2590
  onViewportChange,
@@ -2463,21 +2632,21 @@ function GenreTreeView({
2463
2632
  placement = "default",
2464
2633
  ...focus
2465
2634
  }) {
2466
- const getShareUrlRef = (0, import_react17.useRef)(getShareUrl);
2635
+ const getShareUrlRef = (0, import_react18.useRef)(getShareUrl);
2467
2636
  getShareUrlRef.current = getShareUrl;
2468
- const [internalArtistUuid, setInternalArtistUuid] = (0, import_react17.useState)(null);
2637
+ const [internalArtistUuid, setInternalArtistUuid] = (0, import_react18.useState)(null);
2469
2638
  const isArtistControlled = controlledArtistUuid !== void 0;
2470
2639
  const artistUuid = isArtistControlled ? controlledArtistUuid : internalArtistUuid;
2471
- const [internalSongUuid, setInternalSongUuid] = (0, import_react17.useState)(null);
2640
+ const [internalSongUuid, setInternalSongUuid] = (0, import_react18.useState)(null);
2472
2641
  const isSongControlled = controlledSongUuid !== void 0;
2473
2642
  const songUuid = isSongControlled ? controlledSongUuid : internalSongUuid;
2474
2643
  const changeSong = (uuid) => {
2475
2644
  if (!isSongControlled) setInternalSongUuid(uuid);
2476
2645
  onSongUuidChange?.(uuid);
2477
2646
  };
2478
- const [internalSizeMetric, setInternalSizeMetric] = (0, import_react17.useState)("songs");
2647
+ const [internalSizeMetric, setInternalSizeMetric] = (0, import_react18.useState)("songs");
2479
2648
  const sizeMetric = controlledSizeMetric ?? internalSizeMetric;
2480
- const deferredSizeMetric = (0, import_react17.useDeferredValue)(sizeMetric);
2649
+ const deferredSizeMetric = (0, import_react18.useDeferredValue)(sizeMetric);
2481
2650
  const changeSizeMetric = (metric) => {
2482
2651
  if (controlledSizeMetric === void 0) setInternalSizeMetric(metric);
2483
2652
  onSizeMetricChange?.(metric);
@@ -2492,6 +2661,8 @@ function GenreTreeView({
2492
2661
  genrePlaylistByNodeId,
2493
2662
  visibleGenrePlaylists,
2494
2663
  groupedGenrePlaylistsByRoot,
2664
+ selectedGenreUuid,
2665
+ selectGenre,
2495
2666
  selectedNodeId,
2496
2667
  selectedGenrePlaylist,
2497
2668
  selectNode,
@@ -2504,17 +2675,17 @@ function GenreTreeView({
2504
2675
  allowWheelRotation: internalAllowWheelRotation,
2505
2676
  toggleWheelRotation,
2506
2677
  onNodeHover: handleNodeHover
2507
- } = (0, import_genre_tree27.useGenreTreeViewState)({
2678
+ } = (0, import_genre_tree28.useGenreTreeViewState)({
2508
2679
  scope,
2509
2680
  getBackendBaseUrl,
2510
2681
  viewMode: controlledViewMode,
2511
- selectedNodeId: controlledSelectedNodeId,
2512
- onSelectedNodeIdChange,
2682
+ selectedGenreUuid: controlledSelectedGenreUuid,
2683
+ onSelectedGenreUuidChange,
2513
2684
  criteriaOverviewSchema,
2514
2685
  artistUuid
2515
2686
  });
2516
2687
  const restoreRevision = viewportRestore?.revision;
2517
- const [stackedRestoreOwner, setStackedRestoreOwner] = (0, import_react17.useState)({ revision: restoreRevision, rootUuid: selectedGenrePlaylist?.root.uuid });
2688
+ const [stackedRestoreOwner, setStackedRestoreOwner] = (0, import_react18.useState)({ revision: restoreRevision, rootUuid: selectedGenrePlaylist?.root.uuid });
2518
2689
  let restoreRootUuid = stackedRestoreOwner.rootUuid;
2519
2690
  if (stackedRestoreOwner.revision !== restoreRevision) {
2520
2691
  restoreRootUuid = selectedGenrePlaylist?.root.uuid;
@@ -2523,36 +2694,36 @@ function GenreTreeView({
2523
2694
  restoreRootUuid = selectedGenrePlaylist.root.uuid;
2524
2695
  setStackedRestoreOwner({ revision: restoreRevision, rootUuid: restoreRootUuid });
2525
2696
  }
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);
2697
+ const isGenreUpdating = (0, import_react_query3.useIsMutating)({ mutationKey: import_genre_tree28.genreUpdateMutationKey }) > 0;
2698
+ const { mutate: updateGenre } = (0, import_genre_tree28.useUpdateGenre)(scope, getBackendBaseUrl);
2528
2699
  const busy = deferredSizeMetric !== sizeMetric || isTreeFetching || isGenreUpdating;
2529
2700
  const isArtistFiltered = artistUuid !== null;
2530
2701
  const canSizeByViews = !isArtistFiltered && hasViewsCount(visibleGenrePlaylists);
2531
- const artistResult = (0, import_genre_tree27.useArtist)(artistUuid, artistSearchBaseUrl ?? getBackendBaseUrl);
2702
+ const artistResult = (0, import_genre_tree28.useArtist)(artistUuid, artistSearchBaseUrl ?? getBackendBaseUrl);
2532
2703
  const filterArtistName = artistResult.status === "success" ? artistResult.artist.name : null;
2533
- const filterArtist = (0, import_react17.useMemo)(
2704
+ const filterArtist = (0, import_react18.useMemo)(
2534
2705
  () => artistUuid !== null && filterArtistName !== null ? { uuid: artistUuid, name: filterArtistName } : null,
2535
2706
  [artistUuid, filterArtistName]
2536
2707
  );
2537
2708
  const hasSelectedGenreNode = selectedGenrePlaylist !== void 0 && visibleGenrePlaylists.some(({ uuid }) => uuid === selectedGenrePlaylist.uuid);
2538
2709
  const playback = useGenrePlaylistPlayback({ scope, genrePlaylists: visibleGenrePlaylists, artist: filterArtist });
2539
2710
  const openArtist = (uuid) => {
2540
- if (selectedNodeId !== null) clearSelection();
2711
+ if (selectedGenreUuid !== null) clearSelection();
2541
2712
  if (songUuid !== null) changeSong(null);
2542
2713
  changeArtist(uuid);
2543
2714
  };
2544
2715
  const openSong = (uuid) => {
2545
- if (selectedNodeId !== null) clearSelection();
2716
+ if (selectedGenreUuid !== null) clearSelection();
2546
2717
  changeSong(uuid);
2547
2718
  };
2548
- const openSongRef = (0, import_react17.useRef)(openSong);
2719
+ const openSongRef = (0, import_react18.useRef)(openSong);
2549
2720
  openSongRef.current = openSong;
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 }),
2721
+ const resolvedRenderSongLink = (0, import_react18.useCallback)(
2722
+ (song) => renderSongLink ? renderSongLink(song) : /* @__PURE__ */ (0, import_jsx_runtime40.jsx)("button", { type: "button", onClick: () => openSongRef.current(song.uuid), className: "hover:underline", children: song.title }),
2552
2723
  [renderSongLink]
2553
2724
  );
2554
- const [expandedSongUuid, setExpandedSongUuid] = (0, import_react17.useState)(null);
2555
- const [expandedArtistUuid, setExpandedArtistUuid] = (0, import_react17.useState)(null);
2725
+ const [expandedSongUuid, setExpandedSongUuid] = (0, import_react18.useState)(null);
2726
+ const [expandedArtistUuid, setExpandedArtistUuid] = (0, import_react18.useState)(null);
2556
2727
  const artistBaseUrl = artistSearchBaseUrl ?? getBackendBaseUrl;
2557
2728
  const songBaseUrl = getBackendBaseUrl;
2558
2729
  const artistPanel = useArtistPanel({
@@ -2560,7 +2731,7 @@ function GenreTreeView({
2560
2731
  scope,
2561
2732
  getBackendBaseUrl: artistBaseUrl,
2562
2733
  getShareUrl: getShareUrlRef.current,
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 })),
2734
+ renderArtistLink: renderArtistLink ?? ((artist) => artist.isSpecialPurpose ? artist.name : /* @__PURE__ */ (0, import_jsx_runtime40.jsx)("button", { type: "button", onClick: () => openArtist(artist.uuid), className: "hover:underline", children: artist.name })),
2564
2735
  expanded: controlledArtistExpanded ?? (artistUuid !== null && expandedArtistUuid === artistUuid),
2565
2736
  onExpandedChange: (on) => {
2566
2737
  if (controlledArtistExpanded === void 0) setExpandedArtistUuid(on ? artistUuid : null);
@@ -2569,7 +2740,7 @@ function GenreTreeView({
2569
2740
  renderSongLink: resolvedRenderSongLink,
2570
2741
  onClose: () => changeArtist(null)
2571
2742
  });
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 }));
2743
+ const resolvedRenderArtistLink = renderArtistLink ?? ((artist) => artist.isSpecialPurpose ? artist.name : /* @__PURE__ */ (0, import_jsx_runtime40.jsx)("button", { type: "button", onClick: () => openArtist(artist.uuid), className: "hover:underline", children: artist.name }));
2573
2744
  const getGenrePath = (genre) => {
2574
2745
  const path = [];
2575
2746
  let current = visibleGenrePlaylists.find((playlist) => playlist.criteria?.uuid === genre.uuid);
@@ -2587,9 +2758,9 @@ function GenreTreeView({
2587
2758
  getShareUrl: getShareUrlRef.current,
2588
2759
  renderArtistLink: resolvedRenderArtistLink,
2589
2760
  getGenrePath,
2590
- onSelectGenre: (genrePlaylist) => {
2761
+ onSelectGenre: (genrePlaylist2) => {
2591
2762
  changeSong(null);
2592
- selectGenrePlaylist(genrePlaylist);
2763
+ selectGenrePlaylist(genrePlaylist2);
2593
2764
  },
2594
2765
  expanded: controlledSongExpanded ?? (songUuid !== null && expandedSongUuid === songUuid),
2595
2766
  onExpandedChange: (on) => {
@@ -2598,11 +2769,11 @@ function GenreTreeView({
2598
2769
  },
2599
2770
  onClose: () => changeSong(null)
2600
2771
  });
2601
- const builtInSongPanel = overlayPanel || hasSelectedGenreNode ? void 0 : songPanel;
2602
- const builtInArtistPanel = overlayPanel || hasSelectedGenreNode || builtInSongPanel ? void 0 : artistPanel;
2772
+ const builtInSongPanel = overlayPanel || selectedGenreUuid !== null ? void 0 : songPanel;
2773
+ const builtInArtistPanel = overlayPanel || selectedGenreUuid !== null || builtInSongPanel ? void 0 : artistPanel;
2603
2774
  const builtInPanel = builtInSongPanel ?? builtInArtistPanel;
2604
- const [uncontrolledCollapsed, setUncontrolledCollapsed] = (0, import_react17.useState)(false);
2605
- const [uncontrolledExpanded, setUncontrolledExpanded] = (0, import_react17.useState)(false);
2775
+ const [uncontrolledCollapsed, setUncontrolledCollapsed] = (0, import_react18.useState)(false);
2776
+ const [uncontrolledExpanded, setUncontrolledExpanded] = (0, import_react18.useState)(false);
2606
2777
  const isCollapsed = collapsed ?? uncontrolledCollapsed;
2607
2778
  const isExpanded = expanded ?? uncontrolledExpanded;
2608
2779
  const changeCollapsed = (next) => {
@@ -2613,155 +2784,114 @@ function GenreTreeView({
2613
2784
  setUncontrolledExpanded(next);
2614
2785
  onExpandedChange?.(next);
2615
2786
  };
2616
- (0, import_react17.useEffect)(() => {
2787
+ (0, import_react18.useEffect)(() => {
2617
2788
  setUncontrolledCollapsed(false);
2618
- }, [selectedNodeId, artistUuid, songUuid]);
2619
- const [isWheelRevealed, setIsWheelRevealed] = (0, import_react17.useState)(false);
2789
+ }, [selectedGenreUuid, artistUuid, songUuid]);
2790
+ const [isWheelRevealed, setIsWheelRevealed] = (0, import_react18.useState)(false);
2620
2791
  const usesHandoff = viewMode === "wheel" || viewMode === "pop-core";
2621
2792
  const isTreeReady = !isLoading && (!usesHandoff || isWheelRevealed);
2622
- const [resolveStyle, setResolveStyle] = (0, import_react17.useState)(null);
2623
- const handleNodeStyleResolverChange = (0, import_react17.useCallback)(
2793
+ const [resolveStyle, setResolveStyle] = (0, import_react18.useState)(null);
2794
+ const handleNodeStyleResolverChange = (0, import_react18.useCallback)(
2624
2795
  (resolver) => setResolveStyle(() => resolver),
2625
2796
  []
2626
2797
  );
2627
- const panelRef = (0, import_react17.useRef)(null);
2628
- const obscuredInset = (0, import_ui16.useObscuredInset)(panelRef);
2798
+ const panelRef = (0, import_react18.useRef)(null);
2799
+ const obscuredInset = (0, import_ui17.useObscuredInset)(panelRef);
2629
2800
  const hostPanel = { obscuredInset, onNodeStyleResolverChange: handleNodeStyleResolverChange };
2630
2801
  const nodeMetric = isArtistFiltered || !canSizeByViews ? "songs" : deferredSizeMetric;
2631
- const treeNodes = (0, import_react17.useMemo)(() => toTreeNodes(visibleGenrePlaylists, nodeMetric), [visibleGenrePlaylists, nodeMetric]);
2632
- const relations = (0, import_genre_tree_view4.useGenreNodeRelations)(
2802
+ const treeNodes = (0, import_react18.useMemo)(() => toTreeNodes(visibleGenrePlaylists, nodeMetric), [visibleGenrePlaylists, nodeMetric]);
2803
+ const relations = (0, import_genre_tree_view5.useGenreNodeRelations)(
2633
2804
  treeNodes,
2634
2805
  !overlayPanel && hasSelectedGenreNode ? selectedNodeId : null,
2635
2806
  resolveStyle
2636
2807
  );
2637
- const [selectionHistory, setSelectionHistory] = (0, import_react17.useState)({
2638
- current: selectedNodeId,
2639
- previous: null
2640
- });
2641
- if (selectionHistory.current !== selectedNodeId) {
2642
- setSelectionHistory({
2643
- current: selectedNodeId,
2644
- previous: selectionHistory.current ?? selectionHistory.previous
2645
- });
2808
+ const [selectionHistory, setSelectionHistory] = (0, import_react18.useState)({ current: selectedGenreUuid, previous: null });
2809
+ if (selectionHistory.current !== selectedGenreUuid) {
2810
+ setSelectionHistory({ current: selectedGenreUuid, previous: selectionHistory.current ?? selectionHistory.previous });
2646
2811
  }
2647
- const previousPlaylist = selectionHistory.previous !== null && selectionHistory.previous !== selectedNodeId ? genrePlaylistByNodeId.get(selectionHistory.previous) : void 0;
2648
- const handlePrevious = previousPlaylist && visibleGenrePlaylists.includes(previousPlaylist) ? () => handleGenreSearchSelect(previousPlaylist) : void 0;
2812
+ const handlePrevious = selectionHistory.previous ? () => selectGenre(selectionHistory.previous) : void 0;
2649
2813
  const { showPopup, hidePopup } = (0, import_popup5.usePopup)();
2814
+ const reopenPanel = () => {
2815
+ setUncontrolledCollapsed(false);
2816
+ onCollapsedChange?.(false);
2817
+ };
2650
2818
  const handleSelectedNodeChange = (node) => {
2819
+ reopenPanel();
2651
2820
  if (node !== null && songUuid !== null) changeSong(null);
2652
2821
  selectNode(node);
2653
2822
  };
2654
2823
  const handleGenreSearchClear = clearSelection;
2655
- const handleGenreSearchSelect = (genrePlaylist) => {
2824
+ const handleGenreSearchSelect = (genrePlaylist2) => {
2825
+ reopenPanel();
2656
2826
  if (songUuid !== null) changeSong(null);
2657
- selectGenrePlaylist(genrePlaylist);
2827
+ selectGenrePlaylist(genrePlaylist2);
2658
2828
  };
2659
2829
  const selectedUuid = selectedGenrePlaylist?.uuid;
2660
- const selectedChildren = selectedUuid ? visibleGenrePlaylists.filter((genrePlaylist) => genrePlaylist.parent?.uuid === selectedUuid) : [];
2830
+ const selectedChildren = selectedUuid ? visibleGenrePlaylists.filter((genrePlaylist2) => genrePlaylist2.parent?.uuid === selectedUuid) : [];
2661
2831
  const canZoomIn = viewMode === "pop-core" && focus.onFocusGenreChange !== void 0 && selectedUuid !== void 0 && focus.focusGenreUuid !== selectedUuid && selectedChildren.length > 0 && selectedChildren.filter((child) => child.criteria?.side === "pop").length <= 1;
2662
2832
  const handleGenreSearchZoomIn = () => {
2663
2833
  if (selectedUuid) focus.onFocusGenreChange?.(selectedUuid, "zoom-in");
2664
2834
  };
2665
- const zoomInRef = (0, import_react17.useRef)(void 0);
2835
+ const zoomInRef = (0, import_react18.useRef)(void 0);
2666
2836
  zoomInRef.current = canZoomIn ? handleGenreSearchZoomIn : void 0;
2667
- const selectGenreFromPanel = (nodeId) => {
2668
- const genrePlaylist = genrePlaylistByNodeId.get(nodeId);
2669
- if (genrePlaylist) handleGenreSearchSelect(genrePlaylist);
2670
- };
2671
- const renderGenreBody = (relation) => {
2672
- const { node } = relation;
2673
- const family = /* @__PURE__ */ (0, import_jsx_runtime39.jsx)(
2674
- import_genre_tree_view4.GenreNodeFamily,
2675
- {
2676
- node,
2677
- fill: relation.fill,
2678
- textColor: relation.textColor,
2679
- parentNode: relation.parentNode,
2680
- childNodes: relation.childNodes,
2681
- onSelectNode: selectGenreFromPanel
2682
- }
2683
- );
2684
- const ancestors = /* @__PURE__ */ (0, import_jsx_runtime39.jsx)(import_genre_tree_view4.GenreNodeAncestors, { ancestorNodes: relation.ancestorNodes, onSelectNode: selectGenreFromPanel });
2685
- const genrePlaylist = genrePlaylistByNodeId.get(node.id);
2686
- const criteria = genrePlaylist?.criteria;
2687
- if (!genrePlaylist || !criteria) return { primary: /* @__PURE__ */ (0, import_jsx_runtime39.jsxs)(import_jsx_runtime39.Fragment, { children: [
2688
- family,
2689
- ancestors
2690
- ] }) };
2691
- const genreUuid = criteria.uuid;
2692
- const parentPlaylist = visibleGenrePlaylists.find(({ uuid }) => uuid === genrePlaylist.parent?.uuid);
2693
- const canToggleSide = !readOnly && parentPlaylist !== void 0 && parentPlaylist.parent === null;
2694
- const side = criteria.side ?? "core";
2695
- const shareTarget = {
2696
- kind: "genre",
2697
- genrePlaylistUuid: genrePlaylist.uuid,
2698
- name: criteria.name,
2699
- scope,
2700
- artistUuid
2701
- };
2702
- const shareUrl = getShareUrlRef.current?.(shareTarget) ?? void 0;
2703
- const popSibling = canToggleSide && side === "core" ? visibleGenrePlaylists.find(
2704
- (sibling) => sibling.parent?.uuid === parentPlaylist.uuid && sibling.uuid !== genrePlaylist.uuid && sibling.criteria?.side === "pop"
2705
- ) : void 0;
2706
- return {
2707
- primary: /* @__PURE__ */ (0, import_jsx_runtime39.jsxs)(import_jsx_runtime39.Fragment, { children: [
2708
- /* @__PURE__ */ (0, import_jsx_runtime39.jsx)(
2709
- GenreInfoPanelBody,
2837
+ const genrePlaylist = selectedGenrePlaylist;
2838
+ const criteria = genrePlaylist?.criteria;
2839
+ const parentPlaylist = visibleGenrePlaylists.find(({ uuid }) => uuid === genrePlaylist?.parent?.uuid);
2840
+ const canToggleSide = !readOnly && criteria !== void 0 && criteria !== null && parentPlaylist !== void 0 && parentPlaylist.parent === null;
2841
+ const side = criteria?.side ?? "core";
2842
+ const popSibling = canToggleSide && side === "core" ? visibleGenrePlaylists.find((sibling) => sibling.parent?.uuid === parentPlaylist.uuid && sibling.uuid !== genrePlaylist?.uuid && sibling.criteria?.side === "pop") : void 0;
2843
+ const shareTarget = selectedGenreUuid ? { kind: "genre", genreUuid: selectedGenreUuid, name: criteria?.name ?? "Genre", scope, artistUuid } : void 0;
2844
+ const shareUrl = shareTarget ? getShareUrlRef.current?.(shareTarget) ?? void 0 : void 0;
2845
+ const genreContent = useGenreDetailContent({
2846
+ genreUuid: selectedGenreUuid,
2847
+ scope,
2848
+ getBackendBaseUrl,
2849
+ onSelectGenre: (uuid) => {
2850
+ reopenPanel();
2851
+ selectGenre(uuid);
2852
+ },
2853
+ expanded: isExpanded,
2854
+ playback,
2855
+ genrePlaylist: selectedGenrePlaylist,
2856
+ band: {
2857
+ ...relations ? { node: relations.node } : {},
2858
+ shareTarget: shareUrl ? shareTarget : void 0,
2859
+ shareUrl,
2860
+ onZoomIn: zoomInRef.current,
2861
+ onAddChild: readOnly || !criteria ? void 0 : () => handleGenreCreationAction(criteria),
2862
+ onRename: readOnly || !criteria ? void 0 : () => handleGenreRenameAction(criteria),
2863
+ onChangeParent: readOnly || !genrePlaylist || !criteria ? void 0 : () => showPopup(
2864
+ /* @__PURE__ */ (0, import_jsx_runtime40.jsx)(
2865
+ ChangeParentPopup,
2710
2866
  {
2711
- family: /* @__PURE__ */ (0, import_jsx_runtime39.jsxs)(import_jsx_runtime39.Fragment, { children: [
2712
- family,
2713
- ancestors
2714
- ] }),
2715
- genreUuid,
2716
- genrePlaylist,
2717
- catalogue: genrePlaylist.catalogue,
2718
- wikidataId: criteria.wikidataId,
2867
+ genrePlaylist: { ...genrePlaylist, criteria },
2868
+ genrePlaylists: visibleGenrePlaylists,
2719
2869
  scope,
2720
2870
  getBackendBaseUrl,
2721
- playback,
2722
- band: {
2723
- node,
2724
- shareTarget: shareUrl ? shareTarget : void 0,
2725
- shareUrl,
2726
- onZoomIn: zoomInRef.current,
2727
- onAddChild: readOnly ? void 0 : () => handleGenreCreationAction(criteria),
2728
- onRename: readOnly ? void 0 : () => handleGenreRenameAction(criteria),
2729
- onChangeParent: readOnly ? void 0 : () => showPopup(
2730
- /* @__PURE__ */ (0, import_jsx_runtime39.jsx)(
2731
- ChangeParentPopup,
2732
- {
2733
- genrePlaylist: { ...genrePlaylist, criteria },
2734
- genrePlaylists: visibleGenrePlaylists,
2735
- scope,
2736
- getBackendBaseUrl,
2737
- onClose: hidePopup,
2738
- onParentChanged
2739
- }
2740
- )
2741
- ),
2742
- side,
2743
- onToggleSide: canToggleSide ? () => updateGenre({ uuid: genreUuid, data: { side: side === "pop" ? "core" : "pop" } }) : void 0,
2744
- toggleSideDisabledReason: popSibling ? `${popSibling.name} is already the pop side of ${parentPlaylist?.name}` : void 0,
2745
- additionalActions
2746
- },
2747
- extras: {
2748
- criteriaOverviewSchema,
2749
- renderGenreDetailExtras,
2750
- renderArtistLink,
2751
- renderSongLink: resolvedRenderSongLink,
2752
- artist: filterArtist,
2753
- renderSummary,
2754
- renderEssentialTracks,
2755
- renderSongLeading,
2756
- renderSongActions,
2757
- renderTracksToolbar
2758
- }
2871
+ onClose: hidePopup,
2872
+ onParentChanged
2759
2873
  }
2760
- ),
2761
- renderDangerZone?.(node)
2762
- ] })
2763
- };
2764
- };
2874
+ )
2875
+ ),
2876
+ side,
2877
+ onToggleSide: canToggleSide && criteria ? () => updateGenre({ uuid: criteria.uuid, data: { side: side === "pop" ? "core" : "pop" } }) : void 0,
2878
+ toggleSideDisabledReason: popSibling ? `${popSibling.name} is already the pop side of ${parentPlaylist?.name}` : void 0,
2879
+ additionalActions
2880
+ },
2881
+ extras: {
2882
+ criteriaOverviewSchema,
2883
+ renderGenreDetailExtras,
2884
+ renderArtistLink,
2885
+ renderSongLink: resolvedRenderSongLink,
2886
+ artist: filterArtist,
2887
+ renderSummary,
2888
+ renderEssentialTracks,
2889
+ renderSongLeading,
2890
+ renderSongActions,
2891
+ renderTracksToolbar
2892
+ },
2893
+ trailing: relations ? renderDangerZone?.(relations.node) : void 0
2894
+ });
2765
2895
  const isRotationControlled = controlledAllowWheelRotation !== void 0;
2766
2896
  const allowWheelRotation = controlledAllowWheelRotation ?? internalAllowWheelRotation;
2767
2897
  let activePanel = null;
@@ -2775,16 +2905,16 @@ function GenreTreeView({
2775
2905
  onClose: overlayPanel.onClose,
2776
2906
  compact: false
2777
2907
  };
2778
- } else if (relations) {
2779
- const { primary } = renderGenreBody(relations);
2908
+ } else if (selectedGenreUuid) {
2780
2909
  activePanel = {
2781
- title: relations.node.name,
2782
- content: primary,
2910
+ title: genreContent.title,
2911
+ content: genreContent.content,
2912
+ aside: genreContent.aside,
2783
2913
  expanded: isExpanded,
2784
2914
  onExpandedChange: changeExpanded,
2785
2915
  onClose: clearSelection,
2786
- compact: true,
2787
- headerAccent: { background: relations.fill, color: relations.textColor }
2916
+ compact: false,
2917
+ headerAccent: relations ? { background: relations.fill, color: relations.textColor } : genreContent.headerAccent
2788
2918
  };
2789
2919
  } else if (builtInPanel) {
2790
2920
  activePanel = {
@@ -2797,36 +2927,36 @@ function GenreTreeView({
2797
2927
  compact: true
2798
2928
  };
2799
2929
  }
2800
- const isPanelShown = isTreeReady && activePanel !== null;
2930
+ const isPanelShown = activePanel !== null && (selectedGenreUuid !== null || isTreeReady);
2801
2931
  const detailTab = placement === "regions" && activePanel?.onExpandedChange ? {
2802
2932
  edge: activePanel.expanded ? { label: "Shrink panel", onClick: () => activePanel.onExpandedChange?.(false) } : isCollapsed ? null : { label: "Hide panel", onClick: () => changeCollapsed(true) },
2803
2933
  center: activePanel.expanded ? null : isCollapsed ? { label: "Show panel", onClick: () => changeCollapsed(false) } : activePanel.onExpandedChange ? { label: "Extend panel", onClick: () => activePanel.onExpandedChange?.(true) } : null
2804
2934
  } : void 0;
2805
- const expandButton = placement === "default" && activePanel?.onExpandedChange ? /* @__PURE__ */ (0, import_jsx_runtime39.jsx)(
2806
- import_ui16.IconButton,
2935
+ const expandButton = placement === "default" && activePanel?.onExpandedChange ? /* @__PURE__ */ (0, import_jsx_runtime40.jsx)(
2936
+ import_ui17.IconButton,
2807
2937
  {
2808
2938
  "aria-label": activePanel.expanded ? "Shrink panel" : "Extend panel",
2809
2939
  style: activePanel.headerAccent && { color: "inherit" },
2810
2940
  onClick: () => activePanel.onExpandedChange?.(!activePanel.expanded),
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" })
2941
+ children: activePanel.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" })
2812
2942
  }
2813
2943
  ) : null;
2814
2944
  const renderArtistFilter = () => {
2815
2945
  if (isLoading || !filterArtist) return null;
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)(
2946
+ return /* @__PURE__ */ (0, import_jsx_runtime40.jsxs)("span", { className: "inline-flex items-center overflow-hidden rounded-full bg-[var(--color-neutral-100)] text-sm", children: [
2947
+ /* @__PURE__ */ (0, import_jsx_runtime40.jsx)(
2818
2948
  "button",
2819
2949
  {
2820
2950
  type: "button",
2821
2951
  className: "py-1 pl-3 pr-2 [@media(pointer:coarse)]:min-h-[var(--layout-touch-target)] hover:underline",
2822
2952
  "aria-label": `Show artist ${filterArtist.name}`,
2823
2953
  onClick: () => {
2824
- if (selectedNodeId !== null) clearSelection();
2954
+ if (selectedGenreUuid !== null) clearSelection();
2825
2955
  },
2826
2956
  children: filterArtist.name
2827
2957
  }
2828
2958
  ),
2829
- /* @__PURE__ */ (0, import_jsx_runtime39.jsx)(
2959
+ /* @__PURE__ */ (0, import_jsx_runtime40.jsx)(
2830
2960
  "button",
2831
2961
  {
2832
2962
  type: "button",
@@ -2838,8 +2968,8 @@ function GenreTreeView({
2838
2968
  )
2839
2969
  ] });
2840
2970
  };
2841
- const actions = /* @__PURE__ */ (0, import_jsx_runtime39.jsxs)(import_jsx_runtime39.Fragment, { children: [
2842
- !isLoading && /* @__PURE__ */ (0, import_jsx_runtime39.jsx)(
2971
+ const actions = /* @__PURE__ */ (0, import_jsx_runtime40.jsxs)(import_jsx_runtime40.Fragment, { children: [
2972
+ !isLoading && /* @__PURE__ */ (0, import_jsx_runtime40.jsx)(
2843
2973
  GenreTreeSearch,
2844
2974
  {
2845
2975
  artistBaseUrl,
@@ -2857,7 +2987,12 @@ function GenreTreeView({
2857
2987
  SearchGroups,
2858
2988
  genrePlaylists: visibleGenrePlaylists,
2859
2989
  onSelect: handleGenreSearchSelect,
2860
- selectedName: selectedGenrePlaylist?.name ?? (typeof builtInPanel?.title === "string" ? builtInPanel.title : null),
2990
+ onSelectGenre: (uuid) => {
2991
+ reopenPanel();
2992
+ if (songUuid) changeSong(null);
2993
+ selectGenre(uuid);
2994
+ },
2995
+ selectedName: (selectedGenreUuid ? genreContent.title : null) ?? (typeof builtInPanel?.title === "string" ? builtInPanel.title : null),
2861
2996
  onClear: builtInSongPanel ? () => changeSong(null) : builtInArtistPanel ? () => changeArtist(null) : handleGenreSearchClear,
2862
2997
  onPrevious: handlePrevious,
2863
2998
  onQueryChange: onSearchQueryChange,
@@ -2865,10 +3000,10 @@ function GenreTreeView({
2865
3000
  extraGroups: extraSearchGroups
2866
3001
  }
2867
3002
  ),
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)(
2871
- import_ui16.SegmentedControl,
3003
+ !isLoading && filterArtist && /* @__PURE__ */ (0, import_jsx_runtime40.jsx)("div", { className: "hidden w-full justify-center max-md:flex", children: renderArtistFilter() }),
3004
+ /* @__PURE__ */ (0, import_jsx_runtime40.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: [
3005
+ !isLoading && !isControlled && /* @__PURE__ */ (0, import_jsx_runtime40.jsx)(
3006
+ import_ui17.SegmentedControl,
2872
3007
  {
2873
3008
  "aria-label": "Tree view mode",
2874
3009
  value: viewMode,
@@ -2881,14 +3016,14 @@ function GenreTreeView({
2881
3016
  ]
2882
3017
  }
2883
3018
  ),
2884
- !isLoading && showDisplayOptions && /* @__PURE__ */ (0, import_jsx_runtime39.jsx)(
3019
+ !isLoading && showDisplayOptions && /* @__PURE__ */ (0, import_jsx_runtime40.jsx)(
2885
3020
  "div",
2886
3021
  {
2887
3022
  className: "flex items-center gap-1",
2888
3023
  role: "group",
2889
3024
  "aria-label": "Tree display options",
2890
- children: !isRotationControlled && viewMode !== "stacked" && viewMode !== "outline" && /* @__PURE__ */ (0, import_jsx_runtime39.jsx)(
2891
- import_ui16.Button,
3025
+ children: !isRotationControlled && viewMode !== "stacked" && viewMode !== "outline" && /* @__PURE__ */ (0, import_jsx_runtime40.jsx)(
3026
+ import_ui17.Button,
2892
3027
  {
2893
3028
  variant: allowWheelRotation ? "default" : "outline",
2894
3029
  size: "sm",
@@ -2898,44 +3033,44 @@ function GenreTreeView({
2898
3033
  )
2899
3034
  }
2900
3035
  ),
2901
- !isLoading && !readOnly && showAddRoot && /* @__PURE__ */ (0, import_jsx_runtime39.jsx)(
2902
- import_ui16.IconTextButton,
3036
+ !isLoading && !readOnly && showAddRoot && /* @__PURE__ */ (0, import_jsx_runtime40.jsx)(
3037
+ import_ui17.IconTextButton,
2903
3038
  {
2904
- icon: import_lucide_react11.Plus,
3039
+ icon: import_lucide_react12.Plus,
2905
3040
  text: "Add root",
2906
3041
  onClick: () => handleGenreCreationAction(null)
2907
3042
  }
2908
3043
  )
2909
3044
  ] })
2910
3045
  ] });
2911
- return /* @__PURE__ */ (0, import_jsx_runtime39.jsxs)(
3046
+ return /* @__PURE__ */ (0, import_jsx_runtime40.jsxs)(
2912
3047
  "div",
2913
3048
  {
2914
3049
  "data-left-panel": isPanelShown && !isCollapsed ? void 0 : "hidden",
2915
3050
  className: ["genre-tree-view relative flex flex-col h-full", placement === "regions" && REGIONS_CLASS, className2].filter(Boolean).join(" "),
2916
3051
  children: [
2917
- /* @__PURE__ */ (0, import_jsx_runtime39.jsx)(
3052
+ /* @__PURE__ */ (0, import_jsx_runtime40.jsx)(
2918
3053
  "div",
2919
3054
  {
2920
3055
  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"}`,
2921
3056
  children: actions
2922
3057
  }
2923
3058
  ),
2924
- !isLoading && filterArtist && /* @__PURE__ */ (0, import_jsx_runtime39.jsx)(
3059
+ !isLoading && filterArtist && /* @__PURE__ */ (0, import_jsx_runtime40.jsx)(
2925
3060
  "div",
2926
3061
  {
2927
3062
  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"}`,
2928
3063
  children: renderArtistFilter()
2929
3064
  }
2930
3065
  ),
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" ? (
3066
+ /* @__PURE__ */ (0, import_jsx_runtime40.jsx)("div", { className: "content-container flex-1 min-h-0 flex flex-row gap-4", children: /* @__PURE__ */ (0, import_jsx_runtime40.jsxs)("div", { className: "tree-view-container relative flex-1 min-w-0 flex flex-col h-full", children: [
3067
+ !isLoading && viewMode === "outline" && floatingControlsLeading && /* @__PURE__ */ (0, import_jsx_runtime40.jsx)("div", { className: "absolute bottom-3 right-3 z-10", children: floatingControlsLeading }),
3068
+ isLoading ? /* @__PURE__ */ (0, import_jsx_runtime40.jsx)(import_genre_tree_view5.GenreTreeViewSkeleton, { viewMode }) : viewMode === "wheel" ? (
2934
3069
  // The info panel's top edge aligns with the tree's; the search bar floats `--layout-panel-inset` below it,
2935
3070
  // and the header inset (the bar's height plus that inset) extends the header's fill behind the bar,
2936
3071
  // Google Maps style; the 8px bottom padding mirrors the inset above the bar, and the hidden title's height is zeroed.
2937
3072
  // The panel's own Zoom in button is hidden: the search bar carries it.
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)(
3073
+ /* @__PURE__ */ (0, import_jsx_runtime40.jsx)("div", { className: "tree-container search-overlay relative flex-1 w-full min-h-0 ", children: /* @__PURE__ */ (0, import_jsx_runtime40.jsx)(GenreTreeWheelHandoff, { skeleton: /* @__PURE__ */ (0, import_jsx_runtime40.jsx)(import_genre_tree_view5.GenreTreeWheelSkeleton, {}), onRevealedChange: setIsWheelRevealed, children: /* @__PURE__ */ (0, import_jsx_runtime40.jsx)(
2939
3074
  GenrePlaylistTreeWheel,
2940
3075
  {
2941
3076
  scope,
@@ -2952,9 +3087,9 @@ function GenreTreeView({
2952
3087
  allowWheelRotation,
2953
3088
  sizeMetric: isArtistFiltered || !canSizeByViews ? "songs" : deferredSizeMetric,
2954
3089
  busy,
2955
- floatingControlsLeading: floatingControlsLeading || canSizeByViews ? /* @__PURE__ */ (0, import_jsx_runtime39.jsxs)("div", { className: "flex items-center gap-2", children: [
3090
+ floatingControlsLeading: floatingControlsLeading || canSizeByViews ? /* @__PURE__ */ (0, import_jsx_runtime40.jsxs)("div", { className: "flex items-center gap-2", children: [
2956
3091
  floatingControlsLeading,
2957
- canSizeByViews && /* @__PURE__ */ (0, import_jsx_runtime39.jsx)(SizeMetricToggle, { value: sizeMetric, onChange: changeSizeMetric })
3092
+ canSizeByViews && /* @__PURE__ */ (0, import_jsx_runtime40.jsx)(SizeMetricToggle, { value: sizeMetric, onChange: changeSizeMetric })
2958
3093
  ] }) : void 0,
2959
3094
  defaultViewport,
2960
3095
  viewportRestore,
@@ -2963,7 +3098,7 @@ function GenreTreeView({
2963
3098
  ) }) })
2964
3099
  ) : viewMode === "pop-core" ? (
2965
3100
  // Same as the wheel: the header inset extends its fill behind the floating search bar, Google Maps style.
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)(
3101
+ /* @__PURE__ */ (0, import_jsx_runtime40.jsx)("div", { className: "tree-container search-overlay relative flex-1 w-full min-h-0 ", children: /* @__PURE__ */ (0, import_jsx_runtime40.jsx)(GenreTreeWheelHandoff, { skeleton: /* @__PURE__ */ (0, import_jsx_runtime40.jsx)(import_genre_tree_view5.GenreTreeWheelSkeleton, {}), onRevealedChange: setIsWheelRevealed, children: /* @__PURE__ */ (0, import_jsx_runtime40.jsx)(
2967
3102
  GenrePlaylistTreeWheelRadialPopCore,
2968
3103
  {
2969
3104
  scope,
@@ -2980,9 +3115,9 @@ function GenreTreeView({
2980
3115
  allowWheelRotation,
2981
3116
  sizeMetric: isArtistFiltered || !canSizeByViews ? "songs" : deferredSizeMetric,
2982
3117
  busy,
2983
- floatingControlsLeading: floatingControlsLeading || canSizeByViews ? /* @__PURE__ */ (0, import_jsx_runtime39.jsxs)("div", { className: "flex items-center gap-2", children: [
3118
+ floatingControlsLeading: floatingControlsLeading || canSizeByViews ? /* @__PURE__ */ (0, import_jsx_runtime40.jsxs)("div", { className: "flex items-center gap-2", children: [
2984
3119
  floatingControlsLeading,
2985
- canSizeByViews && /* @__PURE__ */ (0, import_jsx_runtime39.jsx)(SizeMetricToggle, { value: sizeMetric, onChange: changeSizeMetric })
3120
+ canSizeByViews && /* @__PURE__ */ (0, import_jsx_runtime40.jsx)(SizeMetricToggle, { value: sizeMetric, onChange: changeSizeMetric })
2986
3121
  ] }) : void 0,
2987
3122
  defaultViewport,
2988
3123
  viewportRestore,
@@ -2990,7 +3125,7 @@ function GenreTreeView({
2990
3125
  ...focus
2991
3126
  }
2992
3127
  ) }) })
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)(
3128
+ ) : viewMode === "outline" ? /* @__PURE__ */ (0, import_jsx_runtime40.jsx)("div", { className: "tree-container flex-1 min-h-0 w-full overflow-y-auto relative", children: /* @__PURE__ */ (0, import_jsx_runtime40.jsx)(
2994
3129
  GenrePlaylistTreeWheelRadialPopCore,
2995
3130
  {
2996
3131
  outline: true,
@@ -3006,13 +3141,13 @@ function GenreTreeView({
3006
3141
  selectedNodeId,
3007
3142
  readOnly
3008
3143
  }
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(
3144
+ ) }) : /* @__PURE__ */ (0, import_jsx_runtime40.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(
3010
3145
  ([uuid, genrePlaylistTreePerRoot]) => {
3011
- return /* @__PURE__ */ (0, import_jsx_runtime39.jsx)(
3146
+ return /* @__PURE__ */ (0, import_jsx_runtime40.jsx)(
3012
3147
  "div",
3013
3148
  {
3014
3149
  className: "tree-per-root-container relative shrink-0 h-[28rem] mt-2 mr-16 p-2 bg-[var(--color-neutral-100)] rounded-lg",
3015
- children: /* @__PURE__ */ (0, import_jsx_runtime39.jsx)("div", { className: "graph-container relative z-10 h-full", children: /* @__PURE__ */ (0, import_jsx_runtime39.jsx)(
3150
+ children: /* @__PURE__ */ (0, import_jsx_runtime40.jsx)("div", { className: "graph-container relative z-10 h-full", children: /* @__PURE__ */ (0, import_jsx_runtime40.jsx)(
3016
3151
  GenrePlaylistTreePerRoot,
3017
3152
  {
3018
3153
  scope,
@@ -3038,8 +3173,8 @@ function GenreTreeView({
3038
3173
  );
3039
3174
  }
3040
3175
  ) }),
3041
- /* @__PURE__ */ (0, import_jsx_runtime39.jsx)(
3042
- import_ui16.DetailPanel,
3176
+ /* @__PURE__ */ (0, import_jsx_runtime40.jsx)(
3177
+ import_ui17.DetailPanel,
3043
3178
  {
3044
3179
  ref: panelRef,
3045
3180
  side: "left",
@@ -3049,13 +3184,13 @@ function GenreTreeView({
3049
3184
  headerAccent: activePanel?.headerAccent,
3050
3185
  headerActions: expandButton,
3051
3186
  onClose: activePanel?.onClose ?? noop,
3052
- collapsed: isCollapsed || !isTreeReady || !activePanel,
3187
+ collapsed: isCollapsed || !isPanelShown,
3053
3188
  expanded: activePanel?.expanded,
3054
3189
  onExpandedChange: activePanel?.onExpandedChange,
3055
3190
  aside: activePanel?.aside,
3056
3191
  edgeTab: detailTab,
3057
- headerClassName: activePanel?.compact ? "pt-[calc(var(--layout-search-h)+var(--layout-panel-inset)+0.5rem)]" : void 0,
3058
- children: activePanel?.aside && !activePanel.expanded ? /* @__PURE__ */ (0, import_jsx_runtime39.jsxs)(import_jsx_runtime39.Fragment, { children: [
3192
+ headerClassName: "pt-[calc(var(--layout-search-h)+var(--layout-panel-inset)+0.5rem)]",
3193
+ children: activePanel?.aside && !activePanel.expanded ? /* @__PURE__ */ (0, import_jsx_runtime40.jsxs)(import_jsx_runtime40.Fragment, { children: [
3059
3194
  activePanel.content,
3060
3195
  activePanel.aside
3061
3196
  ] }) : activePanel?.content
@@ -3067,134 +3202,6 @@ function GenreTreeView({
3067
3202
  );
3068
3203
  }
3069
3204
 
3070
- // src/genre-tree/GenreDetailPanel.tsx
3071
- var import_react18 = require("react");
3072
- var import_genre_tree_view5 = require("@behindthemusictree/genre-tree-view");
3073
- var import_ui17 = require("@behindthemusictree/ui");
3074
- var import_lucide_react13 = require("lucide-react");
3075
- var import_genre_tree28 = require("@behindthemusictree/app-kit/genre-tree");
3076
- var import_jsx_runtime40 = require("react/jsx-runtime");
3077
- var RELATIVE_ITEM_COUNT = 1;
3078
- function toNode(genre, parentId) {
3079
- return {
3080
- id: genre.uuid,
3081
- parentId,
3082
- name: genre.name,
3083
- itemCount: RELATIVE_ITEM_COUNT,
3084
- side: genre.side ?? void 0,
3085
- wikidataId: genre.wikidataId
3086
- };
3087
- }
3088
- function GenreDetailPanel({
3089
- genreUuid,
3090
- scope,
3091
- getBackendBaseUrl,
3092
- onClose,
3093
- onSelectGenre,
3094
- onZoomIn,
3095
- shareUrl,
3096
- shareTarget,
3097
- collapsed,
3098
- expanded,
3099
- onExpandedChange,
3100
- edgeTab,
3101
- extras
3102
- }) {
3103
- const { data: detail, isPending, isError } = (0, import_genre_tree28.useFetchGenreDetail)(genreUuid, scope, getBackendBaseUrl);
3104
- const playback = useGenrePlaylistPlayback({ scope, genrePlaylists: [] });
3105
- const view = (0, import_react18.useMemo)(() => {
3106
- if (!detail) return null;
3107
- const rootColor = (0, import_genre_tree_view5.getGenreTreeColor)(detail.root.wikidataId);
3108
- const relative = (genre, parentId) => ({
3109
- node: toNode(genre, parentId),
3110
- fill: (0, import_genre_tree_view5.tintSurface)(rootColor),
3111
- textColor: import_genre_tree_view5.TEXT_COLOR
3112
- });
3113
- const ancestors2 = [...detail.ascendants].sort((a, b) => b.degree - a.degree);
3114
- return {
3115
- node: {
3116
- id: detail.uuid,
3117
- parentId: detail.parent?.uuid ?? null,
3118
- name: detail.name,
3119
- itemCount: scope === "reference" ? (0, import_genre_tree28.genreMusicCount)(import_genre_tree28.GenrePlaylistSummarySchema.parse(detail.criteriaPlaylist)) : detail.tracksCount,
3120
- wikidataId: detail.wikidataId
3121
- },
3122
- fill: rootColor,
3123
- parentNode: detail.parent ? relative(detail.parent, null) : null,
3124
- childNodes: detail.children.map((child) => relative(child, detail.uuid)),
3125
- ancestorNodes: ancestors2.map(
3126
- ({ ascendant }, index) => relative(ascendant, index === 0 ? null : ancestors2[index - 1].ascendant.uuid)
3127
- )
3128
- };
3129
- }, [detail, scope]);
3130
- if (isError) {
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." });
3132
- }
3133
- if (isPending || !detail || !view) {
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" })
3138
- ] });
3139
- }
3140
- const selectGenre = (nodeId) => onSelectGenre(nodeId);
3141
- const family = /* @__PURE__ */ (0, import_jsx_runtime40.jsx)(
3142
- import_genre_tree_view5.GenreNodeFamily,
3143
- {
3144
- node: view.node,
3145
- fill: view.fill,
3146
- textColor: import_genre_tree_view5.ACCENT_TEXT_COLOR,
3147
- parentNode: view.parentNode,
3148
- childNodes: view.childNodes,
3149
- onSelectNode: selectGenre
3150
- }
3151
- );
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)(
3154
- import_ui17.DetailPanel,
3155
- {
3156
- side: "left",
3157
- title: view.node.name,
3158
- headerAccent: { background: view.fill, color: import_genre_tree_view5.ACCENT_TEXT_COLOR },
3159
- headerActions: onExpandedChange ? /* @__PURE__ */ (0, import_jsx_runtime40.jsx)(
3160
- import_ui17.IconButton,
3161
- {
3162
- "aria-label": expanded ? "Shrink panel" : "Extend panel",
3163
- style: { color: "inherit" },
3164
- onClick: () => onExpandedChange(!expanded),
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" })
3166
- }
3167
- ) : void 0,
3168
- onClose,
3169
- collapsed,
3170
- expanded,
3171
- onExpandedChange,
3172
- edgeTab,
3173
- children: [
3174
- /* @__PURE__ */ (0, import_jsx_runtime40.jsx)(
3175
- GenreInfoPanelBody,
3176
- {
3177
- family: /* @__PURE__ */ (0, import_jsx_runtime40.jsxs)(import_jsx_runtime40.Fragment, { children: [
3178
- family,
3179
- ancestors
3180
- ] }),
3181
- genreUuid: detail.uuid,
3182
- genrePlaylist: scope === "reference" ? import_genre_tree28.GenrePlaylistSummarySchema.parse(detail.criteriaPlaylist) : { ...detail.criteriaPlaylist, tracksCount: detail.tracksCount },
3183
- catalogue: detail.catalogue,
3184
- wikidataId: detail.wikidataId,
3185
- scope,
3186
- getBackendBaseUrl,
3187
- playback,
3188
- band: { node: view.node, onZoomIn, shareUrl, shareTarget: shareUrl ? shareTarget : void 0 },
3189
- extras
3190
- }
3191
- ),
3192
- !expanded && ancestors
3193
- ]
3194
- }
3195
- );
3196
- }
3197
-
3198
3205
  // src/genre-tree/index.ts
3199
3206
  var import_genre_tree_view6 = require("@behindthemusictree/genre-tree-view");
3200
3207