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