@behindthemusictree/app-kit-ui 34.1.0 → 36.0.0

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