@behindthemusictree/app-kit-ui 34.1.0 → 35.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.cjs CHANGED
@@ -587,13 +587,13 @@ function AuthCallbackHandler({ renderStatus, ...options }) {
587
587
  }
588
588
 
589
589
  // src/genre-tree/GenreTreeView.tsx
590
- var import_react17 = require("react");
590
+ var import_react18 = require("react");
591
591
  var import_react_query3 = require("@tanstack/react-query");
592
- var import_lucide_react15 = require("lucide-react");
593
592
  var import_lucide_react16 = require("lucide-react");
594
- var import_ui19 = require("@behindthemusictree/ui");
595
- var import_genre_tree_view4 = require("@behindthemusictree/genre-tree-view");
596
- var import_genre_tree27 = require("@behindthemusictree/app-kit/genre-tree");
593
+ var import_lucide_react17 = require("lucide-react");
594
+ var import_ui20 = require("@behindthemusictree/ui");
595
+ var import_genre_tree_view5 = require("@behindthemusictree/genre-tree-view");
596
+ var import_genre_tree28 = require("@behindthemusictree/app-kit/genre-tree");
597
597
  var import_popup6 = require("@behindthemusictree/app-kit/popup");
598
598
 
599
599
  // src/genre-tree/playlist-tree/size-metric.ts
@@ -1240,6 +1240,7 @@ function GenreTreeSearch({
1240
1240
  artistSearch,
1241
1241
  songSearch,
1242
1242
  SearchGroups,
1243
+ onSelectGenre,
1243
1244
  extraGroups = [],
1244
1245
  onQueryChange,
1245
1246
  onFocus,
@@ -1265,9 +1266,16 @@ function GenreTreeSearch({
1265
1266
  extraGroups: [...extraGroups, ...hostGroups, artistGroup, songGroup].filter((group) => group !== void 0)
1266
1267
  }
1267
1268
  );
1268
- return SearchGroups ? /* @__PURE__ */ (0, import_jsx_runtime21.jsx)(SearchGroups, { query: deferredQuery, focused, children: render }) : render([]);
1269
+ return SearchGroups ? /* @__PURE__ */ (0, import_jsx_runtime21.jsx)(SearchGroups, { query: deferredQuery, focused, onSelectGenre, children: render }) : render([]);
1269
1270
  }
1270
1271
 
1272
+ // src/genre-tree/GenreDetailPanel.tsx
1273
+ var import_react14 = require("react");
1274
+ var import_genre_tree_view4 = require("@behindthemusictree/genre-tree-view");
1275
+ var import_ui10 = require("@behindthemusictree/ui");
1276
+ var import_lucide_react12 = require("lucide-react");
1277
+ var import_genre_tree19 = require("@behindthemusictree/app-kit/genre-tree");
1278
+
1271
1279
  // src/genre-tree/GenreInfoPanelBody.tsx
1272
1280
  var import_genre_tree18 = require("@behindthemusictree/app-kit/genre-tree");
1273
1281
  var import_track_noun4 = require("@behindthemusictree/app-kit/track-noun");
@@ -1689,6 +1697,7 @@ var nextCursor = (next) => {
1689
1697
  function GenrePlaylistTracks({
1690
1698
  genreUuid,
1691
1699
  genrePlaylist,
1700
+ genreName: selectedGenreName,
1692
1701
  selection,
1693
1702
  scope,
1694
1703
  getBackendBaseUrl,
@@ -1704,13 +1713,17 @@ function GenrePlaylistTracks({
1704
1713
  const { playerTrackObject, playState, handlePlayPauseAction } = (0, import_player4.usePlayer)();
1705
1714
  const { showPopup } = (0, import_popup5.usePopup)();
1706
1715
  const trackNoun = (0, import_track_noun2.useTrackNoun)();
1707
- const { uuid } = genrePlaylist;
1716
+ const uuid = genrePlaylist?.uuid;
1717
+ const genreName = selectedGenreName ?? genrePlaylist?.name ?? "Genre";
1708
1718
  const { fetch } = (0, import_transport5.useFetchWrapper)(getBackendBaseUrl);
1709
1719
  const playback = (0, import_transport5.usePlaybackContext)();
1710
1720
  const { data, isPending, isError, refetch, hasNextPage, isFetchingNextPage, fetchNextPage } = (0, import_react_query.useInfiniteQuery)({
1711
- queryKey: [...scope === "me" ? import_genre_tree12.genrePlaylistQueryKeys.me.tracks(uuid) : artist ? [...import_genre_tree13.libraryQueryKeys.reference.songs.byGenre(getBackendBaseUrl(), genreUuid, selection), "artist", artist.uuid] : import_genre_tree13.libraryQueryKeys.reference.songs.byGenre(getBackendBaseUrl(), genreUuid, selection), ...playback.key ? [playback.key] : []],
1721
+ 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] : []],
1712
1722
  queryFn: async ({ pageParam }) => {
1713
- if (scope === "me") return fetchGenrePlaylistTracksPage(uuid, scope, Number(pageParam));
1723
+ if (scope === "me") {
1724
+ if (!uuid) throw new Error("Personal genre has no playlist");
1725
+ return fetchGenrePlaylistTracksPage(uuid, scope, Number(pageParam));
1726
+ }
1714
1727
  const response = await fetch(
1715
1728
  import_genre_tree13.libraryEndpoints.reference.songs.list(),
1716
1729
  true,
@@ -1723,7 +1736,7 @@ function GenrePlaylistTracks({
1723
1736
  },
1724
1737
  initialPageParam: scope === "me" ? 1 : "",
1725
1738
  getNextPageParam: (lastPage) => lastPage.nextPage ?? void 0,
1726
- enabled: playback.ready && (scope !== "me" || sessionRestored && !!session?.accessToken)
1739
+ enabled: playback.ready && (scope !== "me" || uuid !== void 0 && sessionRestored && !!session?.accessToken)
1727
1740
  });
1728
1741
  const [sentinel, setSentinel] = (0, import_react12.useState)(null);
1729
1742
  (0, import_react12.useEffect)(() => {
@@ -1745,7 +1758,7 @@ function GenrePlaylistTracks({
1745
1758
  ordering: "title",
1746
1759
  genreUuid,
1747
1760
  ...selection && { selection }
1748
- }) : scope === "reference" ? playNewTrackListFromGenreCatalogue({ uuid: genreUuid, name: genrePlaylist.name }, scope, { startSongUuid, ...selection && { selection } }) : playNewTrackListFromGenrePlaylist(genrePlaylist, scope, { startSongUuid });
1761
+ }) : 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"));
1749
1762
  started.catch((error) => {
1750
1763
  console.error("Failed to play genre playlist:", error);
1751
1764
  showPopup(/* @__PURE__ */ (0, import_jsx_runtime29.jsx)(InternalErrorPopup, { errorCode: import_transport8.ErrorCode.CLIENT_UNKNOWN }));
@@ -1753,13 +1766,13 @@ function GenrePlaylistTracks({
1753
1766
  };
1754
1767
  return /* @__PURE__ */ (0, import_jsx_runtime29.jsx)(import_ui8.PanelSection, { title: `${(0, import_track_noun2.capitalize)(trackNoun.plural)}${total !== void 0 ? ` (${total})` : ""}`, children: /* @__PURE__ */ (0, import_jsx_runtime29.jsxs)("div", { children: [
1755
1768
  renderTracksToolbar?.(),
1756
- isPending ? /* @__PURE__ */ (0, import_jsx_runtime29.jsx)(import_ui8.Skeleton, { className: "h-4 w-3/4", "aria-busy": "true" }) : isError && tracks.length === 0 ? /* @__PURE__ */ (0, import_jsx_runtime29.jsxs)("p", { role: "alert", children: [
1769
+ scope === "me" && !genrePlaylist ? /* @__PURE__ */ (0, import_jsx_runtime29.jsx)("p", { children: "\u2014" }) : isPending ? /* @__PURE__ */ (0, import_jsx_runtime29.jsx)(import_ui8.Skeleton, { className: "h-4 w-3/4", "aria-busy": "true" }) : isError && tracks.length === 0 ? /* @__PURE__ */ (0, import_jsx_runtime29.jsxs)("p", { role: "alert", children: [
1757
1770
  "Couldn't load ",
1758
1771
  trackNoun.plural,
1759
1772
  ".",
1760
1773
  " ",
1761
1774
  /* @__PURE__ */ (0, import_jsx_runtime29.jsx)("button", { type: "button", className: "underline", onClick: () => void refetch(), children: "Retry" })
1762
- ] }) : tracks.length === 0 ? /* @__PURE__ */ (0, import_jsx_runtime29.jsx)("p", { children: "\u2014" }) : /* @__PURE__ */ (0, import_jsx_runtime29.jsxs)("ul", { className: "mt-1 max-h-64 overflow-y-auto text-[0.8125rem] leading-[1.45] [container-type:inline-size]", "aria-label": `${genrePlaylist.name} ${trackNoun.plural}`, children: [
1775
+ ] }) : tracks.length === 0 ? /* @__PURE__ */ (0, import_jsx_runtime29.jsx)("p", { children: "\u2014" }) : /* @__PURE__ */ (0, import_jsx_runtime29.jsxs)("ul", { className: "mt-1 max-h-64 overflow-y-auto text-[0.8125rem] leading-[1.45] [container-type:inline-size]", "aria-label": `${genreName} ${trackNoun.plural}`, children: [
1763
1776
  tracks.map((track, index) => {
1764
1777
  const label = track.artists?.length ? `${track.title} \u2014 ${track.artists.map((artist2) => artist2.name).join(", ")}` : track.title;
1765
1778
  const isCurrent = playerTrackObject?.track.id === track.uuid;
@@ -1826,6 +1839,7 @@ var import_jsx_runtime30 = require("react/jsx-runtime");
1826
1839
  function GenreDetailExtras({
1827
1840
  genreUuid,
1828
1841
  genrePlaylist,
1842
+ genreName,
1829
1843
  selection,
1830
1844
  wikidataId,
1831
1845
  scope,
@@ -1893,6 +1907,7 @@ function GenreDetailExtras({
1893
1907
  {
1894
1908
  genreUuid,
1895
1909
  genrePlaylist,
1910
+ genreName,
1896
1911
  selection,
1897
1912
  scope,
1898
1913
  getBackendBaseUrl,
@@ -1934,6 +1949,7 @@ function GenreInfoPanelBody({
1934
1949
  genreUuid,
1935
1950
  genrePlaylist,
1936
1951
  catalogue,
1952
+ countsContext,
1937
1953
  wikidataId,
1938
1954
  scope,
1939
1955
  getBackendBaseUrl,
@@ -1942,7 +1958,7 @@ function GenreInfoPanelBody({
1942
1958
  extras
1943
1959
  }) {
1944
1960
  const noun = (0, import_track_noun4.useTrackNoun)();
1945
- const summary = scope === "reference" ? import_genre_tree18.GenreCatalogueCountsSchema.parse(genrePlaylist) : void 0;
1961
+ const summary = scope === "reference" ? import_genre_tree18.GenreCatalogueCountsSchema.parse(genrePlaylist ?? catalogue) : void 0;
1946
1962
  const selection = summary ? (0, import_genre_tree18.defaultGenreMusicSelection)(summary) : void 0;
1947
1963
  const describeCounts = (counts) => {
1948
1964
  const scopes = counts.genreAndSubgenresTracksCount;
@@ -1950,17 +1966,19 @@ function GenreInfoPanelBody({
1950
1966
  };
1951
1967
  return /* @__PURE__ */ (0, import_jsx_runtime31.jsxs)(import_jsx_runtime31.Fragment, { children: [
1952
1968
  toolbar,
1953
- /* @__PURE__ */ (0, import_jsx_runtime31.jsx)(GenreNodeActionBand, { ...band, genrePlaylist, playback }),
1969
+ genrePlaylist ? /* @__PURE__ */ (0, import_jsx_runtime31.jsx)(GenreNodeActionBand, { ...band, genrePlaylist, playback }) : /* @__PURE__ */ (0, import_jsx_runtime31.jsx)(GenreActionBand, { ...band }),
1954
1970
  summary && catalogue && /* @__PURE__ */ (0, import_jsx_runtime31.jsxs)("div", { className: "px-3.5 py-3 text-sm text-[var(--color-text)]", children: [
1955
1971
  /* @__PURE__ */ (0, import_jsx_runtime31.jsxs)("p", { children: [
1956
1972
  "Catalogue ",
1957
1973
  noun.plural,
1974
+ countsContext ? ` (${countsContext})` : "",
1958
1975
  ": ",
1959
1976
  describeCounts(catalogue)
1960
1977
  ] }),
1961
- /* @__PURE__ */ (0, import_jsx_runtime31.jsxs)("p", { children: [
1978
+ genrePlaylist && /* @__PURE__ */ (0, import_jsx_runtime31.jsxs)("p", { children: [
1962
1979
  "Playlist ",
1963
1980
  noun.plural,
1981
+ countsContext ? ` (${countsContext})` : "",
1964
1982
  ": ",
1965
1983
  describeCounts(summary)
1966
1984
  ] })
@@ -1973,6 +1991,7 @@ function GenreInfoPanelBody({
1973
1991
  family,
1974
1992
  genreUuid,
1975
1993
  genrePlaylist,
1994
+ genreName: band.node.name,
1976
1995
  selection,
1977
1996
  wikidataId,
1978
1997
  scope,
@@ -1982,10 +2001,160 @@ function GenreInfoPanelBody({
1982
2001
  ] });
1983
2002
  }
1984
2003
 
1985
- // src/genre-tree/ChangeParentPopup.tsx
1986
- var import_react14 = require("react");
1987
- var import_genre_tree19 = require("@behindthemusictree/app-kit/genre-tree");
2004
+ // src/genre-tree/GenreDetailPanel.tsx
1988
2005
  var import_jsx_runtime32 = require("react/jsx-runtime");
2006
+ var RELATIVE_ITEM_COUNT = 1;
2007
+ function toNode(genre, parentId) {
2008
+ return {
2009
+ id: genre.uuid,
2010
+ parentId,
2011
+ name: genre.name,
2012
+ itemCount: RELATIVE_ITEM_COUNT,
2013
+ side: genre.side ?? void 0,
2014
+ wikidataId: genre.wikidataId
2015
+ };
2016
+ }
2017
+ function useGenreDetailContent({
2018
+ genreUuid,
2019
+ scope,
2020
+ getBackendBaseUrl,
2021
+ onSelectGenre,
2022
+ extras,
2023
+ band,
2024
+ playback: hostPlayback,
2025
+ trailing,
2026
+ genrePlaylist: hostPlaylist,
2027
+ expanded
2028
+ }) {
2029
+ const { data: detail, isPending, isError } = (0, import_genre_tree19.useFetchGenreDetail)(genreUuid, scope, getBackendBaseUrl);
2030
+ const defaultPlayback = useGenrePlaylistPlayback({ scope, genrePlaylists: [], artist: extras?.artist });
2031
+ const playback = hostPlayback ?? defaultPlayback;
2032
+ const view = (0, import_react14.useMemo)(() => {
2033
+ if (!detail) return null;
2034
+ const rootColor = (0, import_genre_tree_view4.getGenreTreeColor)(detail.root.wikidataId);
2035
+ const relative = (genre, parentId) => ({
2036
+ node: toNode(genre, parentId),
2037
+ fill: (0, import_genre_tree_view4.tintSurface)(rootColor),
2038
+ textColor: import_genre_tree_view4.TEXT_COLOR
2039
+ });
2040
+ const ancestors2 = [...detail.ascendants].sort((a, b) => b.degree - a.degree);
2041
+ return {
2042
+ node: {
2043
+ id: detail.uuid,
2044
+ parentId: detail.parent?.uuid ?? null,
2045
+ name: detail.name,
2046
+ 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,
2047
+ wikidataId: detail.wikidataId
2048
+ },
2049
+ fill: rootColor,
2050
+ parentNode: detail.parent ? relative(detail.parent, null) : null,
2051
+ childNodes: detail.children.map((child) => relative(child, detail.uuid)),
2052
+ ancestorNodes: ancestors2.map(
2053
+ ({ ascendant }, index) => relative(ascendant, index === 0 ? null : ancestors2[index - 1].ascendant.uuid)
2054
+ )
2055
+ };
2056
+ }, [detail, scope]);
2057
+ if (isError) return {
2058
+ title: "Genre unavailable",
2059
+ content: /* @__PURE__ */ (0, import_jsx_runtime32.jsx)("p", { role: "alert", className: "p-4 text-sm text-[var(--color-text)]", children: "This genre could not be loaded." })
2060
+ };
2061
+ if (isPending || !detail || !view) return {
2062
+ title: "Loading genre\u2026",
2063
+ content: /* @__PURE__ */ (0, import_jsx_runtime32.jsxs)("div", { className: "flex flex-col gap-3 p-4", "aria-busy": "true", "data-testid": "genre-detail-panel-skeleton", children: [
2064
+ /* @__PURE__ */ (0, import_jsx_runtime32.jsx)(import_ui10.Skeleton, { className: "h-6 w-2/3" }),
2065
+ /* @__PURE__ */ (0, import_jsx_runtime32.jsx)(import_ui10.Skeleton, { className: "h-4 w-full" }),
2066
+ /* @__PURE__ */ (0, import_jsx_runtime32.jsx)(import_ui10.Skeleton, { className: "h-4 w-3/4" })
2067
+ ] })
2068
+ };
2069
+ const selectGenre = (nodeId) => onSelectGenre(import_genre_tree19.GenreUuidSchema.parse(nodeId));
2070
+ const family = /* @__PURE__ */ (0, import_jsx_runtime32.jsx)(
2071
+ import_genre_tree_view4.GenreNodeFamily,
2072
+ {
2073
+ node: view.node,
2074
+ fill: view.fill,
2075
+ textColor: import_genre_tree_view4.ACCENT_TEXT_COLOR,
2076
+ parentNode: view.parentNode,
2077
+ childNodes: view.childNodes,
2078
+ onSelectNode: selectGenre
2079
+ }
2080
+ );
2081
+ const ancestors = /* @__PURE__ */ (0, import_jsx_runtime32.jsx)(import_genre_tree_view4.GenreNodeAncestors, { ancestorNodes: view.ancestorNodes, onSelectNode: selectGenre });
2082
+ return {
2083
+ title: detail.name,
2084
+ headerAccent: { background: view.fill, color: import_genre_tree_view4.ACCENT_TEXT_COLOR },
2085
+ aside: expanded ? ancestors : void 0,
2086
+ content: /* @__PURE__ */ (0, import_jsx_runtime32.jsxs)(import_jsx_runtime32.Fragment, { children: [
2087
+ /* @__PURE__ */ (0, import_jsx_runtime32.jsx)(
2088
+ GenreInfoPanelBody,
2089
+ {
2090
+ family: /* @__PURE__ */ (0, import_jsx_runtime32.jsxs)(import_jsx_runtime32.Fragment, { children: [
2091
+ family,
2092
+ !expanded && ancestors
2093
+ ] }),
2094
+ genreUuid: detail.uuid,
2095
+ 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 }),
2096
+ catalogue: detail.catalogue,
2097
+ countsContext: !hostPlaylist && extras?.artist ? "all artists" : void 0,
2098
+ wikidataId: detail.wikidataId,
2099
+ scope,
2100
+ getBackendBaseUrl,
2101
+ playback,
2102
+ band: { node: view.node, ...band, shareTarget: band?.shareTarget?.kind === "genre" ? { ...band.shareTarget, name: detail.name } : band?.shareTarget },
2103
+ extras
2104
+ }
2105
+ ),
2106
+ trailing
2107
+ ] })
2108
+ };
2109
+ }
2110
+ function GenreDetailPanel({
2111
+ genreUuid,
2112
+ scope,
2113
+ getBackendBaseUrl,
2114
+ onClose,
2115
+ onSelectGenre,
2116
+ onZoomIn,
2117
+ shareUrl,
2118
+ shareTarget,
2119
+ collapsed,
2120
+ expanded,
2121
+ onExpandedChange,
2122
+ edgeTab,
2123
+ extras,
2124
+ headerClassName
2125
+ }) {
2126
+ const panel = useGenreDetailContent({
2127
+ genreUuid,
2128
+ scope,
2129
+ getBackendBaseUrl,
2130
+ onSelectGenre,
2131
+ extras,
2132
+ expanded,
2133
+ band: { onZoomIn, shareUrl, shareTarget: shareUrl ? shareTarget : void 0 }
2134
+ });
2135
+ return /* @__PURE__ */ (0, import_jsx_runtime32.jsx)(
2136
+ import_ui10.DetailPanel,
2137
+ {
2138
+ side: "left",
2139
+ title: panel.title,
2140
+ headerAccent: panel.headerAccent,
2141
+ onClose,
2142
+ collapsed,
2143
+ expanded,
2144
+ onExpandedChange,
2145
+ edgeTab,
2146
+ aside: panel.aside,
2147
+ headerClassName,
2148
+ headerActions: onExpandedChange ? /* @__PURE__ */ (0, import_jsx_runtime32.jsx)(import_ui10.IconButton, { "aria-label": expanded ? "Shrink panel" : "Extend panel", onClick: () => onExpandedChange(!expanded), children: expanded ? /* @__PURE__ */ (0, import_jsx_runtime32.jsx)(import_lucide_react12.Minimize2, { "aria-hidden": "true", className: "h-4 w-4" }) : /* @__PURE__ */ (0, import_jsx_runtime32.jsx)(import_lucide_react12.Maximize2, { "aria-hidden": "true", className: "h-4 w-4" }) }) : void 0,
2149
+ children: panel.content
2150
+ }
2151
+ );
2152
+ }
2153
+
2154
+ // src/genre-tree/ChangeParentPopup.tsx
2155
+ var import_react15 = require("react");
2156
+ var import_genre_tree20 = require("@behindthemusictree/app-kit/genre-tree");
2157
+ var import_jsx_runtime33 = require("react/jsx-runtime");
1989
2158
  function ChangeParentPopup({
1990
2159
  genrePlaylist,
1991
2160
  genrePlaylists,
@@ -1994,9 +2163,9 @@ function ChangeParentPopup({
1994
2163
  onClose,
1995
2164
  onParentChanged
1996
2165
  }) {
1997
- const { mutate, isPending, formErrors } = (0, import_genre_tree19.useUpdateGenre)(scope, getBackendBaseUrl);
1998
- const candidates = (0, import_react14.useMemo)(
1999
- () => (0, import_genre_tree19.getReparentCandidates)(genrePlaylists, genrePlaylist.uuid),
2166
+ const { mutate, isPending, formErrors } = (0, import_genre_tree20.useUpdateGenre)(scope, getBackendBaseUrl);
2167
+ const candidates = (0, import_react15.useMemo)(
2168
+ () => (0, import_genre_tree20.getReparentCandidates)(genrePlaylists, genrePlaylist.uuid),
2000
2169
  [genrePlaylists, genrePlaylist.uuid]
2001
2170
  );
2002
2171
  const handleSelect = (newParent) => {
@@ -2013,19 +2182,19 @@ function ChangeParentPopup({
2013
2182
  }
2014
2183
  );
2015
2184
  };
2016
- (0, import_react14.useEffect)(() => {
2185
+ (0, import_react15.useEffect)(() => {
2017
2186
  const handleKeyDown = (event) => {
2018
2187
  if (event.key === "Escape") onClose();
2019
2188
  };
2020
2189
  window.addEventListener("keydown", handleKeyDown);
2021
2190
  return () => window.removeEventListener("keydown", handleKeyDown);
2022
2191
  }, [onClose]);
2023
- return /* @__PURE__ */ (0, import_jsx_runtime32.jsxs)(BasePopup_default, { title: `Change parent of ${genrePlaylist.name}`, onClose, showCancelButton: true, fitContent: true, children: [
2024
- /* @__PURE__ */ (0, import_jsx_runtime32.jsxs)("p", { className: "mb-3 text-sm text-[var(--color-text-muted)]", children: [
2192
+ return /* @__PURE__ */ (0, import_jsx_runtime33.jsxs)(BasePopup_default, { title: `Change parent of ${genrePlaylist.name}`, onClose, showCancelButton: true, fitContent: true, children: [
2193
+ /* @__PURE__ */ (0, import_jsx_runtime33.jsxs)("p", { className: "mb-3 text-sm text-[var(--color-text-muted)]", children: [
2025
2194
  "Current parent: ",
2026
2195
  genrePlaylist.parent?.name ?? "none"
2027
2196
  ] }),
2028
- /* @__PURE__ */ (0, import_jsx_runtime32.jsx)("div", { "aria-busy": isPending, className: isPending ? "pointer-events-none opacity-60" : void 0, children: /* @__PURE__ */ (0, import_jsx_runtime32.jsx)(
2197
+ /* @__PURE__ */ (0, import_jsx_runtime33.jsx)("div", { "aria-busy": isPending, className: isPending ? "pointer-events-none opacity-60" : void 0, children: /* @__PURE__ */ (0, import_jsx_runtime33.jsx)(
2029
2198
  GenreSearch,
2030
2199
  {
2031
2200
  genrePlaylists: candidates,
@@ -2033,24 +2202,24 @@ function ChangeParentPopup({
2033
2202
  placeholder: "Search a new parent\u2026"
2034
2203
  }
2035
2204
  ) }),
2036
- formErrors.length > 0 && /* @__PURE__ */ (0, import_jsx_runtime32.jsx)("p", { role: "alert", className: "mt-3 text-sm text-[var(--color-red-500)]", children: formErrors.map(({ message }) => message).join(" ") })
2205
+ formErrors.length > 0 && /* @__PURE__ */ (0, import_jsx_runtime33.jsx)("p", { role: "alert", className: "mt-3 text-sm text-[var(--color-red-500)]", children: formErrors.map(({ message }) => message).join(" ") })
2037
2206
  ] });
2038
2207
  }
2039
2208
 
2040
2209
  // src/genre-tree/artist/useArtistPanel.tsx
2041
- var import_ui15 = require("@behindthemusictree/ui");
2042
- var import_genre_tree24 = require("@behindthemusictree/app-kit/genre-tree");
2210
+ var import_ui16 = require("@behindthemusictree/ui");
2211
+ var import_genre_tree25 = require("@behindthemusictree/app-kit/genre-tree");
2043
2212
 
2044
2213
  // src/genre-tree/artist/ArtistHeader.tsx
2045
- var import_ui11 = require("@behindthemusictree/ui");
2214
+ var import_ui12 = require("@behindthemusictree/ui");
2046
2215
 
2047
2216
  // src/genre-tree/artist/ArtistLinks.tsx
2048
- var import_lucide_react12 = require("lucide-react");
2217
+ var import_lucide_react13 = require("lucide-react");
2049
2218
  var import_si3 = require("react-icons/si");
2050
- var import_ui10 = require("@behindthemusictree/ui");
2051
- var import_jsx_runtime33 = require("react/jsx-runtime");
2219
+ var import_ui11 = require("@behindthemusictree/ui");
2220
+ var import_jsx_runtime34 = require("react/jsx-runtime");
2052
2221
  var LINK_KINDS = {
2053
- official: { label: "Official website", Icon: import_lucide_react12.Globe },
2222
+ official: { label: "Official website", Icon: import_lucide_react13.Globe },
2054
2223
  wikipedia: { label: "Wikipedia", Icon: import_si3.SiWikipedia },
2055
2224
  wikidata: { label: "Wikidata", Icon: import_si3.SiWikidata },
2056
2225
  spotify: { label: "Spotify", Icon: import_si3.SiSpotify },
@@ -2058,7 +2227,7 @@ var LINK_KINDS = {
2058
2227
  bandcamp: { label: "Bandcamp", Icon: import_si3.SiBandcamp },
2059
2228
  soundcloud: { label: "SoundCloud", Icon: import_si3.SiSoundcloud },
2060
2229
  discogs: { label: "Discogs", Icon: import_si3.SiDiscogs },
2061
- social: { label: "Social", Icon: import_lucide_react12.Share2 }
2230
+ social: { label: "Social", Icon: import_lucide_react13.Share2 }
2062
2231
  };
2063
2232
  function socialLabel(url) {
2064
2233
  try {
@@ -2085,7 +2254,7 @@ function artistLinks(artist) {
2085
2254
  return links;
2086
2255
  }
2087
2256
  function ArtistLinks({ links }) {
2088
- return /* @__PURE__ */ (0, import_jsx_runtime33.jsx)("ul", { className: "flex flex-wrap gap-1.5", children: links.map(({ key, url, Icon, label }) => /* @__PURE__ */ (0, import_jsx_runtime33.jsx)("li", { children: /* @__PURE__ */ (0, import_jsx_runtime33.jsx)(import_ui10.Chip, { href: url, icon: /* @__PURE__ */ (0, import_jsx_runtime33.jsx)(Icon, { className: "h-3.5 w-3.5 shrink-0", "aria-hidden": "true" }), children: label }) }, key)) });
2257
+ return /* @__PURE__ */ (0, import_jsx_runtime34.jsx)("ul", { className: "flex flex-wrap gap-1.5", children: links.map(({ key, url, Icon, label }) => /* @__PURE__ */ (0, import_jsx_runtime34.jsx)("li", { children: /* @__PURE__ */ (0, import_jsx_runtime34.jsx)(import_ui11.Chip, { href: url, icon: /* @__PURE__ */ (0, import_jsx_runtime34.jsx)(Icon, { className: "h-3.5 w-3.5 shrink-0", "aria-hidden": "true" }), children: label }) }, key)) });
2089
2258
  }
2090
2259
 
2091
2260
  // src/genre-tree/artist/formatYears.ts
@@ -2096,7 +2265,7 @@ function formatYears(beginYear, endYear, ended) {
2096
2265
  }
2097
2266
 
2098
2267
  // src/genre-tree/artist/ArtistHeader.tsx
2099
- var import_jsx_runtime34 = require("react/jsx-runtime");
2268
+ var import_jsx_runtime35 = require("react/jsx-runtime");
2100
2269
  var countryNames = new Intl.DisplayNames(["en"], { type: "region" });
2101
2270
  function ArtistHeader({ artist }) {
2102
2271
  const fields = [];
@@ -2106,35 +2275,35 @@ function ArtistHeader({ artist }) {
2106
2275
  const years = formatYears(artist.beginYear, artist.endYear, artist.ended);
2107
2276
  if (years) fields.push({ label: "Active", value: years });
2108
2277
  const links = artistLinks(artist);
2109
- if (links.length > 0) fields.push({ label: "Links", value: /* @__PURE__ */ (0, import_jsx_runtime34.jsx)(ArtistLinks, { links }) });
2278
+ if (links.length > 0) fields.push({ label: "Links", value: /* @__PURE__ */ (0, import_jsx_runtime35.jsx)(ArtistLinks, { links }) });
2110
2279
  if (fields.length === 0) return null;
2111
- return /* @__PURE__ */ (0, import_jsx_runtime34.jsx)(import_ui11.FieldList, { items: fields });
2280
+ return /* @__PURE__ */ (0, import_jsx_runtime35.jsx)(import_ui12.FieldList, { items: fields });
2112
2281
  }
2113
2282
 
2114
2283
  // src/genre-tree/artist/ArtistPhoto.tsx
2115
- var import_jsx_runtime35 = require("react/jsx-runtime");
2284
+ var import_jsx_runtime36 = require("react/jsx-runtime");
2116
2285
  function ExternalLink({ href, children }) {
2117
- return /* @__PURE__ */ (0, import_jsx_runtime35.jsx)("a", { href, target: "_blank", rel: "noopener noreferrer", className: "underline-offset-2 hover:underline", children });
2286
+ return /* @__PURE__ */ (0, import_jsx_runtime36.jsx)("a", { href, target: "_blank", rel: "noopener noreferrer", className: "underline-offset-2 hover:underline", children });
2118
2287
  }
2119
2288
  function ArtistPhoto({ image, name }) {
2120
- return /* @__PURE__ */ (0, import_jsx_runtime35.jsxs)("figure", { className: "flex flex-col gap-1.5 px-6 py-2.5", children: [
2121
- /* @__PURE__ */ (0, import_jsx_runtime35.jsx)("img", { src: image.url, alt: name, className: "h-64 w-full object-contain bg-[var(--color-surface-muted)] rounded" }),
2122
- /* @__PURE__ */ (0, import_jsx_runtime35.jsxs)("figcaption", { className: "text-xs text-[var(--color-text-muted)]", children: [
2289
+ return /* @__PURE__ */ (0, import_jsx_runtime36.jsxs)("figure", { className: "flex flex-col gap-1.5 px-6 py-2.5", children: [
2290
+ /* @__PURE__ */ (0, import_jsx_runtime36.jsx)("img", { src: image.url, alt: name, className: "h-64 w-full object-contain bg-[var(--color-surface-muted)] rounded" }),
2291
+ /* @__PURE__ */ (0, import_jsx_runtime36.jsxs)("figcaption", { className: "text-xs text-[var(--color-text-muted)]", children: [
2123
2292
  "Photo: ",
2124
- /* @__PURE__ */ (0, import_jsx_runtime35.jsx)(ExternalLink, { href: image.pageUrl, children: image.author ?? "Wikimedia Commons" }),
2293
+ /* @__PURE__ */ (0, import_jsx_runtime36.jsx)(ExternalLink, { href: image.pageUrl, children: image.author ?? "Wikimedia Commons" }),
2125
2294
  ",",
2126
2295
  " ",
2127
- image.licenseUrl ? /* @__PURE__ */ (0, import_jsx_runtime35.jsx)(ExternalLink, { href: image.licenseUrl, children: image.license }) : image.license
2296
+ image.licenseUrl ? /* @__PURE__ */ (0, import_jsx_runtime36.jsx)(ExternalLink, { href: image.licenseUrl, children: image.license }) : image.license
2128
2297
  ] })
2129
2298
  ] });
2130
2299
  }
2131
2300
 
2132
2301
  // src/genre-tree/artist/ArtistMembers.tsx
2133
- var import_ui12 = require("@behindthemusictree/ui");
2134
- var import_jsx_runtime36 = require("react/jsx-runtime");
2302
+ var import_ui13 = require("@behindthemusictree/ui");
2303
+ var import_jsx_runtime37 = require("react/jsx-runtime");
2135
2304
  function MembershipList({ title, memberships, renderArtistLink }) {
2136
2305
  if (memberships.length === 0) return null;
2137
- return /* @__PURE__ */ (0, import_jsx_runtime36.jsx)(import_ui12.PanelGroup, { title, children: /* @__PURE__ */ (0, import_jsx_runtime36.jsx)("ul", { children: memberships.map((membership, index) => {
2306
+ return /* @__PURE__ */ (0, import_jsx_runtime37.jsx)(import_ui13.PanelGroup, { title, children: /* @__PURE__ */ (0, import_jsx_runtime37.jsx)("ul", { children: memberships.map((membership, index) => {
2138
2307
  const details = [
2139
2308
  formatYears(membership.beginYear, membership.endYear, membership.ended),
2140
2309
  ...membership.roles
@@ -2142,9 +2311,9 @@ function MembershipList({ title, memberships, renderArtistLink }) {
2142
2311
  const detailsText = details.filter(Boolean).join(" \xB7 ");
2143
2312
  return (
2144
2313
  // A member can have several periods in the same group.
2145
- /* @__PURE__ */ (0, import_jsx_runtime36.jsxs)("li", { className: "truncate py-0.5", title: detailsText || void 0, children: [
2146
- /* @__PURE__ */ (0, import_jsx_runtime36.jsx)("span", { className: "font-medium text-[var(--color-text)]", children: renderArtistLink(membership.artist) }),
2147
- detailsText && /* @__PURE__ */ (0, import_jsx_runtime36.jsxs)("span", { className: "text-[var(--color-text-muted)]", children: [
2314
+ /* @__PURE__ */ (0, import_jsx_runtime37.jsxs)("li", { className: "truncate py-0.5", title: detailsText || void 0, children: [
2315
+ /* @__PURE__ */ (0, import_jsx_runtime37.jsx)("span", { className: "font-medium text-[var(--color-text)]", children: renderArtistLink(membership.artist) }),
2316
+ detailsText && /* @__PURE__ */ (0, import_jsx_runtime37.jsxs)("span", { className: "text-[var(--color-text-muted)]", children: [
2148
2317
  " \u2014 ",
2149
2318
  detailsText
2150
2319
  ] })
@@ -2155,9 +2324,9 @@ function MembershipList({ title, memberships, renderArtistLink }) {
2155
2324
  function ArtistMembers({ title, memberships, renderArtistLink }) {
2156
2325
  if (memberships.length === 0) return null;
2157
2326
  const isCurrent = (membership) => !membership.ended && membership.endYear === null;
2158
- return /* @__PURE__ */ (0, import_jsx_runtime36.jsxs)(import_ui12.PanelSection, { title, children: [
2159
- /* @__PURE__ */ (0, import_jsx_runtime36.jsx)(MembershipList, { title: "Current", memberships: memberships.filter(isCurrent), renderArtistLink }),
2160
- /* @__PURE__ */ (0, import_jsx_runtime36.jsx)(
2327
+ return /* @__PURE__ */ (0, import_jsx_runtime37.jsxs)(import_ui13.PanelSection, { title, children: [
2328
+ /* @__PURE__ */ (0, import_jsx_runtime37.jsx)(MembershipList, { title: "Current", memberships: memberships.filter(isCurrent), renderArtistLink }),
2329
+ /* @__PURE__ */ (0, import_jsx_runtime37.jsx)(
2161
2330
  MembershipList,
2162
2331
  {
2163
2332
  title: "Past",
@@ -2169,8 +2338,8 @@ function ArtistMembers({ title, memberships, renderArtistLink }) {
2169
2338
  }
2170
2339
 
2171
2340
  // src/genre-tree/artist/ArtistDiscography.tsx
2172
- var import_ui13 = require("@behindthemusictree/ui");
2173
- var import_jsx_runtime37 = require("react/jsx-runtime");
2341
+ var import_ui14 = require("@behindthemusictree/ui");
2342
+ var import_jsx_runtime38 = require("react/jsx-runtime");
2174
2343
  var GROUPS = ["Albums", "EPs", "Singles", "Live", "Compilations", "Other"];
2175
2344
  var PRIMARY_GROUPS = { album: "Albums", ep: "EPs", single: "Singles" };
2176
2345
  function discographyGroup({ primaryType, secondaryTypes }) {
@@ -2190,34 +2359,34 @@ function byYear(a, b) {
2190
2359
  function ArtistDiscography({ albums }) {
2191
2360
  if (albums.length === 0) return null;
2192
2361
  const sorted = [...albums].sort(byYear);
2193
- return /* @__PURE__ */ (0, import_jsx_runtime37.jsx)(import_ui13.PanelSection, { title: "Discography", children: GROUPS.map((group) => {
2362
+ return /* @__PURE__ */ (0, import_jsx_runtime38.jsx)(import_ui14.PanelSection, { title: "Discography", children: GROUPS.map((group) => {
2194
2363
  const groupAlbums = sorted.filter((album) => discographyGroup(album) === group);
2195
2364
  if (groupAlbums.length === 0) return null;
2196
- return /* @__PURE__ */ (0, import_jsx_runtime37.jsx)(import_ui13.PanelGroup, { title: group, children: /* @__PURE__ */ (0, import_jsx_runtime37.jsx)("ul", { children: groupAlbums.map((album) => /* @__PURE__ */ (0, import_jsx_runtime37.jsxs)("li", { className: "grid grid-cols-[2.5rem_minmax(0,1fr)] gap-2 py-0.5", children: [
2197
- /* @__PURE__ */ (0, import_jsx_runtime37.jsx)("span", { className: "tabular-nums text-[var(--color-text-muted)]", children: album.year ?? "\u2014" }),
2198
- /* @__PURE__ */ (0, import_jsx_runtime37.jsx)("span", { className: "truncate text-[var(--color-text)]", title: album.name, children: album.name })
2365
+ return /* @__PURE__ */ (0, import_jsx_runtime38.jsx)(import_ui14.PanelGroup, { title: group, children: /* @__PURE__ */ (0, import_jsx_runtime38.jsx)("ul", { children: groupAlbums.map((album) => /* @__PURE__ */ (0, import_jsx_runtime38.jsxs)("li", { className: "grid grid-cols-[2.5rem_minmax(0,1fr)] gap-2 py-0.5", children: [
2366
+ /* @__PURE__ */ (0, import_jsx_runtime38.jsx)("span", { className: "tabular-nums text-[var(--color-text-muted)]", children: album.year ?? "\u2014" }),
2367
+ /* @__PURE__ */ (0, import_jsx_runtime38.jsx)("span", { className: "truncate text-[var(--color-text)]", title: album.name, children: album.name })
2199
2368
  ] }, album.uuid)) }) }, group);
2200
2369
  }) });
2201
2370
  }
2202
2371
 
2203
2372
  // src/genre-tree/artist/ArtistSongs.tsx
2204
- var import_react15 = require("react");
2373
+ var import_react16 = require("react");
2205
2374
  var import_react_query2 = require("@tanstack/react-query");
2206
- var import_lucide_react13 = require("lucide-react");
2207
- var import_ui14 = require("@behindthemusictree/ui");
2375
+ var import_lucide_react14 = require("lucide-react");
2376
+ var import_ui15 = require("@behindthemusictree/ui");
2208
2377
  var import_player10 = require("@behindthemusictree/app-kit/player");
2209
- var import_genre_tree23 = require("@behindthemusictree/app-kit/genre-tree");
2378
+ var import_genre_tree24 = require("@behindthemusictree/app-kit/genre-tree");
2210
2379
  var import_transport9 = require("@behindthemusictree/app-kit/transport");
2211
2380
 
2212
2381
  // src/genre-tree/track-list-sidebar/TrackItem.tsx
2213
2382
  var import_player9 = require("@behindthemusictree/app-kit/player");
2214
- var import_genre_tree20 = require("@behindthemusictree/app-kit/genre-tree");
2383
+ var import_genre_tree21 = require("@behindthemusictree/app-kit/genre-tree");
2215
2384
 
2216
2385
  // src/genre-tree/TrackPositionPlayPause.tsx
2217
2386
  var import_fa5 = require("react-icons/fa");
2218
2387
  var import_player7 = require("@behindthemusictree/app-kit/player");
2219
2388
  var import_player8 = require("@behindthemusictree/app-kit/player");
2220
- var import_jsx_runtime38 = require("react/jsx-runtime");
2389
+ var import_jsx_runtime39 = require("react/jsx-runtime");
2221
2390
  function TrackPositionPlayPause({
2222
2391
  position,
2223
2392
  uuid,
@@ -2231,14 +2400,14 @@ function TrackPositionPlayPause({
2231
2400
  const isPlaying = isThisTrack && isConfirmedPlayable && playState === import_player8.PlayStates.PLAYING;
2232
2401
  const isPaused = isThisTrack && isConfirmedPlayable && playState === import_player8.PlayStates.PAUSED;
2233
2402
  const isCurrentTrackActive = isPlaying || isPaused;
2234
- return /* @__PURE__ */ (0, import_jsx_runtime38.jsxs)(
2403
+ return /* @__PURE__ */ (0, import_jsx_runtime39.jsxs)(
2235
2404
  "div",
2236
2405
  {
2237
2406
  className: "track-position-play-pause relative flex items-center justify-center gap-2 text-lg w-16 cursor-pointer",
2238
2407
  style: { minWidth: "4rem", maxWidth: "4rem" },
2239
2408
  onClick: handlePlayPauseClick,
2240
2409
  children: [
2241
- /* @__PURE__ */ (0, import_jsx_runtime38.jsx)("div", { className: "track-position-play-pause__status group-hover:hidden flex items-center justify-center", children: isAwaitingPlayback ? /* @__PURE__ */ (0, import_jsx_runtime38.jsx)("span", { className: "track-position-play-pause__spinner", role: "status", "aria-label": "Loading track" }) : isCurrentTrackActive ? /* @__PURE__ */ (0, import_jsx_runtime38.jsxs)(
2410
+ /* @__PURE__ */ (0, import_jsx_runtime39.jsx)("div", { className: "track-position-play-pause__status group-hover:hidden flex items-center justify-center", children: isAwaitingPlayback ? /* @__PURE__ */ (0, import_jsx_runtime39.jsx)("span", { className: "track-position-play-pause__spinner", role: "status", "aria-label": "Loading track" }) : isCurrentTrackActive ? /* @__PURE__ */ (0, import_jsx_runtime39.jsxs)(
2242
2411
  "span",
2243
2412
  {
2244
2413
  className: "track-position-play-pause__bars",
@@ -2246,18 +2415,18 @@ function TrackPositionPlayPause({
2246
2415
  role: "img",
2247
2416
  "aria-label": isPlaying ? "Playing" : "Paused",
2248
2417
  children: [
2249
- /* @__PURE__ */ (0, import_jsx_runtime38.jsx)("span", { className: "track-position-play-pause__bar" }),
2250
- /* @__PURE__ */ (0, import_jsx_runtime38.jsx)("span", { className: "track-position-play-pause__bar" }),
2251
- /* @__PURE__ */ (0, import_jsx_runtime38.jsx)("span", { className: "track-position-play-pause__bar" })
2418
+ /* @__PURE__ */ (0, import_jsx_runtime39.jsx)("span", { className: "track-position-play-pause__bar" }),
2419
+ /* @__PURE__ */ (0, import_jsx_runtime39.jsx)("span", { className: "track-position-play-pause__bar" }),
2420
+ /* @__PURE__ */ (0, import_jsx_runtime39.jsx)("span", { className: "track-position-play-pause__bar" })
2252
2421
  ]
2253
2422
  }
2254
2423
  ) : position }),
2255
- /* @__PURE__ */ (0, import_jsx_runtime38.jsx)(
2424
+ /* @__PURE__ */ (0, import_jsx_runtime39.jsx)(
2256
2425
  "div",
2257
2426
  {
2258
2427
  className: `hidden group-hover:flex items-center justify-center ${isCurrentTrackActive ? "" : "absolute inset-0"}`,
2259
2428
  "aria-hidden": "true",
2260
- children: isAwaitingPlayback ? /* @__PURE__ */ (0, import_jsx_runtime38.jsx)("span", { className: "track-position-play-pause__spinner" }) : isPlaying ? /* @__PURE__ */ (0, import_jsx_runtime38.jsx)(import_fa5.FaPause, {}) : /* @__PURE__ */ (0, import_jsx_runtime38.jsx)(import_fa5.FaPlay, {})
2429
+ children: isAwaitingPlayback ? /* @__PURE__ */ (0, import_jsx_runtime39.jsx)("span", { className: "track-position-play-pause__spinner" }) : isPlaying ? /* @__PURE__ */ (0, import_jsx_runtime39.jsx)(import_fa5.FaPause, {}) : /* @__PURE__ */ (0, import_jsx_runtime39.jsx)(import_fa5.FaPlay, {})
2261
2430
  }
2262
2431
  )
2263
2432
  ]
@@ -2266,9 +2435,9 @@ function TrackPositionPlayPause({
2266
2435
  }
2267
2436
 
2268
2437
  // src/genre-tree/track-list-sidebar/TrackItem.tsx
2269
- var import_genre_tree21 = require("@behindthemusictree/app-kit/genre-tree");
2270
2438
  var import_genre_tree22 = require("@behindthemusictree/app-kit/genre-tree");
2271
- var import_jsx_runtime39 = require("react/jsx-runtime");
2439
+ var import_genre_tree23 = require("@behindthemusictree/app-kit/genre-tree");
2440
+ var import_jsx_runtime40 = require("react/jsx-runtime");
2272
2441
  function TrackItem({
2273
2442
  track,
2274
2443
  position,
@@ -2278,11 +2447,11 @@ function TrackItem({
2278
2447
  renderSongLink
2279
2448
  }) {
2280
2449
  const { handlePlayPauseAction, playerTrackObject, loadTrackForPlayer } = (0, import_player9.usePlayer)();
2281
- const { trackList, selectedTrack, toTrackAtPosition, runtimeUnplayableTrackIds } = (0, import_genre_tree20.useTrackList)();
2450
+ const { trackList, selectedTrack, toTrackAtPosition, runtimeUnplayableTrackIds } = (0, import_genre_tree21.useTrackList)();
2282
2451
  const scope = trackList?.origin?.scope ?? null;
2283
2452
  const isCurrentTrack = playerTrackObject?.track.id === track.uuid;
2284
2453
  const hasPlaybackError = isCurrentTrack && !!playerTrackObject?.loadError;
2285
- const isUnplayable = hasPlaybackError || runtimeUnplayableTrackIds?.has(track.uuid) === true || (0, import_genre_tree21.isSong)(track) && (0, import_genre_tree21.songUnplayableReason)(track) !== null;
2454
+ const isUnplayable = hasPlaybackError || runtimeUnplayableTrackIds?.has(track.uuid) === true || (0, import_genre_tree22.isSong)(track) && (0, import_genre_tree22.songUnplayableReason)(track) !== null;
2286
2455
  const handlePlayPauseClick = (event) => {
2287
2456
  event.stopPropagation();
2288
2457
  if (isCurrentTrack) {
@@ -2292,12 +2461,12 @@ function TrackItem({
2292
2461
  loadTrackForPlayer(track.uuid);
2293
2462
  }
2294
2463
  };
2295
- return /* @__PURE__ */ (0, import_jsx_runtime39.jsxs)(
2464
+ return /* @__PURE__ */ (0, import_jsx_runtime40.jsxs)(
2296
2465
  "div",
2297
2466
  {
2298
2467
  className: `track-item flex h-14 text-[var(--color-text)] ${isUnplayable ? "opacity-50" : "hover:bg-[var(--color-surface-muted)] group"}`,
2299
2468
  children: [
2300
- isUnplayable ? /* @__PURE__ */ (0, import_jsx_runtime39.jsx)(
2469
+ isUnplayable ? /* @__PURE__ */ (0, import_jsx_runtime40.jsx)(
2301
2470
  "div",
2302
2471
  {
2303
2472
  className: "flex items-center justify-center text-lg w-16",
@@ -2305,7 +2474,7 @@ function TrackItem({
2305
2474
  "aria-disabled": "true",
2306
2475
  children: position
2307
2476
  }
2308
- ) : /* @__PURE__ */ (0, import_jsx_runtime39.jsx)(
2477
+ ) : /* @__PURE__ */ (0, import_jsx_runtime40.jsx)(
2309
2478
  TrackPositionPlayPause,
2310
2479
  {
2311
2480
  position,
@@ -2314,12 +2483,12 @@ function TrackItem({
2314
2483
  isSelected: selectedTrack?.uuid === track.uuid
2315
2484
  }
2316
2485
  ),
2317
- /* @__PURE__ */ (0, import_jsx_runtime39.jsxs)("div", { className: "title-artist-container flex flex-col items-start justify-center min-w-0 flex-1", children: [
2318
- /* @__PURE__ */ (0, import_jsx_runtime39.jsx)("div", { className: "flex max-w-full items-center gap-2", children: /* @__PURE__ */ (0, import_jsx_runtime39.jsx)("div", { className: "title text-lg font-bold text-[var(--color-text)] max-w-full truncate", children: renderSongLink ? renderSongLink(track) : (0, import_genre_tree21.isSong)(track) ? /* @__PURE__ */ (0, import_jsx_runtime39.jsx)(MusicBrainzRecordingLink, { song: track, className: "hover:underline focus-visible:underline", children: track.title }) : track.title }) }),
2319
- track.artists && track.artists.length > 0 ? /* @__PURE__ */ (0, import_jsx_runtime39.jsx)("div", { className: "artist text-base max-w-full truncate text-[var(--color-text-muted)]", children: /* @__PURE__ */ (0, import_jsx_runtime39.jsx)(import_genre_tree22.ArtistNames, { artists: track.artists, renderArtistLink }) }) : ""
2486
+ /* @__PURE__ */ (0, import_jsx_runtime40.jsxs)("div", { className: "title-artist-container flex flex-col items-start justify-center min-w-0 flex-1", children: [
2487
+ /* @__PURE__ */ (0, import_jsx_runtime40.jsx)("div", { className: "flex max-w-full items-center gap-2", children: /* @__PURE__ */ (0, import_jsx_runtime40.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_runtime40.jsx)(MusicBrainzRecordingLink, { song: track, className: "hover:underline focus-visible:underline", children: track.title }) : track.title }) }),
2488
+ track.artists && track.artists.length > 0 ? /* @__PURE__ */ (0, import_jsx_runtime40.jsx)("div", { className: "artist text-base max-w-full truncate text-[var(--color-text-muted)]", children: /* @__PURE__ */ (0, import_jsx_runtime40.jsx)(import_genre_tree23.ArtistNames, { artists: track.artists, renderArtistLink }) }) : ""
2320
2489
  ] }),
2321
- /* @__PURE__ */ (0, import_jsx_runtime39.jsx)("div", { className: "genre-name-container flex items-center justify-start w-[21.6%] min-w-0 shrink-0", children: track.genre ? /* @__PURE__ */ (0, import_jsx_runtime39.jsx)("div", { className: "genre-name max-w-full whitespace-normal break-words p-1 text-xs font-bold", children: track.genre.name }) : "" }),
2322
- renderDuration && /* @__PURE__ */ (0, import_jsx_runtime39.jsx)(
2490
+ /* @__PURE__ */ (0, import_jsx_runtime40.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_runtime40.jsx)("div", { className: "genre-name max-w-full whitespace-normal break-words p-1 text-xs font-bold", children: track.genre.name }) : "" }),
2491
+ renderDuration && /* @__PURE__ */ (0, import_jsx_runtime40.jsx)(
2323
2492
  "div",
2324
2493
  {
2325
2494
  className: "duration flex text-base w-16 items-center justify-center",
@@ -2327,28 +2496,28 @@ function TrackItem({
2327
2496
  children: renderDuration(track)
2328
2497
  }
2329
2498
  ),
2330
- renderActions && /* @__PURE__ */ (0, import_jsx_runtime39.jsx)("div", { className: "edit flex text-base w-6 items-center justify-center mr-2 cursor-pointer", children: renderActions(track) })
2499
+ renderActions && /* @__PURE__ */ (0, import_jsx_runtime40.jsx)("div", { className: "edit flex text-base w-6 items-center justify-center mr-2 cursor-pointer", children: renderActions(track) })
2331
2500
  ]
2332
2501
  }
2333
2502
  );
2334
2503
  }
2335
2504
 
2336
2505
  // src/genre-tree/artist/ArtistSongs.tsx
2337
- var import_jsx_runtime40 = require("react/jsx-runtime");
2338
- var SongPageSchema2 = (0, import_transport9.PaginatedResponseSchema)(import_genre_tree23.SongDetailedSchema);
2506
+ var import_jsx_runtime41 = require("react/jsx-runtime");
2507
+ var SongPageSchema2 = (0, import_transport9.PaginatedResponseSchema)(import_genre_tree24.SongDetailedSchema);
2339
2508
  var SORTS = [
2340
2509
  { ordering: "title", label: "Title" },
2341
2510
  { ordering: "-albumYear", label: "Year" }
2342
2511
  ];
2343
2512
  function ArtistSongs({ artist, getBackendBaseUrl, renderArtistLink, renderSongLink }) {
2344
- const [page, setPage] = (0, import_react15.useState)(1);
2345
- const [ordering, setOrdering] = (0, import_react15.useState)("title");
2513
+ const [page, setPage] = (0, import_react16.useState)(1);
2514
+ const [ordering, setOrdering] = (0, import_react16.useState)("title");
2346
2515
  const { fetch } = (0, import_transport9.useFetchWrapper)(getBackendBaseUrl);
2347
- const { playNewTrackListFromArtist } = (0, import_genre_tree23.useTrackList)();
2516
+ const { playNewTrackListFromArtist } = (0, import_genre_tree24.useTrackList)();
2348
2517
  const { playerTrackObject } = (0, import_player10.usePlayer)();
2349
2518
  const { data, isPending, isError } = (0, import_transport9.useQueryWithParse)({
2350
2519
  queryKey: ["referenceSongs", "artist", artist.uuid, ordering, page],
2351
- queryFn: () => fetch(import_genre_tree23.libraryEndpoints.reference.songs.list(), true, false, void 0, {
2520
+ queryFn: () => fetch(import_genre_tree24.libraryEndpoints.reference.songs.list(), true, false, void 0, {
2352
2521
  artist: artist.uuid,
2353
2522
  ordering,
2354
2523
  page
@@ -2363,25 +2532,25 @@ function ArtistSongs({ artist, getBackendBaseUrl, renderArtistLink, renderSongLi
2363
2532
  });
2364
2533
  const handleRowClickCapture = (event, song) => {
2365
2534
  if (!(event.target instanceof Element) || !event.target.closest(".track-position-play-pause")) return;
2366
- if (playerTrackObject?.track.id === song.uuid || (0, import_genre_tree23.songUnplayableReason)(song) !== null) return;
2535
+ if (playerTrackObject?.track.id === song.uuid || (0, import_genre_tree24.songUnplayableReason)(song) !== null) return;
2367
2536
  event.stopPropagation();
2368
2537
  void play(song.uuid);
2369
2538
  };
2370
- return /* @__PURE__ */ (0, import_jsx_runtime40.jsx)(import_ui14.PanelSection, { title: "Songs", children: isPending ? /* @__PURE__ */ (0, import_jsx_runtime40.jsx)(import_ui14.Skeleton, { className: "h-14", "aria-busy": "true" }) : isError ? /* @__PURE__ */ (0, import_jsx_runtime40.jsx)("p", { className: "text-red-600", children: "Could not load songs." }) : data.results.length === 0 ? /* @__PURE__ */ (0, import_jsx_runtime40.jsx)("p", { className: "text-[var(--color-text-muted)]", children: "No songs yet." }) : /* @__PURE__ */ (0, import_jsx_runtime40.jsxs)(import_jsx_runtime40.Fragment, { children: [
2371
- /* @__PURE__ */ (0, import_jsx_runtime40.jsxs)("div", { className: "flex flex-wrap items-center justify-between gap-2 mb-2", children: [
2372
- /* @__PURE__ */ (0, import_jsx_runtime40.jsxs)(
2539
+ return /* @__PURE__ */ (0, import_jsx_runtime41.jsx)(import_ui15.PanelSection, { title: "Songs", children: isPending ? /* @__PURE__ */ (0, import_jsx_runtime41.jsx)(import_ui15.Skeleton, { className: "h-14", "aria-busy": "true" }) : isError ? /* @__PURE__ */ (0, import_jsx_runtime41.jsx)("p", { className: "text-red-600", children: "Could not load songs." }) : data.results.length === 0 ? /* @__PURE__ */ (0, import_jsx_runtime41.jsx)("p", { className: "text-[var(--color-text-muted)]", children: "No songs yet." }) : /* @__PURE__ */ (0, import_jsx_runtime41.jsxs)(import_jsx_runtime41.Fragment, { children: [
2540
+ /* @__PURE__ */ (0, import_jsx_runtime41.jsxs)("div", { className: "flex flex-wrap items-center justify-between gap-2 mb-2", children: [
2541
+ /* @__PURE__ */ (0, import_jsx_runtime41.jsxs)(
2373
2542
  "button",
2374
2543
  {
2375
2544
  type: "button",
2376
2545
  onClick: () => void play(),
2377
2546
  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",
2378
2547
  children: [
2379
- /* @__PURE__ */ (0, import_jsx_runtime40.jsx)(import_lucide_react13.Play, { className: "h-3.5 w-3.5", "aria-hidden": "true" }),
2548
+ /* @__PURE__ */ (0, import_jsx_runtime41.jsx)(import_lucide_react14.Play, { className: "h-3.5 w-3.5", "aria-hidden": "true" }),
2380
2549
  "Play all"
2381
2550
  ]
2382
2551
  }
2383
2552
  ),
2384
- /* @__PURE__ */ (0, import_jsx_runtime40.jsx)("div", { role: "group", "aria-label": "Sort songs by", className: "flex gap-1 text-xs", children: SORTS.map((sort) => /* @__PURE__ */ (0, import_jsx_runtime40.jsx)(
2553
+ /* @__PURE__ */ (0, import_jsx_runtime41.jsx)("div", { role: "group", "aria-label": "Sort songs by", className: "flex gap-1 text-xs", children: SORTS.map((sort) => /* @__PURE__ */ (0, import_jsx_runtime41.jsx)(
2385
2554
  "button",
2386
2555
  {
2387
2556
  type: "button",
@@ -2396,7 +2565,7 @@ function ArtistSongs({ artist, getBackendBaseUrl, renderArtistLink, renderSongLi
2396
2565
  sort.ordering
2397
2566
  )) })
2398
2567
  ] }),
2399
- /* @__PURE__ */ (0, import_jsx_runtime40.jsx)("ul", { className: "overflow-hidden mb-2 bg-zinc-950 rounded-lg", children: data.results.map((song, index) => /* @__PURE__ */ (0, import_jsx_runtime40.jsx)("li", { onClickCapture: (event) => handleRowClickCapture(event, song), children: /* @__PURE__ */ (0, import_jsx_runtime40.jsx)(
2568
+ /* @__PURE__ */ (0, import_jsx_runtime41.jsx)("ul", { className: "overflow-hidden mb-2 bg-zinc-950 rounded-lg", children: data.results.map((song, index) => /* @__PURE__ */ (0, import_jsx_runtime41.jsx)("li", { onClickCapture: (event) => handleRowClickCapture(event, song), children: /* @__PURE__ */ (0, import_jsx_runtime41.jsx)(
2400
2569
  TrackItem,
2401
2570
  {
2402
2571
  track: song,
@@ -2405,12 +2574,12 @@ function ArtistSongs({ artist, getBackendBaseUrl, renderArtistLink, renderSongLi
2405
2574
  renderSongLink
2406
2575
  }
2407
2576
  ) }, song.uuid)) }),
2408
- /* @__PURE__ */ (0, import_jsx_runtime40.jsx)(import_ui14.Pagination, { currentPage: page, totalPages: data.totalPages, onPageChange: setPage })
2577
+ /* @__PURE__ */ (0, import_jsx_runtime41.jsx)(import_ui15.Pagination, { currentPage: page, totalPages: data.totalPages, onPageChange: setPage })
2409
2578
  ] }) });
2410
2579
  }
2411
2580
 
2412
2581
  // src/genre-tree/artist/useArtistPanel.tsx
2413
- var import_jsx_runtime41 = require("react/jsx-runtime");
2582
+ var import_jsx_runtime42 = require("react/jsx-runtime");
2414
2583
  function useArtistPanel({
2415
2584
  uuid,
2416
2585
  scope,
@@ -2422,7 +2591,7 @@ function useArtistPanel({
2422
2591
  onExpandedChange,
2423
2592
  onClose
2424
2593
  }) {
2425
- const result = (0, import_genre_tree24.useArtist)(uuid, getBackendBaseUrl);
2594
+ const result = (0, import_genre_tree25.useArtist)(uuid, getBackendBaseUrl);
2426
2595
  const panel = { expanded, onExpandedChange, onClose };
2427
2596
  switch (result.status) {
2428
2597
  case "idle":
@@ -2431,26 +2600,26 @@ function useArtistPanel({
2431
2600
  return {
2432
2601
  ...panel,
2433
2602
  title: "Artist",
2434
- content: /* @__PURE__ */ (0, import_jsx_runtime41.jsxs)("div", { "aria-busy": "true", className: "flex flex-col gap-2.5 px-6 py-2.5", children: [
2435
- /* @__PURE__ */ (0, import_jsx_runtime41.jsx)(import_ui15.Skeleton, { className: "h-3 w-16" }),
2436
- /* @__PURE__ */ (0, import_jsx_runtime41.jsx)(import_ui15.Skeleton, { className: "h-4 w-32" }),
2437
- /* @__PURE__ */ (0, import_jsx_runtime41.jsx)(import_ui15.Skeleton, { className: "h-3 w-16" }),
2438
- /* @__PURE__ */ (0, import_jsx_runtime41.jsx)(import_ui15.Skeleton, { className: "h-4 w-24" }),
2439
- /* @__PURE__ */ (0, import_jsx_runtime41.jsx)(import_ui15.Skeleton, { className: "h-16 mt-2.5" }),
2440
- /* @__PURE__ */ (0, import_jsx_runtime41.jsx)(import_ui15.Skeleton, { className: "h-16" })
2603
+ content: /* @__PURE__ */ (0, import_jsx_runtime42.jsxs)("div", { "aria-busy": "true", className: "flex flex-col gap-2.5 px-6 py-2.5", children: [
2604
+ /* @__PURE__ */ (0, import_jsx_runtime42.jsx)(import_ui16.Skeleton, { className: "h-3 w-16" }),
2605
+ /* @__PURE__ */ (0, import_jsx_runtime42.jsx)(import_ui16.Skeleton, { className: "h-4 w-32" }),
2606
+ /* @__PURE__ */ (0, import_jsx_runtime42.jsx)(import_ui16.Skeleton, { className: "h-3 w-16" }),
2607
+ /* @__PURE__ */ (0, import_jsx_runtime42.jsx)(import_ui16.Skeleton, { className: "h-4 w-24" }),
2608
+ /* @__PURE__ */ (0, import_jsx_runtime42.jsx)(import_ui16.Skeleton, { className: "h-16 mt-2.5" }),
2609
+ /* @__PURE__ */ (0, import_jsx_runtime42.jsx)(import_ui16.Skeleton, { className: "h-16" })
2441
2610
  ] })
2442
2611
  };
2443
2612
  case "not-found":
2444
2613
  return {
2445
2614
  ...panel,
2446
2615
  title: "Artist not found",
2447
- content: /* @__PURE__ */ (0, import_jsx_runtime41.jsx)("p", { className: "px-6 py-2.5 text-[var(--color-text-muted)]", children: "This artist does not exist." })
2616
+ content: /* @__PURE__ */ (0, import_jsx_runtime42.jsx)("p", { className: "px-6 py-2.5 text-[var(--color-text-muted)]", children: "This artist does not exist." })
2448
2617
  };
2449
2618
  case "error":
2450
2619
  return {
2451
2620
  ...panel,
2452
2621
  title: "Artist",
2453
- content: /* @__PURE__ */ (0, import_jsx_runtime41.jsx)("p", { className: "px-6 py-2.5 text-red-600", children: "Could not load this artist." })
2622
+ content: /* @__PURE__ */ (0, import_jsx_runtime42.jsx)("p", { className: "px-6 py-2.5 text-red-600", children: "Could not load this artist." })
2454
2623
  };
2455
2624
  case "success": {
2456
2625
  const { artist } = result;
@@ -2459,13 +2628,13 @@ function useArtistPanel({
2459
2628
  return {
2460
2629
  ...panel,
2461
2630
  title: artist.name,
2462
- content: /* @__PURE__ */ (0, import_jsx_runtime41.jsxs)(import_jsx_runtime41.Fragment, { children: [
2463
- shareUrl && /* @__PURE__ */ (0, import_jsx_runtime41.jsx)("div", { className: "genre-action-band border-b border-[var(--color-border)]", children: /* @__PURE__ */ (0, import_jsx_runtime41.jsx)("div", { className: "flex flex-wrap gap-1 px-3.5 py-3", children: shareTarget && /* @__PURE__ */ (0, import_jsx_runtime41.jsx)(ShareActionButton, { target: shareTarget, url: shareUrl }) }) }),
2464
- artist.image && /* @__PURE__ */ (0, import_jsx_runtime41.jsx)(ArtistPhoto, { image: artist.image, name: artist.name }),
2465
- /* @__PURE__ */ (0, import_jsx_runtime41.jsx)(ArtistHeader, { artist }),
2466
- /* @__PURE__ */ (0, import_jsx_runtime41.jsx)(ArtistMembers, { title: "Members", memberships: artist.members, renderArtistLink }),
2467
- /* @__PURE__ */ (0, import_jsx_runtime41.jsx)(ArtistMembers, { title: "Member of", memberships: artist.memberOf, renderArtistLink }),
2468
- artist.tracksCount > 0 && /* @__PURE__ */ (0, import_jsx_runtime41.jsx)(
2631
+ content: /* @__PURE__ */ (0, import_jsx_runtime42.jsxs)(import_jsx_runtime42.Fragment, { children: [
2632
+ shareUrl && /* @__PURE__ */ (0, import_jsx_runtime42.jsx)("div", { className: "genre-action-band border-b border-[var(--color-border)]", children: /* @__PURE__ */ (0, import_jsx_runtime42.jsx)("div", { className: "flex flex-wrap gap-1 px-3.5 py-3", children: shareTarget && /* @__PURE__ */ (0, import_jsx_runtime42.jsx)(ShareActionButton, { target: shareTarget, url: shareUrl }) }) }),
2633
+ artist.image && /* @__PURE__ */ (0, import_jsx_runtime42.jsx)(ArtistPhoto, { image: artist.image, name: artist.name }),
2634
+ /* @__PURE__ */ (0, import_jsx_runtime42.jsx)(ArtistHeader, { artist }),
2635
+ /* @__PURE__ */ (0, import_jsx_runtime42.jsx)(ArtistMembers, { title: "Members", memberships: artist.members, renderArtistLink }),
2636
+ /* @__PURE__ */ (0, import_jsx_runtime42.jsx)(ArtistMembers, { title: "Member of", memberships: artist.memberOf, renderArtistLink }),
2637
+ artist.tracksCount > 0 && /* @__PURE__ */ (0, import_jsx_runtime42.jsx)(
2469
2638
  ArtistSongs,
2470
2639
  {
2471
2640
  artist: { uuid: artist.uuid, name: artist.name },
@@ -2475,56 +2644,56 @@ function useArtistPanel({
2475
2644
  }
2476
2645
  )
2477
2646
  ] }),
2478
- extraContent: artist.albums.length > 0 ? /* @__PURE__ */ (0, import_jsx_runtime41.jsx)(ArtistDiscography, { albums: artist.albums }) : void 0
2647
+ extraContent: artist.albums.length > 0 ? /* @__PURE__ */ (0, import_jsx_runtime42.jsx)(ArtistDiscography, { albums: artist.albums }) : void 0
2479
2648
  };
2480
2649
  }
2481
2650
  }
2482
2651
  }
2483
2652
 
2484
2653
  // src/genre-tree/song/useSongPanel.tsx
2485
- var import_ui17 = require("@behindthemusictree/ui");
2486
- var import_genre_tree26 = require("@behindthemusictree/app-kit/genre-tree");
2654
+ var import_ui18 = require("@behindthemusictree/ui");
2655
+ var import_genre_tree27 = require("@behindthemusictree/app-kit/genre-tree");
2487
2656
 
2488
2657
  // src/genre-tree/song/SongPanelContent.tsx
2489
- var import_react16 = require("react");
2490
- var import_lucide_react14 = require("lucide-react");
2491
- var import_ui16 = require("@behindthemusictree/ui");
2658
+ var import_react17 = require("react");
2659
+ var import_lucide_react15 = require("lucide-react");
2660
+ var import_ui17 = require("@behindthemusictree/ui");
2492
2661
  var import_player11 = require("@behindthemusictree/app-kit/player");
2493
- var import_genre_tree25 = require("@behindthemusictree/app-kit/genre-tree");
2494
- var import_jsx_runtime42 = require("react/jsx-runtime");
2662
+ var import_genre_tree26 = require("@behindthemusictree/app-kit/genre-tree");
2663
+ var import_jsx_runtime43 = require("react/jsx-runtime");
2495
2664
  var numberFormat = new Intl.NumberFormat("en");
2496
2665
  var dateFormat = new Intl.DateTimeFormat("en", { dateStyle: "medium" });
2497
2666
  var externalLink = "text-[var(--color-primary)] hover:underline focus-visible:underline";
2498
2667
  function SongHistory({ uuid, getBackendBaseUrl }) {
2499
- const [open, setOpen] = (0, import_react16.useState)(false);
2500
- const history = (0, import_genre_tree25.useSongHistory)(uuid, getBackendBaseUrl, open);
2668
+ const [open, setOpen] = (0, import_react17.useState)(false);
2669
+ const history = (0, import_genre_tree26.useSongHistory)(uuid, getBackendBaseUrl, open);
2501
2670
  let body = null;
2502
- if (history.status === "loading") body = /* @__PURE__ */ (0, import_jsx_runtime42.jsx)(import_ui16.Skeleton, { className: "h-10", "aria-busy": "true" });
2503
- else if (history.status === "error") body = /* @__PURE__ */ (0, import_jsx_runtime42.jsx)("p", { className: "text-red-600", children: "Could not load the history." });
2671
+ if (history.status === "loading") body = /* @__PURE__ */ (0, import_jsx_runtime43.jsx)(import_ui17.Skeleton, { className: "h-10", "aria-busy": "true" });
2672
+ else if (history.status === "error") body = /* @__PURE__ */ (0, import_jsx_runtime43.jsx)("p", { className: "text-red-600", children: "Could not load the history." });
2504
2673
  else if (history.status === "success")
2505
- body = history.entries.length === 0 ? /* @__PURE__ */ (0, import_jsx_runtime42.jsx)("p", { className: "text-[var(--color-text-muted)]", children: "No changes yet." }) : /* @__PURE__ */ (0, import_jsx_runtime42.jsx)("ul", { className: "divide-y divide-[var(--color-border)]", children: history.entries.map((entry) => /* @__PURE__ */ (0, import_jsx_runtime42.jsxs)("li", { className: "py-1.5 text-sm", children: [
2506
- /* @__PURE__ */ (0, import_jsx_runtime42.jsx)("span", { className: "font-medium", children: entry.action }),
2507
- entry.oldValue !== null || entry.newValue !== null ? /* @__PURE__ */ (0, import_jsx_runtime42.jsxs)("span", { className: "text-[var(--color-text-muted)]", children: [
2674
+ body = history.entries.length === 0 ? /* @__PURE__ */ (0, import_jsx_runtime43.jsx)("p", { className: "text-[var(--color-text-muted)]", children: "No changes yet." }) : /* @__PURE__ */ (0, import_jsx_runtime43.jsx)("ul", { className: "divide-y divide-[var(--color-border)]", children: history.entries.map((entry) => /* @__PURE__ */ (0, import_jsx_runtime43.jsxs)("li", { className: "py-1.5 text-sm", children: [
2675
+ /* @__PURE__ */ (0, import_jsx_runtime43.jsx)("span", { className: "font-medium", children: entry.action }),
2676
+ entry.oldValue !== null || entry.newValue !== null ? /* @__PURE__ */ (0, import_jsx_runtime43.jsxs)("span", { className: "text-[var(--color-text-muted)]", children: [
2508
2677
  " ",
2509
2678
  "\u2014 ",
2510
2679
  entry.oldValue ?? "none",
2511
2680
  " \u2192 ",
2512
2681
  entry.newValue ?? "none"
2513
2682
  ] }) : null,
2514
- /* @__PURE__ */ (0, import_jsx_runtime42.jsxs)("span", { className: "block text-xs text-[var(--color-text-muted)]", children: [
2683
+ /* @__PURE__ */ (0, import_jsx_runtime43.jsxs)("span", { className: "block text-xs text-[var(--color-text-muted)]", children: [
2515
2684
  entry.actorPseudo ?? "Anonymous",
2516
2685
  " \xB7 ",
2517
2686
  dateFormat.format(new Date(entry.createdOn))
2518
2687
  ] })
2519
2688
  ] }, entry.uuid)) });
2520
- return /* @__PURE__ */ (0, import_jsx_runtime42.jsxs)(
2689
+ return /* @__PURE__ */ (0, import_jsx_runtime43.jsxs)(
2521
2690
  "details",
2522
2691
  {
2523
2692
  onToggle: (event) => setOpen(event.currentTarget.open),
2524
2693
  className: "px-6 pb-2.5 mt-2.5 border-t border-[var(--color-border)]",
2525
2694
  children: [
2526
- /* @__PURE__ */ (0, import_jsx_runtime42.jsx)("summary", { className: "mt-2.5 cursor-pointer text-[0.6875rem] font-semibold uppercase text-[var(--color-text-muted)]", children: "History" }),
2527
- /* @__PURE__ */ (0, import_jsx_runtime42.jsx)("div", { className: "mt-1.5", children: body })
2695
+ /* @__PURE__ */ (0, import_jsx_runtime43.jsx)("summary", { className: "mt-2.5 cursor-pointer text-[0.6875rem] font-semibold uppercase text-[var(--color-text-muted)]", children: "History" }),
2696
+ /* @__PURE__ */ (0, import_jsx_runtime43.jsx)("div", { className: "mt-1.5", children: body })
2528
2697
  ]
2529
2698
  }
2530
2699
  );
@@ -2538,25 +2707,25 @@ function SongPanelContent({
2538
2707
  genrePath,
2539
2708
  onSelectGenre
2540
2709
  }) {
2541
- const { playNewTrackListFromTrackUuid } = (0, import_genre_tree25.useTrackList)();
2710
+ const { playNewTrackListFromTrackUuid } = (0, import_genre_tree26.useTrackList)();
2542
2711
  const { playerTrackObject, playState, handlePlayPauseAction } = (0, import_player11.usePlayer)();
2543
- const unplayableReason = (0, import_genre_tree25.songUnplayableReason)(song);
2712
+ const unplayableReason = (0, import_genre_tree26.songUnplayableReason)(song);
2544
2713
  const isCurrent = playerTrackObject?.track.id === song.uuid;
2545
2714
  const isPlaying = isCurrent && playState === import_player11.PlayStates.PLAYING;
2546
2715
  const shareTarget = { kind: "song", uuid: song.uuid, name: song.title, scope };
2547
- const musicBrainzUrl = (0, import_genre_tree25.musicBrainzRecordingUrl)(song);
2716
+ const musicBrainzUrl = (0, import_genre_tree26.musicBrainzRecordingUrl)(song);
2548
2717
  const fields = [];
2549
2718
  if (song.genre) {
2550
2719
  const path = genrePath.length > 0 ? genrePath : null;
2551
2720
  fields.push({
2552
2721
  label: "Genre",
2553
- value: path ? /* @__PURE__ */ (0, import_jsx_runtime42.jsx)("span", { className: "flex flex-wrap items-center gap-x-1", children: path.map((genrePlaylist, index) => /* @__PURE__ */ (0, import_jsx_runtime42.jsxs)("span", { children: [
2554
- index > 0 && /* @__PURE__ */ (0, import_jsx_runtime42.jsx)("span", { "aria-hidden": "true", children: "\u203A " }),
2555
- /* @__PURE__ */ (0, import_jsx_runtime42.jsx)("button", { type: "button", onClick: () => onSelectGenre(genrePlaylist), className: "hover:underline", children: genrePlaylist.name })
2722
+ value: path ? /* @__PURE__ */ (0, import_jsx_runtime43.jsx)("span", { className: "flex flex-wrap items-center gap-x-1", children: path.map((genrePlaylist, index) => /* @__PURE__ */ (0, import_jsx_runtime43.jsxs)("span", { children: [
2723
+ index > 0 && /* @__PURE__ */ (0, import_jsx_runtime43.jsx)("span", { "aria-hidden": "true", children: "\u203A " }),
2724
+ /* @__PURE__ */ (0, import_jsx_runtime43.jsx)("button", { type: "button", onClick: () => onSelectGenre(genrePlaylist), className: "hover:underline", children: genrePlaylist.name })
2556
2725
  ] }, genrePlaylist.uuid)) }) : song.genre.name
2557
2726
  });
2558
2727
  } else {
2559
- fields.push({ label: "Genre", value: /* @__PURE__ */ (0, import_jsx_runtime42.jsx)("span", { className: "text-[var(--color-text-muted)]", children: "No genre" }) });
2728
+ fields.push({ label: "Genre", value: /* @__PURE__ */ (0, import_jsx_runtime43.jsx)("span", { className: "text-[var(--color-text-muted)]", children: "No genre" }) });
2560
2729
  }
2561
2730
  if (song.firstReleaseYear) fields.push({ label: "First released", value: song.firstReleaseYear });
2562
2731
  const viewCount = song.youtubeVideo?.viewCount;
@@ -2566,11 +2735,11 @@ function SongPanelContent({
2566
2735
  const links = [];
2567
2736
  if (musicBrainzUrl)
2568
2737
  links.push(
2569
- /* @__PURE__ */ (0, import_jsx_runtime42.jsx)("a", { href: musicBrainzUrl, target: "_blank", rel: "noopener noreferrer", className: externalLink, children: "MusicBrainz" }, "mb")
2738
+ /* @__PURE__ */ (0, import_jsx_runtime43.jsx)("a", { href: musicBrainzUrl, target: "_blank", rel: "noopener noreferrer", className: externalLink, children: "MusicBrainz" }, "mb")
2570
2739
  );
2571
2740
  if (song.youtubeVideo)
2572
2741
  links.push(
2573
- /* @__PURE__ */ (0, import_jsx_runtime42.jsx)(
2742
+ /* @__PURE__ */ (0, import_jsx_runtime43.jsx)(
2574
2743
  "a",
2575
2744
  {
2576
2745
  href: `https://www.youtube.com/watch?v=${encodeURIComponent(song.youtubeVideo.id)}`,
@@ -2585,33 +2754,33 @@ function SongPanelContent({
2585
2754
  if (links.length > 0)
2586
2755
  fields.push({
2587
2756
  label: "Links",
2588
- value: /* @__PURE__ */ (0, import_jsx_runtime42.jsx)("span", { className: "flex gap-3", children: links })
2757
+ value: /* @__PURE__ */ (0, import_jsx_runtime43.jsx)("span", { className: "flex gap-3", children: links })
2589
2758
  });
2590
- return /* @__PURE__ */ (0, import_jsx_runtime42.jsxs)(import_jsx_runtime42.Fragment, { children: [
2591
- /* @__PURE__ */ (0, import_jsx_runtime42.jsx)("div", { className: "genre-action-band border-b border-[var(--color-border)]", children: /* @__PURE__ */ (0, import_jsx_runtime42.jsxs)("div", { className: "flex flex-wrap gap-1 px-3.5 py-3", children: [
2592
- unplayableReason ? /* @__PURE__ */ (0, import_jsx_runtime42.jsx)("p", { className: "flex items-center px-2.5 text-sm text-[var(--color-text-muted)]", children: unplayableReason }) : /* @__PURE__ */ (0, import_jsx_runtime42.jsxs)(
2759
+ return /* @__PURE__ */ (0, import_jsx_runtime43.jsxs)(import_jsx_runtime43.Fragment, { children: [
2760
+ /* @__PURE__ */ (0, import_jsx_runtime43.jsx)("div", { className: "genre-action-band border-b border-[var(--color-border)]", children: /* @__PURE__ */ (0, import_jsx_runtime43.jsxs)("div", { className: "flex flex-wrap gap-1 px-3.5 py-3", children: [
2761
+ unplayableReason ? /* @__PURE__ */ (0, import_jsx_runtime43.jsx)("p", { className: "flex items-center px-2.5 text-sm text-[var(--color-text-muted)]", children: unplayableReason }) : /* @__PURE__ */ (0, import_jsx_runtime43.jsxs)(
2593
2762
  "button",
2594
2763
  {
2595
2764
  type: "button",
2596
2765
  onClick: () => isCurrent ? handlePlayPauseAction() : playNewTrackListFromTrackUuid(song, scope),
2597
2766
  className: "group flex flex-col items-center gap-1 w-16 text-xs text-center text-[var(--color-text)] focus-visible:outline-none",
2598
2767
  children: [
2599
- /* @__PURE__ */ (0, import_jsx_runtime42.jsx)("span", { className: "flex items-center justify-center w-10 h-10 rounded-full group-focus-visible:ring-2 group-focus-visible:ring-[var(--color-focus-ring)] bg-[var(--color-surface-muted)] transition-colors group-hover:bg-[var(--color-border)]", children: isPlaying ? /* @__PURE__ */ (0, import_jsx_runtime42.jsx)(import_lucide_react14.Pause, { "aria-hidden": "true", className: "w-5 h-5" }) : /* @__PURE__ */ (0, import_jsx_runtime42.jsx)(import_lucide_react14.Play, { "aria-hidden": "true", className: "w-5 h-5" }) }),
2768
+ /* @__PURE__ */ (0, import_jsx_runtime43.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_runtime43.jsx)(import_lucide_react15.Pause, { "aria-hidden": "true", className: "w-5 h-5" }) : /* @__PURE__ */ (0, import_jsx_runtime43.jsx)(import_lucide_react15.Play, { "aria-hidden": "true", className: "w-5 h-5" }) }),
2600
2769
  isPlaying ? "Pause" : "Play"
2601
2770
  ]
2602
2771
  }
2603
2772
  ),
2604
- shareUrl && /* @__PURE__ */ (0, import_jsx_runtime42.jsx)(ShareActionButton, { target: shareTarget, url: shareUrl })
2773
+ shareUrl && /* @__PURE__ */ (0, import_jsx_runtime43.jsx)(ShareActionButton, { target: shareTarget, url: shareUrl })
2605
2774
  ] }) }),
2606
- song.artists && song.artists.length > 0 && /* @__PURE__ */ (0, import_jsx_runtime42.jsx)("p", { className: "px-6 pt-2.5 text-[var(--color-text-muted)]", children: /* @__PURE__ */ (0, import_jsx_runtime42.jsx)(import_genre_tree25.ArtistNames, { artists: song.artists, renderArtistLink }) }),
2607
- /* @__PURE__ */ (0, import_jsx_runtime42.jsx)(import_ui16.FieldList, { items: fields }),
2608
- song.essentialGenres && song.essentialGenres.length > 0 && /* @__PURE__ */ (0, import_jsx_runtime42.jsx)(import_ui16.PanelSection, { title: "Essential in", children: /* @__PURE__ */ (0, import_jsx_runtime42.jsx)("ul", { children: song.essentialGenres.map((genre) => /* @__PURE__ */ (0, import_jsx_runtime42.jsx)("li", { className: "py-0.5", children: genre.name }, genre.uuid)) }) }),
2609
- /* @__PURE__ */ (0, import_jsx_runtime42.jsx)(SongHistory, { uuid: song.uuid, getBackendBaseUrl })
2775
+ song.artists && song.artists.length > 0 && /* @__PURE__ */ (0, import_jsx_runtime43.jsx)("p", { className: "px-6 pt-2.5 text-[var(--color-text-muted)]", children: /* @__PURE__ */ (0, import_jsx_runtime43.jsx)(import_genre_tree26.ArtistNames, { artists: song.artists, renderArtistLink }) }),
2776
+ /* @__PURE__ */ (0, import_jsx_runtime43.jsx)(import_ui17.FieldList, { items: fields }),
2777
+ song.essentialGenres && song.essentialGenres.length > 0 && /* @__PURE__ */ (0, import_jsx_runtime43.jsx)(import_ui17.PanelSection, { title: "Essential in", children: /* @__PURE__ */ (0, import_jsx_runtime43.jsx)("ul", { children: song.essentialGenres.map((genre) => /* @__PURE__ */ (0, import_jsx_runtime43.jsx)("li", { className: "py-0.5", children: genre.name }, genre.uuid)) }) }),
2778
+ /* @__PURE__ */ (0, import_jsx_runtime43.jsx)(SongHistory, { uuid: song.uuid, getBackendBaseUrl })
2610
2779
  ] });
2611
2780
  }
2612
2781
 
2613
2782
  // src/genre-tree/song/useSongPanel.tsx
2614
- var import_jsx_runtime43 = require("react/jsx-runtime");
2783
+ var import_jsx_runtime44 = require("react/jsx-runtime");
2615
2784
  function useSongPanel({
2616
2785
  uuid,
2617
2786
  scope,
@@ -2624,7 +2793,7 @@ function useSongPanel({
2624
2793
  onExpandedChange,
2625
2794
  onClose
2626
2795
  }) {
2627
- const result = (0, import_genre_tree26.useSong)(uuid, getBackendBaseUrl);
2796
+ const result = (0, import_genre_tree27.useSong)(uuid, getBackendBaseUrl);
2628
2797
  const panel = { expanded, onExpandedChange, onClose };
2629
2798
  switch (result.status) {
2630
2799
  case "idle":
@@ -2633,24 +2802,24 @@ function useSongPanel({
2633
2802
  return {
2634
2803
  ...panel,
2635
2804
  title: "Song",
2636
- content: /* @__PURE__ */ (0, import_jsx_runtime43.jsxs)("div", { "aria-busy": "true", className: "flex flex-col gap-2.5 px-6 py-2.5", children: [
2637
- /* @__PURE__ */ (0, import_jsx_runtime43.jsx)(import_ui17.Skeleton, { className: "h-3 w-16" }),
2638
- /* @__PURE__ */ (0, import_jsx_runtime43.jsx)(import_ui17.Skeleton, { className: "h-4 w-32" }),
2639
- /* @__PURE__ */ (0, import_jsx_runtime43.jsx)(import_ui17.Skeleton, { className: "h-3 w-16" }),
2640
- /* @__PURE__ */ (0, import_jsx_runtime43.jsx)(import_ui17.Skeleton, { className: "h-4 w-24" })
2805
+ content: /* @__PURE__ */ (0, import_jsx_runtime44.jsxs)("div", { "aria-busy": "true", className: "flex flex-col gap-2.5 px-6 py-2.5", children: [
2806
+ /* @__PURE__ */ (0, import_jsx_runtime44.jsx)(import_ui18.Skeleton, { className: "h-3 w-16" }),
2807
+ /* @__PURE__ */ (0, import_jsx_runtime44.jsx)(import_ui18.Skeleton, { className: "h-4 w-32" }),
2808
+ /* @__PURE__ */ (0, import_jsx_runtime44.jsx)(import_ui18.Skeleton, { className: "h-3 w-16" }),
2809
+ /* @__PURE__ */ (0, import_jsx_runtime44.jsx)(import_ui18.Skeleton, { className: "h-4 w-24" })
2641
2810
  ] })
2642
2811
  };
2643
2812
  case "not-found":
2644
2813
  return {
2645
2814
  ...panel,
2646
2815
  title: "Song not found",
2647
- content: /* @__PURE__ */ (0, import_jsx_runtime43.jsx)("p", { className: "px-6 py-2.5 text-[var(--color-text-muted)]", children: "This song does not exist." })
2816
+ content: /* @__PURE__ */ (0, import_jsx_runtime44.jsx)("p", { className: "px-6 py-2.5 text-[var(--color-text-muted)]", children: "This song does not exist." })
2648
2817
  };
2649
2818
  case "error":
2650
2819
  return {
2651
2820
  ...panel,
2652
2821
  title: "Song",
2653
- content: /* @__PURE__ */ (0, import_jsx_runtime43.jsx)("p", { className: "px-6 py-2.5 text-red-600", children: "Could not load this song." })
2822
+ content: /* @__PURE__ */ (0, import_jsx_runtime44.jsx)("p", { className: "px-6 py-2.5 text-red-600", children: "Could not load this song." })
2654
2823
  };
2655
2824
  case "success": {
2656
2825
  const { song } = result;
@@ -2658,7 +2827,7 @@ function useSongPanel({
2658
2827
  return {
2659
2828
  ...panel,
2660
2829
  title: song.title,
2661
- content: /* @__PURE__ */ (0, import_jsx_runtime43.jsx)(
2830
+ content: /* @__PURE__ */ (0, import_jsx_runtime44.jsx)(
2662
2831
  SongPanelContent,
2663
2832
  {
2664
2833
  song,
@@ -2677,29 +2846,29 @@ function useSongPanel({
2677
2846
 
2678
2847
  // src/genre-tree/SizeMetricToggle.tsx
2679
2848
  var import_md2 = require("react-icons/md");
2680
- var import_ui18 = require("@behindthemusictree/ui");
2681
- var import_jsx_runtime44 = require("react/jsx-runtime");
2849
+ var import_ui19 = require("@behindthemusictree/ui");
2850
+ var import_jsx_runtime45 = require("react/jsx-runtime");
2682
2851
  function SizeMetricToggle({
2683
2852
  value,
2684
2853
  onChange
2685
2854
  }) {
2686
- return /* @__PURE__ */ (0, import_jsx_runtime44.jsx)(
2687
- import_ui18.SegmentedControl,
2855
+ return /* @__PURE__ */ (0, import_jsx_runtime45.jsx)(
2856
+ import_ui19.SegmentedControl,
2688
2857
  {
2689
2858
  variant: "floating",
2690
2859
  "aria-label": "Size nodes by",
2691
2860
  value,
2692
2861
  onChange,
2693
2862
  options: [
2694
- { value: "songs", label: /* @__PURE__ */ (0, import_jsx_runtime44.jsx)(import_md2.MdMusicNote, { size: 18, "aria-hidden": true }), "aria-label": "Size by songs", title: "Node size by song count" },
2695
- { value: "views", label: /* @__PURE__ */ (0, import_jsx_runtime44.jsx)(import_md2.MdVisibility, { size: 18, "aria-hidden": true }), "aria-label": "Size by views", title: "Node size by song views" }
2863
+ { value: "songs", label: /* @__PURE__ */ (0, import_jsx_runtime45.jsx)(import_md2.MdMusicNote, { size: 18, "aria-hidden": true }), "aria-label": "Size by songs", title: "Node size by song count" },
2864
+ { value: "views", label: /* @__PURE__ */ (0, import_jsx_runtime45.jsx)(import_md2.MdVisibility, { size: 18, "aria-hidden": true }), "aria-label": "Size by views", title: "Node size by song views" }
2696
2865
  ]
2697
2866
  }
2698
2867
  );
2699
2868
  }
2700
2869
 
2701
2870
  // src/genre-tree/GenreTreeView.tsx
2702
- var import_jsx_runtime45 = require("react/jsx-runtime");
2871
+ var import_jsx_runtime46 = require("react/jsx-runtime");
2703
2872
  var NO_POP_ROOT_TITLE = "This genre tree has no 'Mainstream Pop' root yet";
2704
2873
  var REGIONS_CLASS = "[--gtv-center-left:var(--layout-center-left)] [--gtv-center-right:var(--layout-center-right)] [--gtv-center-top:var(--layout-band-top)]";
2705
2874
  var noop = () => {
@@ -2712,8 +2881,8 @@ function GenreTreeView({
2712
2881
  getBackendBaseUrl,
2713
2882
  additionalActions,
2714
2883
  viewMode: controlledViewMode,
2715
- selectedNodeId: controlledSelectedNodeId,
2716
- onSelectedNodeIdChange,
2884
+ selectedGenreUuid: controlledSelectedGenreUuid,
2885
+ onSelectedGenreUuidChange,
2717
2886
  defaultViewport,
2718
2887
  viewportRestore,
2719
2888
  onViewportChange,
@@ -2761,21 +2930,21 @@ function GenreTreeView({
2761
2930
  placement = "default",
2762
2931
  ...focus
2763
2932
  }) {
2764
- const getShareUrlRef = (0, import_react17.useRef)(getShareUrl);
2933
+ const getShareUrlRef = (0, import_react18.useRef)(getShareUrl);
2765
2934
  getShareUrlRef.current = getShareUrl;
2766
- const [internalArtistUuid, setInternalArtistUuid] = (0, import_react17.useState)(null);
2935
+ const [internalArtistUuid, setInternalArtistUuid] = (0, import_react18.useState)(null);
2767
2936
  const isArtistControlled = controlledArtistUuid !== void 0;
2768
2937
  const artistUuid = isArtistControlled ? controlledArtistUuid : internalArtistUuid;
2769
- const [internalSongUuid, setInternalSongUuid] = (0, import_react17.useState)(null);
2938
+ const [internalSongUuid, setInternalSongUuid] = (0, import_react18.useState)(null);
2770
2939
  const isSongControlled = controlledSongUuid !== void 0;
2771
2940
  const songUuid = isSongControlled ? controlledSongUuid : internalSongUuid;
2772
2941
  const changeSong = (uuid) => {
2773
2942
  if (!isSongControlled) setInternalSongUuid(uuid);
2774
2943
  onSongUuidChange?.(uuid);
2775
2944
  };
2776
- const [internalSizeMetric, setInternalSizeMetric] = (0, import_react17.useState)("songs");
2945
+ const [internalSizeMetric, setInternalSizeMetric] = (0, import_react18.useState)("songs");
2777
2946
  const sizeMetric = controlledSizeMetric ?? internalSizeMetric;
2778
- const deferredSizeMetric = (0, import_react17.useDeferredValue)(sizeMetric);
2947
+ const deferredSizeMetric = (0, import_react18.useDeferredValue)(sizeMetric);
2779
2948
  const changeSizeMetric = (metric) => {
2780
2949
  if (controlledSizeMetric === void 0) setInternalSizeMetric(metric);
2781
2950
  onSizeMetricChange?.(metric);
@@ -2790,6 +2959,8 @@ function GenreTreeView({
2790
2959
  genrePlaylistByNodeId,
2791
2960
  visibleGenrePlaylists,
2792
2961
  groupedGenrePlaylistsByRoot,
2962
+ selectedGenreUuid,
2963
+ selectGenre,
2793
2964
  selectedNodeId,
2794
2965
  selectedGenrePlaylist,
2795
2966
  selectNode,
@@ -2802,17 +2973,17 @@ function GenreTreeView({
2802
2973
  allowWheelRotation: internalAllowWheelRotation,
2803
2974
  toggleWheelRotation,
2804
2975
  onNodeHover: handleNodeHover
2805
- } = (0, import_genre_tree27.useGenreTreeViewState)({
2976
+ } = (0, import_genre_tree28.useGenreTreeViewState)({
2806
2977
  scope,
2807
2978
  getBackendBaseUrl,
2808
2979
  viewMode: controlledViewMode,
2809
- selectedNodeId: controlledSelectedNodeId,
2810
- onSelectedNodeIdChange,
2980
+ selectedGenreUuid: controlledSelectedGenreUuid,
2981
+ onSelectedGenreUuidChange,
2811
2982
  criteriaOverviewSchema,
2812
2983
  artistUuid
2813
2984
  });
2814
2985
  const restoreRevision = viewportRestore?.revision;
2815
- const [stackedRestoreOwner, setStackedRestoreOwner] = (0, import_react17.useState)({ revision: restoreRevision, rootUuid: selectedGenrePlaylist?.root.uuid });
2986
+ const [stackedRestoreOwner, setStackedRestoreOwner] = (0, import_react18.useState)({ revision: restoreRevision, rootUuid: selectedGenrePlaylist?.root.uuid });
2816
2987
  let restoreRootUuid = stackedRestoreOwner.rootUuid;
2817
2988
  if (stackedRestoreOwner.revision !== restoreRevision) {
2818
2989
  restoreRootUuid = selectedGenrePlaylist?.root.uuid;
@@ -2821,36 +2992,36 @@ function GenreTreeView({
2821
2992
  restoreRootUuid = selectedGenrePlaylist.root.uuid;
2822
2993
  setStackedRestoreOwner({ revision: restoreRevision, rootUuid: restoreRootUuid });
2823
2994
  }
2824
- const isGenreUpdating = (0, import_react_query3.useIsMutating)({ mutationKey: import_genre_tree27.genreUpdateMutationKey }) > 0;
2825
- const { mutate: updateGenre } = (0, import_genre_tree27.useUpdateGenre)(scope, getBackendBaseUrl);
2995
+ const isGenreUpdating = (0, import_react_query3.useIsMutating)({ mutationKey: import_genre_tree28.genreUpdateMutationKey }) > 0;
2996
+ const { mutate: updateGenre } = (0, import_genre_tree28.useUpdateGenre)(scope, getBackendBaseUrl);
2826
2997
  const busy = deferredSizeMetric !== sizeMetric || isTreeFetching || isGenreUpdating;
2827
2998
  const isArtistFiltered = artistUuid !== null;
2828
2999
  const canSizeByViews = !isArtistFiltered && hasViewsCount(visibleGenrePlaylists);
2829
- const artistResult = (0, import_genre_tree27.useArtist)(artistUuid, artistSearchBaseUrl ?? getBackendBaseUrl);
3000
+ const artistResult = (0, import_genre_tree28.useArtist)(artistUuid, artistSearchBaseUrl ?? getBackendBaseUrl);
2830
3001
  const filterArtistName = artistResult.status === "success" ? artistResult.artist.name : null;
2831
- const filterArtist = (0, import_react17.useMemo)(
3002
+ const filterArtist = (0, import_react18.useMemo)(
2832
3003
  () => artistUuid !== null && filterArtistName !== null ? { uuid: artistUuid, name: filterArtistName } : null,
2833
3004
  [artistUuid, filterArtistName]
2834
3005
  );
2835
3006
  const hasSelectedGenreNode = selectedGenrePlaylist !== void 0 && visibleGenrePlaylists.some(({ uuid }) => uuid === selectedGenrePlaylist.uuid);
2836
3007
  const playback = useGenrePlaylistPlayback({ scope, genrePlaylists: visibleGenrePlaylists, artist: filterArtist });
2837
3008
  const openArtist = (uuid) => {
2838
- if (selectedNodeId !== null) clearSelection();
3009
+ if (selectedGenreUuid !== null) clearSelection();
2839
3010
  if (songUuid !== null) changeSong(null);
2840
3011
  changeArtist(uuid);
2841
3012
  };
2842
3013
  const openSong = (uuid) => {
2843
- if (selectedNodeId !== null) clearSelection();
3014
+ if (selectedGenreUuid !== null) clearSelection();
2844
3015
  changeSong(uuid);
2845
3016
  };
2846
- const openSongRef = (0, import_react17.useRef)(openSong);
3017
+ const openSongRef = (0, import_react18.useRef)(openSong);
2847
3018
  openSongRef.current = openSong;
2848
- const resolvedRenderSongLink = (0, import_react17.useCallback)(
2849
- (song) => renderSongLink ? renderSongLink(song) : /* @__PURE__ */ (0, import_jsx_runtime45.jsx)("button", { type: "button", onClick: () => openSongRef.current(song.uuid), className: "hover:underline", children: song.title }),
3019
+ const resolvedRenderSongLink = (0, import_react18.useCallback)(
3020
+ (song) => renderSongLink ? renderSongLink(song) : /* @__PURE__ */ (0, import_jsx_runtime46.jsx)("button", { type: "button", onClick: () => openSongRef.current(song.uuid), className: "hover:underline", children: song.title }),
2850
3021
  [renderSongLink]
2851
3022
  );
2852
- const [expandedSongUuid, setExpandedSongUuid] = (0, import_react17.useState)(null);
2853
- const [expandedArtistUuid, setExpandedArtistUuid] = (0, import_react17.useState)(null);
3023
+ const [expandedSongUuid, setExpandedSongUuid] = (0, import_react18.useState)(null);
3024
+ const [expandedArtistUuid, setExpandedArtistUuid] = (0, import_react18.useState)(null);
2854
3025
  const artistBaseUrl = artistSearchBaseUrl ?? getBackendBaseUrl;
2855
3026
  const songBaseUrl = getBackendBaseUrl;
2856
3027
  const artistPanel = useArtistPanel({
@@ -2858,7 +3029,7 @@ function GenreTreeView({
2858
3029
  scope,
2859
3030
  getBackendBaseUrl: artistBaseUrl,
2860
3031
  getShareUrl: getShareUrlRef.current,
2861
- renderArtistLink: renderArtistLink ?? ((artist) => artist.isSpecialPurpose ? artist.name : /* @__PURE__ */ (0, import_jsx_runtime45.jsx)("button", { type: "button", onClick: () => openArtist(artist.uuid), className: "hover:underline", children: artist.name })),
3032
+ renderArtistLink: renderArtistLink ?? ((artist) => artist.isSpecialPurpose ? artist.name : /* @__PURE__ */ (0, import_jsx_runtime46.jsx)("button", { type: "button", onClick: () => openArtist(artist.uuid), className: "hover:underline", children: artist.name })),
2862
3033
  expanded: controlledArtistExpanded ?? (artistUuid !== null && expandedArtistUuid === artistUuid),
2863
3034
  onExpandedChange: (on) => {
2864
3035
  if (controlledArtistExpanded === void 0) setExpandedArtistUuid(on ? artistUuid : null);
@@ -2867,7 +3038,7 @@ function GenreTreeView({
2867
3038
  renderSongLink: resolvedRenderSongLink,
2868
3039
  onClose: () => changeArtist(null)
2869
3040
  });
2870
- const resolvedRenderArtistLink = renderArtistLink ?? ((artist) => artist.isSpecialPurpose ? artist.name : /* @__PURE__ */ (0, import_jsx_runtime45.jsx)("button", { type: "button", onClick: () => openArtist(artist.uuid), className: "hover:underline", children: artist.name }));
3041
+ const resolvedRenderArtistLink = renderArtistLink ?? ((artist) => artist.isSpecialPurpose ? artist.name : /* @__PURE__ */ (0, import_jsx_runtime46.jsx)("button", { type: "button", onClick: () => openArtist(artist.uuid), className: "hover:underline", children: artist.name }));
2871
3042
  const getGenrePath = (genre) => {
2872
3043
  const path = [];
2873
3044
  let current = visibleGenrePlaylists.find((playlist) => playlist.criteria?.uuid === genre.uuid);
@@ -2885,9 +3056,9 @@ function GenreTreeView({
2885
3056
  getShareUrl: getShareUrlRef.current,
2886
3057
  renderArtistLink: resolvedRenderArtistLink,
2887
3058
  getGenrePath,
2888
- onSelectGenre: (genrePlaylist) => {
3059
+ onSelectGenre: (genrePlaylist2) => {
2889
3060
  changeSong(null);
2890
- selectGenrePlaylist(genrePlaylist);
3061
+ selectGenrePlaylist(genrePlaylist2);
2891
3062
  },
2892
3063
  expanded: controlledSongExpanded ?? (songUuid !== null && expandedSongUuid === songUuid),
2893
3064
  onExpandedChange: (on) => {
@@ -2896,11 +3067,11 @@ function GenreTreeView({
2896
3067
  },
2897
3068
  onClose: () => changeSong(null)
2898
3069
  });
2899
- const builtInSongPanel = overlayPanel || hasSelectedGenreNode ? void 0 : songPanel;
2900
- const builtInArtistPanel = overlayPanel || hasSelectedGenreNode || builtInSongPanel ? void 0 : artistPanel;
3070
+ const builtInSongPanel = overlayPanel || selectedGenreUuid !== null ? void 0 : songPanel;
3071
+ const builtInArtistPanel = overlayPanel || selectedGenreUuid !== null || builtInSongPanel ? void 0 : artistPanel;
2901
3072
  const builtInPanel = builtInSongPanel ?? builtInArtistPanel;
2902
- const [uncontrolledCollapsed, setUncontrolledCollapsed] = (0, import_react17.useState)(false);
2903
- const [uncontrolledExpanded, setUncontrolledExpanded] = (0, import_react17.useState)(false);
3073
+ const [uncontrolledCollapsed, setUncontrolledCollapsed] = (0, import_react18.useState)(false);
3074
+ const [uncontrolledExpanded, setUncontrolledExpanded] = (0, import_react18.useState)(false);
2904
3075
  const isCollapsed = collapsed ?? uncontrolledCollapsed;
2905
3076
  const isExpanded = expanded ?? uncontrolledExpanded;
2906
3077
  const changeCollapsed = (next) => {
@@ -2911,155 +3082,114 @@ function GenreTreeView({
2911
3082
  setUncontrolledExpanded(next);
2912
3083
  onExpandedChange?.(next);
2913
3084
  };
2914
- (0, import_react17.useEffect)(() => {
3085
+ (0, import_react18.useEffect)(() => {
2915
3086
  setUncontrolledCollapsed(false);
2916
- }, [selectedNodeId, artistUuid, songUuid]);
2917
- const [isWheelRevealed, setIsWheelRevealed] = (0, import_react17.useState)(false);
3087
+ }, [selectedGenreUuid, artistUuid, songUuid]);
3088
+ const [isWheelRevealed, setIsWheelRevealed] = (0, import_react18.useState)(false);
2918
3089
  const usesHandoff = viewMode === "wheel" || viewMode === "pop-core";
2919
3090
  const isTreeReady = !isLoading && (!usesHandoff || isWheelRevealed);
2920
- const [resolveStyle, setResolveStyle] = (0, import_react17.useState)(null);
2921
- const handleNodeStyleResolverChange = (0, import_react17.useCallback)(
3091
+ const [resolveStyle, setResolveStyle] = (0, import_react18.useState)(null);
3092
+ const handleNodeStyleResolverChange = (0, import_react18.useCallback)(
2922
3093
  (resolver) => setResolveStyle(() => resolver),
2923
3094
  []
2924
3095
  );
2925
- const panelRef = (0, import_react17.useRef)(null);
2926
- const obscuredInset = (0, import_ui19.useObscuredInset)(panelRef);
3096
+ const panelRef = (0, import_react18.useRef)(null);
3097
+ const obscuredInset = (0, import_ui20.useObscuredInset)(panelRef);
2927
3098
  const hostPanel = { obscuredInset, onNodeStyleResolverChange: handleNodeStyleResolverChange };
2928
3099
  const nodeMetric = isArtistFiltered || !canSizeByViews ? "songs" : deferredSizeMetric;
2929
- const treeNodes = (0, import_react17.useMemo)(() => toTreeNodes(visibleGenrePlaylists, nodeMetric), [visibleGenrePlaylists, nodeMetric]);
2930
- const relations = (0, import_genre_tree_view4.useGenreNodeRelations)(
3100
+ const treeNodes = (0, import_react18.useMemo)(() => toTreeNodes(visibleGenrePlaylists, nodeMetric), [visibleGenrePlaylists, nodeMetric]);
3101
+ const relations = (0, import_genre_tree_view5.useGenreNodeRelations)(
2931
3102
  treeNodes,
2932
3103
  !overlayPanel && hasSelectedGenreNode ? selectedNodeId : null,
2933
3104
  resolveStyle
2934
3105
  );
2935
- const [selectionHistory, setSelectionHistory] = (0, import_react17.useState)({
2936
- current: selectedNodeId,
2937
- previous: null
2938
- });
2939
- if (selectionHistory.current !== selectedNodeId) {
2940
- setSelectionHistory({
2941
- current: selectedNodeId,
2942
- previous: selectionHistory.current ?? selectionHistory.previous
2943
- });
3106
+ const [selectionHistory, setSelectionHistory] = (0, import_react18.useState)({ current: selectedGenreUuid, previous: null });
3107
+ if (selectionHistory.current !== selectedGenreUuid) {
3108
+ setSelectionHistory({ current: selectedGenreUuid, previous: selectionHistory.current ?? selectionHistory.previous });
2944
3109
  }
2945
- const previousPlaylist = selectionHistory.previous !== null && selectionHistory.previous !== selectedNodeId ? genrePlaylistByNodeId.get(selectionHistory.previous) : void 0;
2946
- const handlePrevious = previousPlaylist && visibleGenrePlaylists.includes(previousPlaylist) ? () => handleGenreSearchSelect(previousPlaylist) : void 0;
3110
+ const handlePrevious = selectionHistory.previous ? () => selectGenre(selectionHistory.previous) : void 0;
2947
3111
  const { showPopup, hidePopup } = (0, import_popup6.usePopup)();
3112
+ const reopenPanel = () => {
3113
+ setUncontrolledCollapsed(false);
3114
+ onCollapsedChange?.(false);
3115
+ };
2948
3116
  const handleSelectedNodeChange = (node) => {
3117
+ reopenPanel();
2949
3118
  if (node !== null && songUuid !== null) changeSong(null);
2950
3119
  selectNode(node);
2951
3120
  };
2952
3121
  const handleGenreSearchClear = clearSelection;
2953
- const handleGenreSearchSelect = (genrePlaylist) => {
3122
+ const handleGenreSearchSelect = (genrePlaylist2) => {
3123
+ reopenPanel();
2954
3124
  if (songUuid !== null) changeSong(null);
2955
- selectGenrePlaylist(genrePlaylist);
3125
+ selectGenrePlaylist(genrePlaylist2);
2956
3126
  };
2957
3127
  const selectedUuid = selectedGenrePlaylist?.uuid;
2958
- const selectedChildren = selectedUuid ? visibleGenrePlaylists.filter((genrePlaylist) => genrePlaylist.parent?.uuid === selectedUuid) : [];
3128
+ const selectedChildren = selectedUuid ? visibleGenrePlaylists.filter((genrePlaylist2) => genrePlaylist2.parent?.uuid === selectedUuid) : [];
2959
3129
  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;
2960
3130
  const handleGenreSearchZoomIn = () => {
2961
3131
  if (selectedUuid) focus.onFocusGenreChange?.(selectedUuid, "zoom-in");
2962
3132
  };
2963
- const zoomInRef = (0, import_react17.useRef)(void 0);
3133
+ const zoomInRef = (0, import_react18.useRef)(void 0);
2964
3134
  zoomInRef.current = canZoomIn ? handleGenreSearchZoomIn : void 0;
2965
- const selectGenreFromPanel = (nodeId) => {
2966
- const genrePlaylist = genrePlaylistByNodeId.get(nodeId);
2967
- if (genrePlaylist) handleGenreSearchSelect(genrePlaylist);
2968
- };
2969
- const renderGenreBody = (relation) => {
2970
- const { node } = relation;
2971
- const family = /* @__PURE__ */ (0, import_jsx_runtime45.jsx)(
2972
- import_genre_tree_view4.GenreNodeFamily,
2973
- {
2974
- node,
2975
- fill: relation.fill,
2976
- textColor: relation.textColor,
2977
- parentNode: relation.parentNode,
2978
- childNodes: relation.childNodes,
2979
- onSelectNode: selectGenreFromPanel
2980
- }
2981
- );
2982
- const ancestors = /* @__PURE__ */ (0, import_jsx_runtime45.jsx)(import_genre_tree_view4.GenreNodeAncestors, { ancestorNodes: relation.ancestorNodes, onSelectNode: selectGenreFromPanel });
2983
- const genrePlaylist = genrePlaylistByNodeId.get(node.id);
2984
- const criteria = genrePlaylist?.criteria;
2985
- if (!genrePlaylist || !criteria) return { primary: /* @__PURE__ */ (0, import_jsx_runtime45.jsxs)(import_jsx_runtime45.Fragment, { children: [
2986
- family,
2987
- ancestors
2988
- ] }) };
2989
- const genreUuid = criteria.uuid;
2990
- const parentPlaylist = visibleGenrePlaylists.find(({ uuid }) => uuid === genrePlaylist.parent?.uuid);
2991
- const canToggleSide = !readOnly && parentPlaylist !== void 0 && parentPlaylist.parent === null;
2992
- const side = criteria.side ?? "core";
2993
- const shareTarget = {
2994
- kind: "genre",
2995
- genrePlaylistUuid: genrePlaylist.uuid,
2996
- name: criteria.name,
2997
- scope,
2998
- artistUuid
2999
- };
3000
- const shareUrl = getShareUrlRef.current?.(shareTarget) ?? void 0;
3001
- const popSibling = canToggleSide && side === "core" ? visibleGenrePlaylists.find(
3002
- (sibling) => sibling.parent?.uuid === parentPlaylist.uuid && sibling.uuid !== genrePlaylist.uuid && sibling.criteria?.side === "pop"
3003
- ) : void 0;
3004
- return {
3005
- primary: /* @__PURE__ */ (0, import_jsx_runtime45.jsxs)(import_jsx_runtime45.Fragment, { children: [
3006
- /* @__PURE__ */ (0, import_jsx_runtime45.jsx)(
3007
- GenreInfoPanelBody,
3135
+ const genrePlaylist = selectedGenrePlaylist;
3136
+ const criteria = genrePlaylist?.criteria;
3137
+ const parentPlaylist = visibleGenrePlaylists.find(({ uuid }) => uuid === genrePlaylist?.parent?.uuid);
3138
+ const canToggleSide = !readOnly && criteria !== void 0 && criteria !== null && parentPlaylist !== void 0 && parentPlaylist.parent === null;
3139
+ const side = criteria?.side ?? "core";
3140
+ const popSibling = canToggleSide && side === "core" ? visibleGenrePlaylists.find((sibling) => sibling.parent?.uuid === parentPlaylist.uuid && sibling.uuid !== genrePlaylist?.uuid && sibling.criteria?.side === "pop") : void 0;
3141
+ const shareTarget = selectedGenreUuid ? { kind: "genre", genreUuid: selectedGenreUuid, name: criteria?.name ?? "Genre", scope, artistUuid } : void 0;
3142
+ const shareUrl = shareTarget ? getShareUrlRef.current?.(shareTarget) ?? void 0 : void 0;
3143
+ const genreContent = useGenreDetailContent({
3144
+ genreUuid: selectedGenreUuid,
3145
+ scope,
3146
+ getBackendBaseUrl,
3147
+ onSelectGenre: (uuid) => {
3148
+ reopenPanel();
3149
+ selectGenre(uuid);
3150
+ },
3151
+ expanded: isExpanded,
3152
+ playback,
3153
+ genrePlaylist: selectedGenrePlaylist,
3154
+ band: {
3155
+ ...relations ? { node: relations.node } : {},
3156
+ shareTarget: shareUrl ? shareTarget : void 0,
3157
+ shareUrl,
3158
+ onZoomIn: zoomInRef.current,
3159
+ onAddChild: readOnly || !criteria ? void 0 : () => handleGenreCreationAction(criteria),
3160
+ onRename: readOnly || !criteria ? void 0 : () => handleGenreRenameAction(criteria),
3161
+ onChangeParent: readOnly || !genrePlaylist || !criteria ? void 0 : () => showPopup(
3162
+ /* @__PURE__ */ (0, import_jsx_runtime46.jsx)(
3163
+ ChangeParentPopup,
3008
3164
  {
3009
- family: /* @__PURE__ */ (0, import_jsx_runtime45.jsxs)(import_jsx_runtime45.Fragment, { children: [
3010
- family,
3011
- ancestors
3012
- ] }),
3013
- genreUuid,
3014
- genrePlaylist,
3015
- catalogue: genrePlaylist.catalogue,
3016
- wikidataId: criteria.wikidataId,
3165
+ genrePlaylist: { ...genrePlaylist, criteria },
3166
+ genrePlaylists: visibleGenrePlaylists,
3017
3167
  scope,
3018
3168
  getBackendBaseUrl,
3019
- playback,
3020
- band: {
3021
- node,
3022
- shareTarget: shareUrl ? shareTarget : void 0,
3023
- shareUrl,
3024
- onZoomIn: zoomInRef.current,
3025
- onAddChild: readOnly ? void 0 : () => handleGenreCreationAction(criteria),
3026
- onRename: readOnly ? void 0 : () => handleGenreRenameAction(criteria),
3027
- onChangeParent: readOnly ? void 0 : () => showPopup(
3028
- /* @__PURE__ */ (0, import_jsx_runtime45.jsx)(
3029
- ChangeParentPopup,
3030
- {
3031
- genrePlaylist: { ...genrePlaylist, criteria },
3032
- genrePlaylists: visibleGenrePlaylists,
3033
- scope,
3034
- getBackendBaseUrl,
3035
- onClose: hidePopup,
3036
- onParentChanged
3037
- }
3038
- )
3039
- ),
3040
- side,
3041
- onToggleSide: canToggleSide ? () => updateGenre({ uuid: genreUuid, data: { side: side === "pop" ? "core" : "pop" } }) : void 0,
3042
- toggleSideDisabledReason: popSibling ? `${popSibling.name} is already the pop side of ${parentPlaylist?.name}` : void 0,
3043
- additionalActions
3044
- },
3045
- extras: {
3046
- criteriaOverviewSchema,
3047
- renderGenreDetailExtras,
3048
- renderArtistLink,
3049
- renderSongLink: resolvedRenderSongLink,
3050
- artist: filterArtist,
3051
- renderSummary,
3052
- renderEssentialTracks,
3053
- renderSongLeading,
3054
- renderSongActions,
3055
- renderTracksToolbar
3056
- }
3169
+ onClose: hidePopup,
3170
+ onParentChanged
3057
3171
  }
3058
- ),
3059
- renderDangerZone?.(node)
3060
- ] })
3061
- };
3062
- };
3172
+ )
3173
+ ),
3174
+ side,
3175
+ onToggleSide: canToggleSide && criteria ? () => updateGenre({ uuid: criteria.uuid, data: { side: side === "pop" ? "core" : "pop" } }) : void 0,
3176
+ toggleSideDisabledReason: popSibling ? `${popSibling.name} is already the pop side of ${parentPlaylist?.name}` : void 0,
3177
+ additionalActions
3178
+ },
3179
+ extras: {
3180
+ criteriaOverviewSchema,
3181
+ renderGenreDetailExtras,
3182
+ renderArtistLink,
3183
+ renderSongLink: resolvedRenderSongLink,
3184
+ artist: filterArtist,
3185
+ renderSummary,
3186
+ renderEssentialTracks,
3187
+ renderSongLeading,
3188
+ renderSongActions,
3189
+ renderTracksToolbar
3190
+ },
3191
+ trailing: relations ? renderDangerZone?.(relations.node) : void 0
3192
+ });
3063
3193
  const isRotationControlled = controlledAllowWheelRotation !== void 0;
3064
3194
  const allowWheelRotation = controlledAllowWheelRotation ?? internalAllowWheelRotation;
3065
3195
  let activePanel = null;
@@ -3073,16 +3203,16 @@ function GenreTreeView({
3073
3203
  onClose: overlayPanel.onClose,
3074
3204
  compact: false
3075
3205
  };
3076
- } else if (relations) {
3077
- const { primary } = renderGenreBody(relations);
3206
+ } else if (selectedGenreUuid) {
3078
3207
  activePanel = {
3079
- title: relations.node.name,
3080
- content: primary,
3208
+ title: genreContent.title,
3209
+ content: genreContent.content,
3210
+ aside: genreContent.aside,
3081
3211
  expanded: isExpanded,
3082
3212
  onExpandedChange: changeExpanded,
3083
3213
  onClose: clearSelection,
3084
- compact: true,
3085
- headerAccent: { background: relations.fill, color: relations.textColor }
3214
+ compact: false,
3215
+ headerAccent: relations ? { background: relations.fill, color: relations.textColor } : genreContent.headerAccent
3086
3216
  };
3087
3217
  } else if (builtInPanel) {
3088
3218
  activePanel = {
@@ -3095,36 +3225,36 @@ function GenreTreeView({
3095
3225
  compact: true
3096
3226
  };
3097
3227
  }
3098
- const isPanelShown = isTreeReady && activePanel !== null;
3228
+ const isPanelShown = activePanel !== null && (selectedGenreUuid !== null || isTreeReady);
3099
3229
  const detailTab = placement === "regions" && activePanel?.onExpandedChange ? {
3100
3230
  edge: activePanel.expanded ? { label: "Shrink panel", onClick: () => activePanel.onExpandedChange?.(false) } : isCollapsed ? null : { label: "Hide panel", onClick: () => changeCollapsed(true) },
3101
3231
  center: activePanel.expanded ? null : isCollapsed ? { label: "Show panel", onClick: () => changeCollapsed(false) } : activePanel.onExpandedChange ? { label: "Extend panel", onClick: () => activePanel.onExpandedChange?.(true) } : null
3102
3232
  } : void 0;
3103
- const expandButton = placement === "default" && activePanel?.onExpandedChange ? /* @__PURE__ */ (0, import_jsx_runtime45.jsx)(
3104
- import_ui19.IconButton,
3233
+ const expandButton = placement === "default" && activePanel?.onExpandedChange ? /* @__PURE__ */ (0, import_jsx_runtime46.jsx)(
3234
+ import_ui20.IconButton,
3105
3235
  {
3106
3236
  "aria-label": activePanel.expanded ? "Shrink panel" : "Extend panel",
3107
3237
  style: activePanel.headerAccent && { color: "inherit" },
3108
3238
  onClick: () => activePanel.onExpandedChange?.(!activePanel.expanded),
3109
- children: activePanel.expanded ? /* @__PURE__ */ (0, import_jsx_runtime45.jsx)(import_lucide_react16.Minimize2, { "aria-hidden": "true", className: "h-4 w-4" }) : /* @__PURE__ */ (0, import_jsx_runtime45.jsx)(import_lucide_react16.Maximize2, { "aria-hidden": "true", className: "h-4 w-4" })
3239
+ children: activePanel.expanded ? /* @__PURE__ */ (0, import_jsx_runtime46.jsx)(import_lucide_react17.Minimize2, { "aria-hidden": "true", className: "h-4 w-4" }) : /* @__PURE__ */ (0, import_jsx_runtime46.jsx)(import_lucide_react17.Maximize2, { "aria-hidden": "true", className: "h-4 w-4" })
3110
3240
  }
3111
3241
  ) : null;
3112
3242
  const renderArtistFilter = () => {
3113
3243
  if (isLoading || !filterArtist) return null;
3114
- return /* @__PURE__ */ (0, import_jsx_runtime45.jsxs)("span", { className: "inline-flex items-center overflow-hidden rounded-full bg-[var(--color-neutral-100)] text-sm", children: [
3115
- /* @__PURE__ */ (0, import_jsx_runtime45.jsx)(
3244
+ return /* @__PURE__ */ (0, import_jsx_runtime46.jsxs)("span", { className: "inline-flex items-center overflow-hidden rounded-full bg-[var(--color-neutral-100)] text-sm", children: [
3245
+ /* @__PURE__ */ (0, import_jsx_runtime46.jsx)(
3116
3246
  "button",
3117
3247
  {
3118
3248
  type: "button",
3119
3249
  className: "py-1 pl-3 pr-2 [@media(pointer:coarse)]:min-h-[var(--layout-touch-target)] hover:underline",
3120
3250
  "aria-label": `Show artist ${filterArtist.name}`,
3121
3251
  onClick: () => {
3122
- if (selectedNodeId !== null) clearSelection();
3252
+ if (selectedGenreUuid !== null) clearSelection();
3123
3253
  },
3124
3254
  children: filterArtist.name
3125
3255
  }
3126
3256
  ),
3127
- /* @__PURE__ */ (0, import_jsx_runtime45.jsx)(
3257
+ /* @__PURE__ */ (0, import_jsx_runtime46.jsx)(
3128
3258
  "button",
3129
3259
  {
3130
3260
  type: "button",
@@ -3136,8 +3266,8 @@ function GenreTreeView({
3136
3266
  )
3137
3267
  ] });
3138
3268
  };
3139
- const actions = /* @__PURE__ */ (0, import_jsx_runtime45.jsxs)(import_jsx_runtime45.Fragment, { children: [
3140
- !isLoading && /* @__PURE__ */ (0, import_jsx_runtime45.jsx)(
3269
+ const actions = /* @__PURE__ */ (0, import_jsx_runtime46.jsxs)(import_jsx_runtime46.Fragment, { children: [
3270
+ !isLoading && /* @__PURE__ */ (0, import_jsx_runtime46.jsx)(
3141
3271
  GenreTreeSearch,
3142
3272
  {
3143
3273
  artistBaseUrl,
@@ -3155,7 +3285,12 @@ function GenreTreeView({
3155
3285
  SearchGroups,
3156
3286
  genrePlaylists: visibleGenrePlaylists,
3157
3287
  onSelect: handleGenreSearchSelect,
3158
- selectedName: selectedGenrePlaylist?.name ?? (typeof builtInPanel?.title === "string" ? builtInPanel.title : null),
3288
+ onSelectGenre: (uuid) => {
3289
+ reopenPanel();
3290
+ if (songUuid) changeSong(null);
3291
+ selectGenre(uuid);
3292
+ },
3293
+ selectedName: (selectedGenreUuid ? genreContent.title : null) ?? (typeof builtInPanel?.title === "string" ? builtInPanel.title : null),
3159
3294
  onClear: builtInSongPanel ? () => changeSong(null) : builtInArtistPanel ? () => changeArtist(null) : handleGenreSearchClear,
3160
3295
  onPrevious: handlePrevious,
3161
3296
  onQueryChange: onSearchQueryChange,
@@ -3163,10 +3298,10 @@ function GenreTreeView({
3163
3298
  extraGroups: extraSearchGroups
3164
3299
  }
3165
3300
  ),
3166
- !isLoading && filterArtist && /* @__PURE__ */ (0, import_jsx_runtime45.jsx)("div", { className: "hidden w-full justify-center max-md:flex", children: renderArtistFilter() }),
3167
- /* @__PURE__ */ (0, import_jsx_runtime45.jsxs)("div", { className: "contents max-md:flex max-md:w-full max-md:items-center max-md:gap-2 max-md:overflow-x-auto max-md:[scrollbar-width:none] max-md:[&>*]:shrink-0", children: [
3168
- !isLoading && !isControlled && /* @__PURE__ */ (0, import_jsx_runtime45.jsx)(
3169
- import_ui19.SegmentedControl,
3301
+ !isLoading && filterArtist && /* @__PURE__ */ (0, import_jsx_runtime46.jsx)("div", { className: "hidden w-full justify-center max-md:flex", children: renderArtistFilter() }),
3302
+ /* @__PURE__ */ (0, import_jsx_runtime46.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: [
3303
+ !isLoading && !isControlled && /* @__PURE__ */ (0, import_jsx_runtime46.jsx)(
3304
+ import_ui20.SegmentedControl,
3170
3305
  {
3171
3306
  "aria-label": "Tree view mode",
3172
3307
  value: viewMode,
@@ -3179,14 +3314,14 @@ function GenreTreeView({
3179
3314
  ]
3180
3315
  }
3181
3316
  ),
3182
- !isLoading && showDisplayOptions && /* @__PURE__ */ (0, import_jsx_runtime45.jsx)(
3317
+ !isLoading && showDisplayOptions && /* @__PURE__ */ (0, import_jsx_runtime46.jsx)(
3183
3318
  "div",
3184
3319
  {
3185
3320
  className: "flex items-center gap-1",
3186
3321
  role: "group",
3187
3322
  "aria-label": "Tree display options",
3188
- children: !isRotationControlled && viewMode !== "stacked" && viewMode !== "outline" && /* @__PURE__ */ (0, import_jsx_runtime45.jsx)(
3189
- import_ui19.Button,
3323
+ children: !isRotationControlled && viewMode !== "stacked" && viewMode !== "outline" && /* @__PURE__ */ (0, import_jsx_runtime46.jsx)(
3324
+ import_ui20.Button,
3190
3325
  {
3191
3326
  variant: allowWheelRotation ? "default" : "outline",
3192
3327
  size: "sm",
@@ -3196,44 +3331,44 @@ function GenreTreeView({
3196
3331
  )
3197
3332
  }
3198
3333
  ),
3199
- !isLoading && !readOnly && showAddRoot && /* @__PURE__ */ (0, import_jsx_runtime45.jsx)(
3200
- import_ui19.IconTextButton,
3334
+ !isLoading && !readOnly && showAddRoot && /* @__PURE__ */ (0, import_jsx_runtime46.jsx)(
3335
+ import_ui20.IconTextButton,
3201
3336
  {
3202
- icon: import_lucide_react15.Plus,
3337
+ icon: import_lucide_react16.Plus,
3203
3338
  text: "Add root",
3204
3339
  onClick: () => handleGenreCreationAction(null)
3205
3340
  }
3206
3341
  )
3207
3342
  ] })
3208
3343
  ] });
3209
- return /* @__PURE__ */ (0, import_jsx_runtime45.jsxs)(
3344
+ return /* @__PURE__ */ (0, import_jsx_runtime46.jsxs)(
3210
3345
  "div",
3211
3346
  {
3212
3347
  "data-left-panel": isPanelShown && !isCollapsed ? void 0 : "hidden",
3213
3348
  className: ["genre-tree-view relative flex flex-col h-full", placement === "regions" && REGIONS_CLASS, className2].filter(Boolean).join(" "),
3214
3349
  children: [
3215
- /* @__PURE__ */ (0, import_jsx_runtime45.jsx)(
3350
+ /* @__PURE__ */ (0, import_jsx_runtime46.jsx)(
3216
3351
  "div",
3217
3352
  {
3218
3353
  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"}`,
3219
3354
  children: actions
3220
3355
  }
3221
3356
  ),
3222
- !isLoading && filterArtist && /* @__PURE__ */ (0, import_jsx_runtime45.jsx)(
3357
+ !isLoading && filterArtist && /* @__PURE__ */ (0, import_jsx_runtime46.jsx)(
3223
3358
  "div",
3224
3359
  {
3225
3360
  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"}`,
3226
3361
  children: renderArtistFilter()
3227
3362
  }
3228
3363
  ),
3229
- /* @__PURE__ */ (0, import_jsx_runtime45.jsx)("div", { className: "content-container flex-1 min-h-0 flex flex-row gap-4", children: /* @__PURE__ */ (0, import_jsx_runtime45.jsxs)("div", { className: "tree-view-container relative flex-1 min-w-0 flex flex-col h-full", children: [
3230
- !isLoading && viewMode === "outline" && floatingControlsLeading && /* @__PURE__ */ (0, import_jsx_runtime45.jsx)("div", { className: "absolute bottom-3 right-3 z-10", children: floatingControlsLeading }),
3231
- isLoading ? /* @__PURE__ */ (0, import_jsx_runtime45.jsx)(import_genre_tree_view4.GenreTreeViewSkeleton, { viewMode }) : viewMode === "wheel" ? (
3364
+ /* @__PURE__ */ (0, import_jsx_runtime46.jsx)("div", { className: "content-container flex-1 min-h-0 flex flex-row gap-4", children: /* @__PURE__ */ (0, import_jsx_runtime46.jsxs)("div", { className: "tree-view-container relative flex-1 min-w-0 flex flex-col h-full", children: [
3365
+ !isLoading && viewMode === "outline" && floatingControlsLeading && /* @__PURE__ */ (0, import_jsx_runtime46.jsx)("div", { className: "absolute bottom-3 right-3 z-10", children: floatingControlsLeading }),
3366
+ isLoading ? /* @__PURE__ */ (0, import_jsx_runtime46.jsx)(import_genre_tree_view5.GenreTreeViewSkeleton, { viewMode }) : viewMode === "wheel" ? (
3232
3367
  // The info panel's top edge aligns with the tree's; the search bar floats `--layout-panel-inset` below it,
3233
3368
  // and the header inset (the bar's height plus that inset) extends the header's fill behind the bar,
3234
3369
  // Google Maps style; the 8px bottom padding mirrors the inset above the bar, and the hidden title's height is zeroed.
3235
3370
  // The panel's own Zoom in button is hidden: the search bar carries it.
3236
- /* @__PURE__ */ (0, import_jsx_runtime45.jsx)("div", { className: "tree-container search-overlay relative flex-1 w-full min-h-0 ", children: /* @__PURE__ */ (0, import_jsx_runtime45.jsx)(GenreTreeWheelHandoff, { skeleton: /* @__PURE__ */ (0, import_jsx_runtime45.jsx)(import_genre_tree_view4.GenreTreeWheelSkeleton, {}), onRevealedChange: setIsWheelRevealed, children: /* @__PURE__ */ (0, import_jsx_runtime45.jsx)(
3371
+ /* @__PURE__ */ (0, import_jsx_runtime46.jsx)("div", { className: "tree-container search-overlay relative flex-1 w-full min-h-0 ", children: /* @__PURE__ */ (0, import_jsx_runtime46.jsx)(GenreTreeWheelHandoff, { skeleton: /* @__PURE__ */ (0, import_jsx_runtime46.jsx)(import_genre_tree_view5.GenreTreeWheelSkeleton, {}), onRevealedChange: setIsWheelRevealed, children: /* @__PURE__ */ (0, import_jsx_runtime46.jsx)(
3237
3372
  GenrePlaylistTreeWheel,
3238
3373
  {
3239
3374
  scope,
@@ -3250,9 +3385,9 @@ function GenreTreeView({
3250
3385
  allowWheelRotation,
3251
3386
  sizeMetric: isArtistFiltered || !canSizeByViews ? "songs" : deferredSizeMetric,
3252
3387
  busy,
3253
- floatingControlsLeading: floatingControlsLeading || canSizeByViews ? /* @__PURE__ */ (0, import_jsx_runtime45.jsxs)("div", { className: "flex items-center gap-2", children: [
3388
+ floatingControlsLeading: floatingControlsLeading || canSizeByViews ? /* @__PURE__ */ (0, import_jsx_runtime46.jsxs)("div", { className: "flex items-center gap-2", children: [
3254
3389
  floatingControlsLeading,
3255
- canSizeByViews && /* @__PURE__ */ (0, import_jsx_runtime45.jsx)(SizeMetricToggle, { value: sizeMetric, onChange: changeSizeMetric })
3390
+ canSizeByViews && /* @__PURE__ */ (0, import_jsx_runtime46.jsx)(SizeMetricToggle, { value: sizeMetric, onChange: changeSizeMetric })
3256
3391
  ] }) : void 0,
3257
3392
  defaultViewport,
3258
3393
  viewportRestore,
@@ -3261,7 +3396,7 @@ function GenreTreeView({
3261
3396
  ) }) })
3262
3397
  ) : viewMode === "pop-core" ? (
3263
3398
  // Same as the wheel: the header inset extends its fill behind the floating search bar, Google Maps style.
3264
- /* @__PURE__ */ (0, import_jsx_runtime45.jsx)("div", { className: "tree-container search-overlay relative flex-1 w-full min-h-0 ", children: /* @__PURE__ */ (0, import_jsx_runtime45.jsx)(GenreTreeWheelHandoff, { skeleton: /* @__PURE__ */ (0, import_jsx_runtime45.jsx)(import_genre_tree_view4.GenreTreeWheelSkeleton, {}), onRevealedChange: setIsWheelRevealed, children: /* @__PURE__ */ (0, import_jsx_runtime45.jsx)(
3399
+ /* @__PURE__ */ (0, import_jsx_runtime46.jsx)("div", { className: "tree-container search-overlay relative flex-1 w-full min-h-0 ", children: /* @__PURE__ */ (0, import_jsx_runtime46.jsx)(GenreTreeWheelHandoff, { skeleton: /* @__PURE__ */ (0, import_jsx_runtime46.jsx)(import_genre_tree_view5.GenreTreeWheelSkeleton, {}), onRevealedChange: setIsWheelRevealed, children: /* @__PURE__ */ (0, import_jsx_runtime46.jsx)(
3265
3400
  GenrePlaylistTreeWheelRadialPopCore,
3266
3401
  {
3267
3402
  scope,
@@ -3278,9 +3413,9 @@ function GenreTreeView({
3278
3413
  allowWheelRotation,
3279
3414
  sizeMetric: isArtistFiltered || !canSizeByViews ? "songs" : deferredSizeMetric,
3280
3415
  busy,
3281
- floatingControlsLeading: floatingControlsLeading || canSizeByViews ? /* @__PURE__ */ (0, import_jsx_runtime45.jsxs)("div", { className: "flex items-center gap-2", children: [
3416
+ floatingControlsLeading: floatingControlsLeading || canSizeByViews ? /* @__PURE__ */ (0, import_jsx_runtime46.jsxs)("div", { className: "flex items-center gap-2", children: [
3282
3417
  floatingControlsLeading,
3283
- canSizeByViews && /* @__PURE__ */ (0, import_jsx_runtime45.jsx)(SizeMetricToggle, { value: sizeMetric, onChange: changeSizeMetric })
3418
+ canSizeByViews && /* @__PURE__ */ (0, import_jsx_runtime46.jsx)(SizeMetricToggle, { value: sizeMetric, onChange: changeSizeMetric })
3284
3419
  ] }) : void 0,
3285
3420
  defaultViewport,
3286
3421
  viewportRestore,
@@ -3288,7 +3423,7 @@ function GenreTreeView({
3288
3423
  ...focus
3289
3424
  }
3290
3425
  ) }) })
3291
- ) : viewMode === "outline" ? /* @__PURE__ */ (0, import_jsx_runtime45.jsx)("div", { className: "tree-container flex-1 min-h-0 w-full overflow-y-auto relative", children: /* @__PURE__ */ (0, import_jsx_runtime45.jsx)(
3426
+ ) : viewMode === "outline" ? /* @__PURE__ */ (0, import_jsx_runtime46.jsx)("div", { className: "tree-container flex-1 min-h-0 w-full overflow-y-auto relative", children: /* @__PURE__ */ (0, import_jsx_runtime46.jsx)(
3292
3427
  GenrePlaylistTreeWheelRadialPopCore,
3293
3428
  {
3294
3429
  outline: true,
@@ -3304,13 +3439,13 @@ function GenreTreeView({
3304
3439
  selectedNodeId,
3305
3440
  readOnly
3306
3441
  }
3307
- ) }) : /* @__PURE__ */ (0, import_jsx_runtime45.jsx)("div", { className: "tree-container relative flex flex-col gap-4 w-full pt-14 overflow-x-auto overflow-y-auto text-[var(--color-text)]", children: Object.entries(groupedGenrePlaylistsByRoot).map(
3442
+ ) }) : /* @__PURE__ */ (0, import_jsx_runtime46.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(
3308
3443
  ([uuid, genrePlaylistTreePerRoot]) => {
3309
- return /* @__PURE__ */ (0, import_jsx_runtime45.jsx)(
3444
+ return /* @__PURE__ */ (0, import_jsx_runtime46.jsx)(
3310
3445
  "div",
3311
3446
  {
3312
3447
  className: "tree-per-root-container relative shrink-0 h-[28rem] mt-2 mr-16 p-2 bg-[var(--color-neutral-100)] rounded-lg",
3313
- children: /* @__PURE__ */ (0, import_jsx_runtime45.jsx)("div", { className: "graph-container relative z-10 h-full", children: /* @__PURE__ */ (0, import_jsx_runtime45.jsx)(
3448
+ children: /* @__PURE__ */ (0, import_jsx_runtime46.jsx)("div", { className: "graph-container relative z-10 h-full", children: /* @__PURE__ */ (0, import_jsx_runtime46.jsx)(
3314
3449
  GenrePlaylistTreePerRoot,
3315
3450
  {
3316
3451
  scope,
@@ -3336,8 +3471,8 @@ function GenreTreeView({
3336
3471
  );
3337
3472
  }
3338
3473
  ) }),
3339
- /* @__PURE__ */ (0, import_jsx_runtime45.jsx)(
3340
- import_ui19.DetailPanel,
3474
+ /* @__PURE__ */ (0, import_jsx_runtime46.jsx)(
3475
+ import_ui20.DetailPanel,
3341
3476
  {
3342
3477
  ref: panelRef,
3343
3478
  side: "left",
@@ -3347,13 +3482,13 @@ function GenreTreeView({
3347
3482
  headerAccent: activePanel?.headerAccent,
3348
3483
  headerActions: expandButton,
3349
3484
  onClose: activePanel?.onClose ?? noop,
3350
- collapsed: isCollapsed || !isTreeReady || !activePanel,
3485
+ collapsed: isCollapsed || !isPanelShown,
3351
3486
  expanded: activePanel?.expanded,
3352
3487
  onExpandedChange: activePanel?.onExpandedChange,
3353
3488
  aside: activePanel?.aside,
3354
3489
  edgeTab: detailTab,
3355
- headerClassName: activePanel?.compact ? "pt-[calc(var(--layout-search-h)+var(--layout-panel-inset)+0.5rem)]" : void 0,
3356
- children: activePanel?.aside && !activePanel.expanded ? /* @__PURE__ */ (0, import_jsx_runtime45.jsxs)(import_jsx_runtime45.Fragment, { children: [
3490
+ headerClassName: "pt-[calc(var(--layout-search-h)+var(--layout-panel-inset)+0.5rem)]",
3491
+ children: activePanel?.aside && !activePanel.expanded ? /* @__PURE__ */ (0, import_jsx_runtime46.jsxs)(import_jsx_runtime46.Fragment, { children: [
3357
3492
  activePanel.content,
3358
3493
  activePanel.aside
3359
3494
  ] }) : activePanel?.content
@@ -3365,134 +3500,6 @@ function GenreTreeView({
3365
3500
  );
3366
3501
  }
3367
3502
 
3368
- // src/genre-tree/GenreDetailPanel.tsx
3369
- var import_react18 = require("react");
3370
- var import_genre_tree_view5 = require("@behindthemusictree/genre-tree-view");
3371
- var import_ui20 = require("@behindthemusictree/ui");
3372
- var import_lucide_react17 = require("lucide-react");
3373
- var import_genre_tree28 = require("@behindthemusictree/app-kit/genre-tree");
3374
- var import_jsx_runtime46 = require("react/jsx-runtime");
3375
- var RELATIVE_ITEM_COUNT = 1;
3376
- function toNode(genre, parentId) {
3377
- return {
3378
- id: genre.uuid,
3379
- parentId,
3380
- name: genre.name,
3381
- itemCount: RELATIVE_ITEM_COUNT,
3382
- side: genre.side ?? void 0,
3383
- wikidataId: genre.wikidataId
3384
- };
3385
- }
3386
- function GenreDetailPanel({
3387
- genreUuid,
3388
- scope,
3389
- getBackendBaseUrl,
3390
- onClose,
3391
- onSelectGenre,
3392
- onZoomIn,
3393
- shareUrl,
3394
- shareTarget,
3395
- collapsed,
3396
- expanded,
3397
- onExpandedChange,
3398
- edgeTab,
3399
- extras
3400
- }) {
3401
- const { data: detail, isPending, isError } = (0, import_genre_tree28.useFetchGenreDetail)(genreUuid, scope, getBackendBaseUrl);
3402
- const playback = useGenrePlaylistPlayback({ scope, genrePlaylists: [] });
3403
- const view = (0, import_react18.useMemo)(() => {
3404
- if (!detail) return null;
3405
- const rootColor = (0, import_genre_tree_view5.getGenreTreeColor)(detail.root.wikidataId);
3406
- const relative = (genre, parentId) => ({
3407
- node: toNode(genre, parentId),
3408
- fill: (0, import_genre_tree_view5.tintSurface)(rootColor),
3409
- textColor: import_genre_tree_view5.TEXT_COLOR
3410
- });
3411
- const ancestors2 = [...detail.ascendants].sort((a, b) => b.degree - a.degree);
3412
- return {
3413
- node: {
3414
- id: detail.uuid,
3415
- parentId: detail.parent?.uuid ?? null,
3416
- name: detail.name,
3417
- itemCount: scope === "reference" ? (0, import_genre_tree28.genreMusicCount)(import_genre_tree28.GenrePlaylistSummarySchema.parse(detail.criteriaPlaylist)) : detail.tracksCount,
3418
- wikidataId: detail.wikidataId
3419
- },
3420
- fill: rootColor,
3421
- parentNode: detail.parent ? relative(detail.parent, null) : null,
3422
- childNodes: detail.children.map((child) => relative(child, detail.uuid)),
3423
- ancestorNodes: ancestors2.map(
3424
- ({ ascendant }, index) => relative(ascendant, index === 0 ? null : ancestors2[index - 1].ascendant.uuid)
3425
- )
3426
- };
3427
- }, [detail, scope]);
3428
- if (isError) {
3429
- return /* @__PURE__ */ (0, import_jsx_runtime46.jsx)("div", { role: "alert", className: "p-4 text-sm text-[var(--color-text)]", children: "This genre could not be loaded." });
3430
- }
3431
- if (isPending || !detail || !view) {
3432
- return /* @__PURE__ */ (0, import_jsx_runtime46.jsxs)("div", { className: "flex flex-col gap-3 p-4", "aria-busy": "true", "data-testid": "genre-detail-panel-skeleton", children: [
3433
- /* @__PURE__ */ (0, import_jsx_runtime46.jsx)(import_ui20.Skeleton, { className: "h-6 w-2/3" }),
3434
- /* @__PURE__ */ (0, import_jsx_runtime46.jsx)(import_ui20.Skeleton, { className: "h-4 w-full" }),
3435
- /* @__PURE__ */ (0, import_jsx_runtime46.jsx)(import_ui20.Skeleton, { className: "h-4 w-3/4" })
3436
- ] });
3437
- }
3438
- const selectGenre = (nodeId) => onSelectGenre(nodeId);
3439
- const family = /* @__PURE__ */ (0, import_jsx_runtime46.jsx)(
3440
- import_genre_tree_view5.GenreNodeFamily,
3441
- {
3442
- node: view.node,
3443
- fill: view.fill,
3444
- textColor: import_genre_tree_view5.ACCENT_TEXT_COLOR,
3445
- parentNode: view.parentNode,
3446
- childNodes: view.childNodes,
3447
- onSelectNode: selectGenre
3448
- }
3449
- );
3450
- const ancestors = /* @__PURE__ */ (0, import_jsx_runtime46.jsx)(import_genre_tree_view5.GenreNodeAncestors, { ancestorNodes: view.ancestorNodes, onSelectNode: selectGenre });
3451
- return /* @__PURE__ */ (0, import_jsx_runtime46.jsxs)(
3452
- import_ui20.DetailPanel,
3453
- {
3454
- side: "left",
3455
- title: view.node.name,
3456
- headerAccent: { background: view.fill, color: import_genre_tree_view5.ACCENT_TEXT_COLOR },
3457
- headerActions: onExpandedChange ? /* @__PURE__ */ (0, import_jsx_runtime46.jsx)(
3458
- import_ui20.IconButton,
3459
- {
3460
- "aria-label": expanded ? "Shrink panel" : "Extend panel",
3461
- style: { color: "inherit" },
3462
- onClick: () => onExpandedChange(!expanded),
3463
- children: expanded ? /* @__PURE__ */ (0, import_jsx_runtime46.jsx)(import_lucide_react17.Minimize2, { "aria-hidden": "true", className: "h-4 w-4" }) : /* @__PURE__ */ (0, import_jsx_runtime46.jsx)(import_lucide_react17.Maximize2, { "aria-hidden": "true", className: "h-4 w-4" })
3464
- }
3465
- ) : void 0,
3466
- onClose,
3467
- collapsed,
3468
- expanded,
3469
- onExpandedChange,
3470
- edgeTab,
3471
- children: [
3472
- /* @__PURE__ */ (0, import_jsx_runtime46.jsx)(
3473
- GenreInfoPanelBody,
3474
- {
3475
- family: /* @__PURE__ */ (0, import_jsx_runtime46.jsxs)(import_jsx_runtime46.Fragment, { children: [
3476
- family,
3477
- ancestors
3478
- ] }),
3479
- genreUuid: detail.uuid,
3480
- genrePlaylist: scope === "reference" ? import_genre_tree28.GenrePlaylistSummarySchema.parse(detail.criteriaPlaylist) : { ...detail.criteriaPlaylist, tracksCount: detail.tracksCount },
3481
- catalogue: detail.catalogue,
3482
- wikidataId: detail.wikidataId,
3483
- scope,
3484
- getBackendBaseUrl,
3485
- playback,
3486
- band: { node: view.node, onZoomIn, shareUrl, shareTarget: shareUrl ? shareTarget : void 0 },
3487
- extras
3488
- }
3489
- ),
3490
- !expanded && ancestors
3491
- ]
3492
- }
3493
- );
3494
- }
3495
-
3496
3503
  // src/genre-tree/index.ts
3497
3504
  var import_genre_tree_view6 = require("@behindthemusictree/genre-tree-view");
3498
3505