@behindthemusictree/app-kit-ui 40.0.0 → 41.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
@@ -591,11 +591,12 @@ function AuthCallbackHandler({ renderStatus, ...options }) {
591
591
  // src/genre-tree/GenreTreeView.tsx
592
592
  var import_react19 = require("react");
593
593
  var import_react_query3 = require("@tanstack/react-query");
594
+ var import_lucide_react17 = require("lucide-react");
594
595
  var import_lucide_react18 = require("lucide-react");
595
- var import_lucide_react19 = require("lucide-react");
596
596
  var import_ui24 = require("@behindthemusictree/ui");
597
597
  var import_genre_tree_view5 = require("@behindthemusictree/genre-tree-view");
598
598
  var import_genre_tree30 = require("@behindthemusictree/app-kit/genre-tree");
599
+ var import_genre_tree31 = require("@behindthemusictree/app-kit/genre-tree");
599
600
  var import_popup5 = require("@behindthemusictree/app-kit/popup");
600
601
 
601
602
  // src/genre-tree/playlist-tree/size-metric.ts
@@ -626,6 +627,21 @@ function toTreeNodes(genrePlaylists, sizeMetric, focusUuids) {
626
627
  }));
627
628
  }
628
629
 
630
+ // src/genre-tree/playlist-tree/toGenreNodeStyles.ts
631
+ function toGenreNodeStyles(relations, genrePlaylists) {
632
+ const styles = /* @__PURE__ */ new Map();
633
+ if (!relations) return styles;
634
+ const genreUuidByPlaylistUuid = new Map(
635
+ genrePlaylists.flatMap((genrePlaylist) => genrePlaylist.criteria ? [[genrePlaylist.uuid, genrePlaylist.criteria.uuid]] : [])
636
+ );
637
+ const drawn = [relations, relations.parentNode, ...relations.childNodes, ...relations.ancestorNodes];
638
+ for (const entry of drawn) {
639
+ const genreUuid = entry && genreUuidByPlaylistUuid.get(entry.node.id);
640
+ if (entry && genreUuid) styles.set(genreUuid, { fill: entry.fill, textColor: entry.textColor });
641
+ }
642
+ return styles;
643
+ }
644
+
629
645
  // src/genre-tree/playlist-tree/TreePerRoot.tsx
630
646
  var import_genre_tree3 = require("@behindthemusictree/app-kit/genre-tree");
631
647
  var import_react4 = require("react");
@@ -1685,6 +1701,29 @@ function ClampedText({ children, className: className2 = "" }) {
1685
1701
  ] });
1686
1702
  }
1687
1703
 
1704
+ // src/genre-tree/CommonsPhoto.tsx
1705
+ var import_jsx_runtime32 = require("react/jsx-runtime");
1706
+ function ExternalLink({ href, children }) {
1707
+ return /* @__PURE__ */ (0, import_jsx_runtime32.jsx)("a", { href, target: "_blank", rel: "noopener noreferrer", className: "underline-offset-2 hover:underline", children });
1708
+ }
1709
+ function CommonsPhoto({
1710
+ image,
1711
+ alt,
1712
+ creditLabel = "Photo"
1713
+ }) {
1714
+ return /* @__PURE__ */ (0, import_jsx_runtime32.jsxs)("figure", { className: "flex flex-col gap-1.5", children: [
1715
+ /* @__PURE__ */ (0, import_jsx_runtime32.jsx)("img", { src: image.url, alt, className: "h-64 w-full object-contain bg-[var(--color-surface-muted)] rounded" }),
1716
+ /* @__PURE__ */ (0, import_jsx_runtime32.jsxs)("figcaption", { className: "text-caption text-[var(--color-text-muted)]", children: [
1717
+ creditLabel,
1718
+ ": ",
1719
+ /* @__PURE__ */ (0, import_jsx_runtime32.jsx)(ExternalLink, { href: image.pageUrl, children: image.author ?? "Wikimedia Commons" }),
1720
+ ",",
1721
+ " ",
1722
+ image.licenseUrl ? /* @__PURE__ */ (0, import_jsx_runtime32.jsx)(ExternalLink, { href: image.licenseUrl, children: image.license }) : image.license
1723
+ ] })
1724
+ ] });
1725
+ }
1726
+
1688
1727
  // src/genre-tree/GenrePlaylistTracks.tsx
1689
1728
  var import_react13 = require("react");
1690
1729
  var import_react_query = require("@tanstack/react-query");
@@ -1707,15 +1746,15 @@ var import_genre_tree16 = require("@behindthemusictree/app-kit/genre-tree");
1707
1746
 
1708
1747
  // src/genre-tree/MusicBrainzRecordingLink.tsx
1709
1748
  var import_genre_tree11 = require("@behindthemusictree/app-kit/genre-tree");
1710
- var import_jsx_runtime32 = require("react/jsx-runtime");
1749
+ var import_jsx_runtime33 = require("react/jsx-runtime");
1711
1750
  function MusicBrainzRecordingLink({
1712
1751
  song,
1713
1752
  children,
1714
1753
  className: className2 = ""
1715
1754
  }) {
1716
1755
  const url = (0, import_genre_tree11.musicBrainzRecordingUrl)(song);
1717
- if (!url) return /* @__PURE__ */ (0, import_jsx_runtime32.jsx)(import_jsx_runtime32.Fragment, { children });
1718
- return /* @__PURE__ */ (0, import_jsx_runtime32.jsx)(
1756
+ if (!url) return /* @__PURE__ */ (0, import_jsx_runtime33.jsx)(import_jsx_runtime33.Fragment, { children });
1757
+ return /* @__PURE__ */ (0, import_jsx_runtime33.jsx)(
1719
1758
  "a",
1720
1759
  {
1721
1760
  href: url,
@@ -1732,7 +1771,7 @@ function MusicBrainzRecordingLink({
1732
1771
  // src/genre-tree/GenrePlaylistTracks.tsx
1733
1772
  var import_genre_tree17 = require("@behindthemusictree/app-kit/genre-tree");
1734
1773
  var import_track_noun2 = require("@behindthemusictree/app-kit/track-noun");
1735
- var import_jsx_runtime33 = require("react/jsx-runtime");
1774
+ var import_jsx_runtime34 = require("react/jsx-runtime");
1736
1775
  var CATALOGUE_PAGE_SIZE = 100;
1737
1776
  var SongPageSchema = (0, import_transport5.PaginatedResponseSchema)(import_genre_tree15.SongDetailedSchema).extend({
1738
1777
  overallTotal: import_zod.z.number().nullable(),
@@ -1819,35 +1858,35 @@ function GenrePlaylistTracks({
1819
1858
  started.catch((error) => {
1820
1859
  console.error("Failed to play genre playlist:", error);
1821
1860
  if (error instanceof import_genre_tree16.TracksFailureAlreadyReportedError) return;
1822
- showPopup(/* @__PURE__ */ (0, import_jsx_runtime33.jsx)(InternalErrorPopup, { errorCode: error instanceof import_transport7.AppError ? error.code : import_transport7.ErrorCode.CLIENT_UNKNOWN }));
1861
+ showPopup(/* @__PURE__ */ (0, import_jsx_runtime34.jsx)(InternalErrorPopup, { errorCode: error instanceof import_transport7.AppError ? error.code : import_transport7.ErrorCode.CLIENT_UNKNOWN }));
1823
1862
  });
1824
1863
  };
1825
- return /* @__PURE__ */ (0, import_jsx_runtime33.jsx)(import_ui11.PanelSection, { title: `${(0, import_track_noun2.capitalize)(trackNoun.plural)}${total !== void 0 ? ` (${total})` : ""}`, children: /* @__PURE__ */ (0, import_jsx_runtime33.jsxs)("div", { children: [
1864
+ return /* @__PURE__ */ (0, import_jsx_runtime34.jsx)(import_ui11.PanelSection, { title: `${(0, import_track_noun2.capitalize)(trackNoun.plural)}${total !== void 0 ? ` (${total})` : ""}`, children: /* @__PURE__ */ (0, import_jsx_runtime34.jsxs)("div", { children: [
1826
1865
  renderTracksToolbar?.(),
1827
- scope === "me" && !genrePlaylist ? /* @__PURE__ */ (0, import_jsx_runtime33.jsx)("p", { children: "\u2014" }) : isPending ? /* @__PURE__ */ (0, import_jsx_runtime33.jsx)(import_ui11.Skeleton, { className: "h-4 w-3/4", "aria-busy": "true" }) : isError && tracks.length === 0 ? /* @__PURE__ */ (0, import_jsx_runtime33.jsxs)("p", { role: "alert", children: [
1866
+ scope === "me" && !genrePlaylist ? /* @__PURE__ */ (0, import_jsx_runtime34.jsx)("p", { children: "\u2014" }) : isPending ? /* @__PURE__ */ (0, import_jsx_runtime34.jsx)(import_ui11.Skeleton, { className: "h-4 w-3/4", "aria-busy": "true" }) : isError && tracks.length === 0 ? /* @__PURE__ */ (0, import_jsx_runtime34.jsxs)("p", { role: "alert", children: [
1828
1867
  "Couldn't load ",
1829
1868
  trackNoun.plural,
1830
1869
  ".",
1831
1870
  " ",
1832
- /* @__PURE__ */ (0, import_jsx_runtime33.jsx)("button", { type: "button", className: "underline", onClick: () => void refetch(), children: "Retry" })
1833
- ] }) : tracks.length === 0 ? /* @__PURE__ */ (0, import_jsx_runtime33.jsx)("p", { children: "\u2014" }) : /* @__PURE__ */ (0, import_jsx_runtime33.jsxs)("ul", { className: "mt-1 max-h-64 overflow-y-auto text-body [container-type:inline-size]", "aria-label": `${genreName} ${trackNoun.plural}`, children: [
1871
+ /* @__PURE__ */ (0, import_jsx_runtime34.jsx)("button", { type: "button", className: "underline", onClick: () => void refetch(), children: "Retry" })
1872
+ ] }) : tracks.length === 0 ? /* @__PURE__ */ (0, import_jsx_runtime34.jsx)("p", { children: "\u2014" }) : /* @__PURE__ */ (0, import_jsx_runtime34.jsxs)("ul", { className: "mt-1 max-h-64 overflow-y-auto text-body [container-type:inline-size]", "aria-label": `${genreName} ${trackNoun.plural}`, children: [
1834
1873
  tracks.map((track, index) => {
1835
1874
  const label = track.artists?.length ? `${track.title} \u2014 ${track.artists.map((artist2) => artist2.name).join(", ")}` : track.title;
1836
1875
  const isCurrent = playerTrackObject?.track.id === track.uuid;
1837
1876
  const isPlaying = isCurrent && playState === import_player6.PlayStates.PLAYING;
1838
- const text = /* @__PURE__ */ (0, import_jsx_runtime33.jsxs)("span", { className: TEXT_CELL, children: [
1839
- /* @__PURE__ */ (0, import_jsx_runtime33.jsx)("span", { className: TITLE_CELL, children: renderSongLink ? renderSongLink(track) : (0, import_genre_tree15.isSong)(track) ? /* @__PURE__ */ (0, import_jsx_runtime33.jsx)(MusicBrainzRecordingLink, { song: track, className: "hover:underline focus-visible:underline", children: track.title }) : track.title }),
1840
- track.artists?.length ? /* @__PURE__ */ (0, import_jsx_runtime33.jsx)("span", { className: SEPARATOR, children: " \u2014 " }) : null,
1841
- /* @__PURE__ */ (0, import_jsx_runtime33.jsx)("span", { className: COLUMN_CELL, children: track.artists?.length ? /* @__PURE__ */ (0, import_jsx_runtime33.jsx)(import_genre_tree17.ArtistNames, { artists: track.artists, renderArtistLink }) : null }),
1842
- track.album && /* @__PURE__ */ (0, import_jsx_runtime33.jsx)("span", { className: ALBUM_CELL, children: track.album.name })
1877
+ const text = /* @__PURE__ */ (0, import_jsx_runtime34.jsxs)("span", { className: TEXT_CELL, children: [
1878
+ /* @__PURE__ */ (0, import_jsx_runtime34.jsx)("span", { className: TITLE_CELL, children: renderSongLink ? renderSongLink(track) : (0, import_genre_tree15.isSong)(track) ? /* @__PURE__ */ (0, import_jsx_runtime34.jsx)(MusicBrainzRecordingLink, { song: track, className: "hover:underline focus-visible:underline", children: track.title }) : track.title }),
1879
+ track.artists?.length ? /* @__PURE__ */ (0, import_jsx_runtime34.jsx)("span", { className: SEPARATOR, children: " \u2014 " }) : null,
1880
+ /* @__PURE__ */ (0, import_jsx_runtime34.jsx)("span", { className: COLUMN_CELL, children: track.artists?.length ? /* @__PURE__ */ (0, import_jsx_runtime34.jsx)(import_genre_tree17.ArtistNames, { artists: track.artists, renderArtistLink }) : null }),
1881
+ track.album && /* @__PURE__ */ (0, import_jsx_runtime34.jsx)("span", { className: ALBUM_CELL, children: track.album.name })
1843
1882
  ] });
1844
1883
  const leading = renderSongLeading?.(track, index);
1845
1884
  const actions = renderSongActions?.(track);
1846
1885
  const isUnplayable = (0, import_genre_tree15.isSong)(track) && (0, import_genre_tree15.songUnplayableReason)(track) !== null;
1847
1886
  if (isUnplayable) {
1848
- return /* @__PURE__ */ (0, import_jsx_runtime33.jsxs)("li", { className: "flex min-h-8 items-center gap-1 px-1 opacity-50", children: [
1887
+ return /* @__PURE__ */ (0, import_jsx_runtime34.jsxs)("li", { className: "flex min-h-8 items-center gap-1 px-1 opacity-50", children: [
1849
1888
  leading,
1850
- /* @__PURE__ */ (0, import_jsx_runtime33.jsx)(
1889
+ /* @__PURE__ */ (0, import_jsx_runtime34.jsx)(
1851
1890
  "button",
1852
1891
  {
1853
1892
  type: "button",
@@ -1855,27 +1894,27 @@ function GenrePlaylistTracks({
1855
1894
  "aria-disabled": "true",
1856
1895
  "aria-label": label,
1857
1896
  className: "flex h-6 w-6 shrink-0 items-center justify-center rounded-full opacity-0 cursor-not-allowed",
1858
- children: /* @__PURE__ */ (0, import_jsx_runtime33.jsx)(import_fa4.FaPlay, { size: 10 })
1897
+ children: /* @__PURE__ */ (0, import_jsx_runtime34.jsx)(import_fa4.FaPlay, { size: 10 })
1859
1898
  }
1860
1899
  ),
1861
1900
  text,
1862
1901
  actions
1863
1902
  ] }, track.uuid);
1864
1903
  }
1865
- return /* @__PURE__ */ (0, import_jsx_runtime33.jsxs)(
1904
+ return /* @__PURE__ */ (0, import_jsx_runtime34.jsxs)(
1866
1905
  "li",
1867
1906
  {
1868
1907
  className: `group flex min-h-8 items-center gap-1 rounded px-1 hover:bg-[var(--color-neutral-100)] ${isCurrent ? "font-medium text-[var(--color-primary)]" : ""}`,
1869
1908
  children: [
1870
1909
  leading,
1871
- /* @__PURE__ */ (0, import_jsx_runtime33.jsx)(
1910
+ /* @__PURE__ */ (0, import_jsx_runtime34.jsx)(
1872
1911
  "button",
1873
1912
  {
1874
1913
  type: "button",
1875
1914
  "aria-label": `${isPlaying ? "Pause" : "Play"} ${label}`,
1876
1915
  className: `flex h-6 w-6 shrink-0 items-center justify-center rounded-full cursor-pointer group-hover:opacity-100 focus-visible:opacity-100 ${isCurrent && playState !== import_player6.PlayStates.STOPPED ? "opacity-100" : "opacity-0 [@media(pointer:coarse)]:opacity-100"}`,
1877
1916
  onClick: () => isCurrent ? handlePlayPauseAction() : play(track.uuid),
1878
- children: isCurrent && playState === import_player6.PlayStates.LOADING ? /* @__PURE__ */ (0, import_jsx_runtime33.jsx)(import_ui11.RingLoader, { size: 10 }) : isPlaying ? /* @__PURE__ */ (0, import_jsx_runtime33.jsx)(import_fa4.FaPause, { size: 10 }) : /* @__PURE__ */ (0, import_jsx_runtime33.jsx)(import_fa4.FaPlay, { size: 10 })
1917
+ children: isCurrent && playState === import_player6.PlayStates.LOADING ? /* @__PURE__ */ (0, import_jsx_runtime34.jsx)(import_ui11.RingLoader, { size: 10 }) : isPlaying ? /* @__PURE__ */ (0, import_jsx_runtime34.jsx)(import_fa4.FaPause, { size: 10 }) : /* @__PURE__ */ (0, import_jsx_runtime34.jsx)(import_fa4.FaPlay, { size: 10 })
1879
1918
  }
1880
1919
  ),
1881
1920
  text,
@@ -1885,7 +1924,7 @@ function GenrePlaylistTracks({
1885
1924
  track.uuid
1886
1925
  );
1887
1926
  }),
1888
- hasNextPage && /* @__PURE__ */ (0, import_jsx_runtime33.jsx)("li", { ref: setSentinel, "aria-hidden": "true", className: "h-4" })
1927
+ hasNextPage && /* @__PURE__ */ (0, import_jsx_runtime34.jsx)("li", { ref: setSentinel, "aria-hidden": "true", className: "h-4" })
1889
1928
  ] })
1890
1929
  ] }) });
1891
1930
  }
@@ -1893,7 +1932,7 @@ function GenrePlaylistTracks({
1893
1932
  // src/genre-tree/GenreDetailExtras.tsx
1894
1933
  var import_track_noun3 = require("@behindthemusictree/app-kit/track-noun");
1895
1934
  var import_si2 = require("react-icons/si");
1896
- var import_jsx_runtime34 = require("react/jsx-runtime");
1935
+ var import_jsx_runtime35 = require("react/jsx-runtime");
1897
1936
  function GenreDetailExtras({
1898
1937
  genreUuid,
1899
1938
  genrePlaylist,
@@ -1919,17 +1958,18 @@ function GenreDetailExtras({
1919
1958
  criteriaOverviewSchema
1920
1959
  );
1921
1960
  if (isPending) {
1922
- return /* @__PURE__ */ (0, import_jsx_runtime34.jsxs)(import_jsx_runtime34.Fragment, { children: [
1923
- /* @__PURE__ */ (0, import_jsx_runtime34.jsx)(import_ui12.PanelSection, { title: "Summary", children: /* @__PURE__ */ (0, import_jsx_runtime34.jsx)("div", { "aria-busy": "true", "data-testid": "genre-detail-extras-skeleton", children: /* @__PURE__ */ (0, import_jsx_runtime34.jsx)(import_ui12.Skeleton, { className: "h-4 w-3/4" }) }) }),
1961
+ return /* @__PURE__ */ (0, import_jsx_runtime35.jsxs)(import_jsx_runtime35.Fragment, { children: [
1962
+ /* @__PURE__ */ (0, import_jsx_runtime35.jsx)(import_ui12.PanelSection, { title: "Summary", children: /* @__PURE__ */ (0, import_jsx_runtime35.jsx)("div", { "aria-busy": "true", "data-testid": "genre-detail-extras-skeleton", children: /* @__PURE__ */ (0, import_jsx_runtime35.jsx)(import_ui12.Skeleton, { className: "h-4 w-3/4" }) }) }),
1924
1963
  family
1925
1964
  ] });
1926
1965
  }
1927
- if (!overview) return /* @__PURE__ */ (0, import_jsx_runtime34.jsx)(import_jsx_runtime34.Fragment, { children: family });
1928
- const { summary, summarySourceUrl, essentialSongs } = overview;
1966
+ if (!overview) return /* @__PURE__ */ (0, import_jsx_runtime35.jsx)(import_jsx_runtime35.Fragment, { children: family });
1967
+ const { summary, summarySourceUrl, essentialSongs, image } = overview;
1929
1968
  const wikidataUrl = wikidataId ? `https://www.wikidata.org/wiki/${encodeURIComponent(wikidataId)}` : null;
1930
- return /* @__PURE__ */ (0, import_jsx_runtime34.jsxs)(import_jsx_runtime34.Fragment, { children: [
1931
- (summarySourceUrl || wikidataUrl) && /* @__PURE__ */ (0, import_jsx_runtime34.jsx)(import_ui12.PanelSection, { title: "Links", children: /* @__PURE__ */ (0, import_jsx_runtime34.jsxs)("div", { className: "flex flex-wrap gap-x-3 gap-y-1 text-caption text-[var(--color-text)]", children: [
1932
- summarySourceUrl && /* @__PURE__ */ (0, import_jsx_runtime34.jsxs)(
1969
+ return /* @__PURE__ */ (0, import_jsx_runtime35.jsxs)(import_jsx_runtime35.Fragment, { children: [
1970
+ image && /* @__PURE__ */ (0, import_jsx_runtime35.jsx)(CommonsPhoto, { image, alt: overview.name, creditLabel: "Image" }),
1971
+ (summarySourceUrl || wikidataUrl) && /* @__PURE__ */ (0, import_jsx_runtime35.jsx)(import_ui12.PanelSection, { title: "Links", children: /* @__PURE__ */ (0, import_jsx_runtime35.jsxs)("div", { className: "flex flex-wrap gap-x-3 gap-y-1 text-caption text-[var(--color-text)]", children: [
1972
+ summarySourceUrl && /* @__PURE__ */ (0, import_jsx_runtime35.jsxs)(
1933
1973
  "a",
1934
1974
  {
1935
1975
  href: summarySourceUrl,
@@ -1937,12 +1977,12 @@ function GenreDetailExtras({
1937
1977
  rel: "noopener noreferrer",
1938
1978
  className: "inline-flex items-center gap-1.5 text-[var(--color-text-muted)] hover:text-[var(--color-text)]",
1939
1979
  children: [
1940
- /* @__PURE__ */ (0, import_jsx_runtime34.jsx)(import_si2.SiWikipedia, { "aria-hidden": "true", className: "h-3.5 w-3.5" }),
1980
+ /* @__PURE__ */ (0, import_jsx_runtime35.jsx)(import_si2.SiWikipedia, { "aria-hidden": "true", className: "h-3.5 w-3.5" }),
1941
1981
  "Wikipedia"
1942
1982
  ]
1943
1983
  }
1944
1984
  ),
1945
- wikidataUrl && /* @__PURE__ */ (0, import_jsx_runtime34.jsxs)(
1985
+ wikidataUrl && /* @__PURE__ */ (0, import_jsx_runtime35.jsxs)(
1946
1986
  "a",
1947
1987
  {
1948
1988
  href: wikidataUrl,
@@ -1950,17 +1990,17 @@ function GenreDetailExtras({
1950
1990
  rel: "noopener noreferrer",
1951
1991
  className: "inline-flex items-center gap-1.5 text-[var(--color-text-muted)] hover:text-[var(--color-text)]",
1952
1992
  children: [
1953
- /* @__PURE__ */ (0, import_jsx_runtime34.jsx)(import_si2.SiWikidata, { "aria-hidden": "true", className: "h-3.5 w-3.5" }),
1993
+ /* @__PURE__ */ (0, import_jsx_runtime35.jsx)(import_si2.SiWikidata, { "aria-hidden": "true", className: "h-3.5 w-3.5" }),
1954
1994
  "Wikidata"
1955
1995
  ]
1956
1996
  }
1957
1997
  )
1958
1998
  ] }) }),
1959
- /* @__PURE__ */ (0, import_jsx_runtime34.jsx)(import_ui12.PanelSection, { title: "Summary", children: renderSummary ? renderSummary(overview) : /* @__PURE__ */ (0, import_jsx_runtime34.jsx)(ClampedText, { className: "text-body text-[var(--color-text)]", children: summary ?? "\u2014" }, genreUuid) }),
1999
+ /* @__PURE__ */ (0, import_jsx_runtime35.jsx)(import_ui12.PanelSection, { title: "Summary", children: renderSummary ? renderSummary(overview) : /* @__PURE__ */ (0, import_jsx_runtime35.jsx)(ClampedText, { className: "text-body text-[var(--color-text)]", children: summary ?? "\u2014" }, genreUuid) }),
1960
2000
  family,
1961
2001
  renderGenreDetailExtras?.(overview),
1962
- (renderEssentialTracks || essentialSongs) && /* @__PURE__ */ (0, import_jsx_runtime34.jsx)(import_ui12.PanelSection, { title: `Essential ${trackNoun.plural}`, children: renderEssentialTracks ? renderEssentialTracks(overview) : essentialSongs && essentialSongs.length > 0 ? /* @__PURE__ */ (0, import_jsx_runtime34.jsx)("ul", { className: "text-body text-[var(--color-text)]", children: essentialSongs.map((track) => /* @__PURE__ */ (0, import_jsx_runtime34.jsx)("li", { className: "py-1", children: track.title }, track.uuid)) }) : /* @__PURE__ */ (0, import_jsx_runtime34.jsx)("p", { className: "text-body text-[var(--color-text)]", children: "\u2014" }) }),
1963
- /* @__PURE__ */ (0, import_jsx_runtime34.jsx)(
2002
+ (renderEssentialTracks || essentialSongs) && /* @__PURE__ */ (0, import_jsx_runtime35.jsx)(import_ui12.PanelSection, { title: `Essential ${trackNoun.plural}`, children: renderEssentialTracks ? renderEssentialTracks(overview) : essentialSongs && essentialSongs.length > 0 ? /* @__PURE__ */ (0, import_jsx_runtime35.jsx)("ul", { className: "text-body text-[var(--color-text)]", children: essentialSongs.map((track) => /* @__PURE__ */ (0, import_jsx_runtime35.jsx)("li", { className: "py-1", children: track.title }, track.uuid)) }) : /* @__PURE__ */ (0, import_jsx_runtime35.jsx)("p", { className: "text-body text-[var(--color-text)]", children: "\u2014" }) }),
2003
+ /* @__PURE__ */ (0, import_jsx_runtime35.jsx)(
1964
2004
  GenrePlaylistTracks,
1965
2005
  {
1966
2006
  genreUuid,
@@ -1978,10 +2018,10 @@ function GenreDetailExtras({
1978
2018
  }
1979
2019
 
1980
2020
  // src/genre-tree/GenreInfoPanelBody.tsx
1981
- var import_jsx_runtime35 = require("react/jsx-runtime");
2021
+ var import_jsx_runtime36 = require("react/jsx-runtime");
1982
2022
  var import_react14 = require("react");
1983
2023
  function GenreNodeActionBand({ genrePlaylist, playback, ...bandProps }) {
1984
- return /* @__PURE__ */ (0, import_jsx_runtime35.jsx)(GenreActionBand, { ...bandProps, playActions: /* @__PURE__ */ (0, import_jsx_runtime35.jsx)(GenrePlayActions, { genrePlaylist, playback }) });
2024
+ return /* @__PURE__ */ (0, import_jsx_runtime36.jsx)(GenreActionBand, { ...bandProps, playActions: /* @__PURE__ */ (0, import_jsx_runtime36.jsx)(GenrePlayActions, { genrePlaylist, playback }) });
1985
2025
  }
1986
2026
  function GenreInfoPanelBody({
1987
2027
  toolbar,
@@ -1998,9 +2038,9 @@ function GenreInfoPanelBody({
1998
2038
  }) {
1999
2039
  const summary = scope === "reference" ? import_genre_tree19.GenreCatalogueCountsSchema.parse(genrePlaylist ?? catalogue) : void 0;
2000
2040
  const selection = summary ? (0, import_genre_tree19.defaultGenreMusicSelection)(summary) : void 0;
2001
- return /* @__PURE__ */ (0, import_jsx_runtime35.jsxs)(import_jsx_runtime35.Fragment, { children: [
2041
+ return /* @__PURE__ */ (0, import_jsx_runtime36.jsxs)(import_jsx_runtime36.Fragment, { children: [
2002
2042
  toolbar,
2003
- genrePlaylist ? /* @__PURE__ */ (0, import_jsx_runtime35.jsx)(GenreNodeActionBand, { ...band, genrePlaylist, playback }) : /* @__PURE__ */ (0, import_jsx_runtime35.jsx)(GenreActionBand, { ...band }),
2043
+ genrePlaylist ? /* @__PURE__ */ (0, import_jsx_runtime36.jsx)(GenreNodeActionBand, { ...band, genrePlaylist, playback }) : /* @__PURE__ */ (0, import_jsx_runtime36.jsx)(GenreActionBand, { ...band }),
2004
2044
  /* @__PURE__ */ (0, import_react14.createElement)(
2005
2045
  GenreDetailExtras,
2006
2046
  {
@@ -2020,7 +2060,7 @@ function GenreInfoPanelBody({
2020
2060
  }
2021
2061
 
2022
2062
  // src/genre-tree/GenreDetailPanel.tsx
2023
- var import_jsx_runtime36 = require("react/jsx-runtime");
2063
+ var import_jsx_runtime37 = require("react/jsx-runtime");
2024
2064
  var RELATIVE_ITEM_COUNT = 1;
2025
2065
  function toNode(genre, parentId) {
2026
2066
  return {
@@ -2043,27 +2083,21 @@ function useGenreDetailContent({
2043
2083
  trailing,
2044
2084
  genrePlaylist: hostPlaylist,
2045
2085
  expanded,
2046
- treeRelations
2086
+ treeStyles
2047
2087
  }) {
2048
- const { data: detail, isPending, isError } = (0, import_genre_tree20.useFetchGenreDetail)(genreUuid, scope, getBackendBaseUrl);
2088
+ const result = (0, import_genre_tree20.useFetchGenreDetail)(genreUuid, scope, getBackendBaseUrl);
2089
+ const detail = result.status === "success" ? result.genre : void 0;
2049
2090
  const defaultPlayback = useGenrePlaylistPlayback({ scope, genrePlaylists: [], artist: extras?.artist });
2050
2091
  const playback = hostPlayback ?? defaultPlayback;
2051
2092
  const view = (0, import_react15.useMemo)(() => {
2052
2093
  if (!detail) return null;
2053
2094
  const rootColor = (0, import_genre_tree_view4.getGenreTreeColor)(detail.root.wikidataId);
2054
- const treeStyles = /* @__PURE__ */ new Map();
2055
- if (treeRelations) {
2056
- const drawn = [treeRelations, treeRelations.parentNode, ...treeRelations.childNodes, ...treeRelations.ancestorNodes];
2057
- for (const entry of drawn) {
2058
- if (entry) treeStyles.set(entry.node.id, { fill: entry.fill, textColor: entry.textColor });
2059
- }
2060
- }
2061
2095
  const relative = (genre, parentId) => ({
2062
2096
  node: toNode(genre, parentId),
2063
- ...treeStyles.get(genre.uuid) ?? { fill: (0, import_genre_tree_view4.tintSurface)(rootColor), textColor: import_genre_tree_view4.TEXT_COLOR }
2097
+ ...treeStyles?.get(genre.uuid) ?? { fill: (0, import_genre_tree_view4.tintSurface)(rootColor), textColor: import_genre_tree_view4.TEXT_COLOR }
2064
2098
  });
2065
2099
  const ancestors2 = [...detail.ascendants].sort((a, b) => b.degree - a.degree);
2066
- const own = treeStyles.get(detail.uuid) ?? { fill: rootColor, textColor: (0, import_genre_tree_view4.getTextColorOnFill)(rootColor) };
2100
+ const own = treeStyles?.get(detail.uuid) ?? { fill: rootColor, textColor: (0, import_genre_tree_view4.getTextColorOnFill)(rootColor) };
2067
2101
  return {
2068
2102
  node: {
2069
2103
  id: detail.uuid,
@@ -2080,23 +2114,28 @@ function useGenreDetailContent({
2080
2114
  ({ ascendant }, index) => relative(ascendant, index === 0 ? null : ancestors2[index - 1].ascendant.uuid)
2081
2115
  )
2082
2116
  };
2083
- }, [detail, scope, treeRelations]);
2084
- if (isError) return {
2117
+ }, [detail, scope, treeStyles]);
2118
+ if (result.status === "not-found") return {
2119
+ name: null,
2120
+ title: "Genre not found",
2121
+ content: /* @__PURE__ */ (0, import_jsx_runtime37.jsx)("p", { className: "text-[var(--color-text-muted)]", children: "This genre does not exist." })
2122
+ };
2123
+ if (result.status === "error") return {
2085
2124
  name: null,
2086
2125
  title: "Genre unavailable",
2087
- content: /* @__PURE__ */ (0, import_jsx_runtime36.jsx)("p", { role: "alert", className: "text-body text-[var(--color-text)]", children: "This genre could not be loaded." })
2126
+ content: /* @__PURE__ */ (0, import_jsx_runtime37.jsx)("p", { role: "alert", className: "text-body text-[var(--color-text)]", children: "This genre could not be loaded." })
2088
2127
  };
2089
- if (isPending || !detail || !view) return {
2128
+ if (!detail || !view) return {
2090
2129
  name: null,
2091
2130
  title: "Loading genre\u2026",
2092
- content: /* @__PURE__ */ (0, import_jsx_runtime36.jsxs)("div", { className: "flex flex-col gap-3", "aria-busy": "true", "data-testid": "genre-detail-panel-skeleton", children: [
2093
- /* @__PURE__ */ (0, import_jsx_runtime36.jsx)(import_ui13.Skeleton, { className: "h-6 w-2/3" }),
2094
- /* @__PURE__ */ (0, import_jsx_runtime36.jsx)(import_ui13.Skeleton, { className: "h-4 w-full" }),
2095
- /* @__PURE__ */ (0, import_jsx_runtime36.jsx)(import_ui13.Skeleton, { className: "h-4 w-3/4" })
2131
+ content: /* @__PURE__ */ (0, import_jsx_runtime37.jsxs)("div", { className: "flex flex-col gap-3", "aria-busy": "true", "data-testid": "genre-detail-panel-skeleton", children: [
2132
+ /* @__PURE__ */ (0, import_jsx_runtime37.jsx)(import_ui13.Skeleton, { className: "h-6 w-2/3" }),
2133
+ /* @__PURE__ */ (0, import_jsx_runtime37.jsx)(import_ui13.Skeleton, { className: "h-4 w-full" }),
2134
+ /* @__PURE__ */ (0, import_jsx_runtime37.jsx)(import_ui13.Skeleton, { className: "h-4 w-3/4" })
2096
2135
  ] })
2097
2136
  };
2098
2137
  const selectGenre = (nodeId) => onSelectGenre(import_genre_tree20.GenreUuidSchema.parse(nodeId));
2099
- const family = /* @__PURE__ */ (0, import_jsx_runtime36.jsx)(
2138
+ const family = /* @__PURE__ */ (0, import_jsx_runtime37.jsx)(
2100
2139
  import_genre_tree_view4.GenreNodeFamily,
2101
2140
  {
2102
2141
  node: view.node,
@@ -2107,17 +2146,17 @@ function useGenreDetailContent({
2107
2146
  onSelectNode: selectGenre
2108
2147
  }
2109
2148
  );
2110
- const ancestors = /* @__PURE__ */ (0, import_jsx_runtime36.jsx)(import_genre_tree_view4.GenreNodeAncestors, { ancestorNodes: view.ancestorNodes, onSelectNode: selectGenre });
2149
+ const ancestors = /* @__PURE__ */ (0, import_jsx_runtime37.jsx)(import_genre_tree_view4.GenreNodeAncestors, { ancestorNodes: view.ancestorNodes, onSelectNode: selectGenre });
2111
2150
  return {
2112
2151
  name: detail.name,
2113
2152
  title: detail.name,
2114
2153
  headerAccent: { background: view.fill, color: view.textColor },
2115
2154
  aside: expanded ? ancestors : void 0,
2116
- content: /* @__PURE__ */ (0, import_jsx_runtime36.jsxs)(import_jsx_runtime36.Fragment, { children: [
2117
- /* @__PURE__ */ (0, import_jsx_runtime36.jsx)(
2155
+ content: /* @__PURE__ */ (0, import_jsx_runtime37.jsxs)(import_jsx_runtime37.Fragment, { children: [
2156
+ /* @__PURE__ */ (0, import_jsx_runtime37.jsx)(
2118
2157
  GenreInfoPanelBody,
2119
2158
  {
2120
- family: /* @__PURE__ */ (0, import_jsx_runtime36.jsxs)(import_jsx_runtime36.Fragment, { children: [
2159
+ family: /* @__PURE__ */ (0, import_jsx_runtime37.jsxs)(import_jsx_runtime37.Fragment, { children: [
2121
2160
  family,
2122
2161
  !expanded && ancestors
2123
2162
  ] }),
@@ -2161,7 +2200,7 @@ function GenreDetailPanel({
2161
2200
  expanded,
2162
2201
  band: { onZoomIn, shareUrl, shareTarget: shareUrl ? shareTarget : void 0 }
2163
2202
  });
2164
- return /* @__PURE__ */ (0, import_jsx_runtime36.jsx)(
2203
+ return /* @__PURE__ */ (0, import_jsx_runtime37.jsx)(
2165
2204
  import_ui13.DetailPanel,
2166
2205
  {
2167
2206
  side: "left",
@@ -2174,7 +2213,7 @@ function GenreDetailPanel({
2174
2213
  edgeTab,
2175
2214
  aside: panel.aside,
2176
2215
  titleInSearch,
2177
- headerActions: onExpandedChange ? /* @__PURE__ */ (0, import_jsx_runtime36.jsx)(import_ui13.IconButton, { "aria-label": expanded ? "Shrink panel" : "Extend panel", onClick: () => onExpandedChange(!expanded), children: expanded ? /* @__PURE__ */ (0, import_jsx_runtime36.jsx)(import_lucide_react13.Minimize2, { "aria-hidden": "true", className: "h-4 w-4" }) : /* @__PURE__ */ (0, import_jsx_runtime36.jsx)(import_lucide_react13.Maximize2, { "aria-hidden": "true", className: "h-4 w-4" }) }) : void 0,
2216
+ headerActions: onExpandedChange ? /* @__PURE__ */ (0, import_jsx_runtime37.jsx)(import_ui13.IconButton, { "aria-label": expanded ? "Shrink panel" : "Extend panel", onClick: () => onExpandedChange(!expanded), children: expanded ? /* @__PURE__ */ (0, import_jsx_runtime37.jsx)(import_lucide_react13.Minimize2, { "aria-hidden": "true", className: "h-4 w-4" }) : /* @__PURE__ */ (0, import_jsx_runtime37.jsx)(import_lucide_react13.Maximize2, { "aria-hidden": "true", className: "h-4 w-4" }) }) : void 0,
2178
2217
  children: panel.content
2179
2218
  }
2180
2219
  );
@@ -2183,7 +2222,7 @@ function GenreDetailPanel({
2183
2222
  // src/genre-tree/ChangeParentPopup.tsx
2184
2223
  var import_react16 = require("react");
2185
2224
  var import_genre_tree21 = require("@behindthemusictree/app-kit/genre-tree");
2186
- var import_jsx_runtime37 = require("react/jsx-runtime");
2225
+ var import_jsx_runtime38 = require("react/jsx-runtime");
2187
2226
  function ChangeParentPopup({
2188
2227
  genrePlaylist,
2189
2228
  genrePlaylists,
@@ -2218,12 +2257,12 @@ function ChangeParentPopup({
2218
2257
  window.addEventListener("keydown", handleKeyDown);
2219
2258
  return () => window.removeEventListener("keydown", handleKeyDown);
2220
2259
  }, [onClose]);
2221
- return /* @__PURE__ */ (0, import_jsx_runtime37.jsxs)(BasePopup_default, { title: `Change parent of ${genrePlaylist.name}`, onClose, showCancelButton: true, fitContent: true, children: [
2222
- /* @__PURE__ */ (0, import_jsx_runtime37.jsxs)("p", { className: "mb-3 text-body text-[var(--color-text-muted)]", children: [
2260
+ return /* @__PURE__ */ (0, import_jsx_runtime38.jsxs)(BasePopup_default, { title: `Change parent of ${genrePlaylist.name}`, onClose, showCancelButton: true, fitContent: true, children: [
2261
+ /* @__PURE__ */ (0, import_jsx_runtime38.jsxs)("p", { className: "mb-3 text-body text-[var(--color-text-muted)]", children: [
2223
2262
  "Current parent: ",
2224
2263
  genrePlaylist.parent?.name ?? "none"
2225
2264
  ] }),
2226
- /* @__PURE__ */ (0, import_jsx_runtime37.jsx)("div", { "aria-busy": isPending, className: isPending ? "pointer-events-none opacity-60" : void 0, children: /* @__PURE__ */ (0, import_jsx_runtime37.jsx)(
2265
+ /* @__PURE__ */ (0, import_jsx_runtime38.jsx)("div", { "aria-busy": isPending, className: isPending ? "pointer-events-none opacity-60" : void 0, children: /* @__PURE__ */ (0, import_jsx_runtime38.jsx)(
2227
2266
  GenreSearch,
2228
2267
  {
2229
2268
  genrePlaylists: candidates,
@@ -2231,7 +2270,7 @@ function ChangeParentPopup({
2231
2270
  placeholder: "Search a new parent\u2026"
2232
2271
  }
2233
2272
  ) }),
2234
- formErrors.length > 0 && /* @__PURE__ */ (0, import_jsx_runtime37.jsx)("p", { role: "alert", className: "mt-3 text-body text-[var(--color-red-500)]", children: formErrors.map(({ message }) => message).join(" ") })
2273
+ formErrors.length > 0 && /* @__PURE__ */ (0, import_jsx_runtime38.jsx)("p", { role: "alert", className: "mt-3 text-body text-[var(--color-red-500)]", children: formErrors.map(({ message }) => message).join(" ") })
2235
2274
  ] });
2236
2275
  }
2237
2276
 
@@ -2244,7 +2283,7 @@ var import_lucide_react14 = require("lucide-react");
2244
2283
  var import_ui14 = require("@behindthemusictree/ui");
2245
2284
  var import_genre_tree22 = require("@behindthemusictree/app-kit/genre-tree");
2246
2285
  var import_player7 = require("@behindthemusictree/app-kit/player");
2247
- var import_jsx_runtime38 = require("react/jsx-runtime");
2286
+ var import_jsx_runtime39 = require("react/jsx-runtime");
2248
2287
  function ArtistActionBand({ artist, share }) {
2249
2288
  const { trackList, playNewTrackListFromArtist } = (0, import_genre_tree22.useTrackList)();
2250
2289
  const { isPlaying, setIsPlaying } = (0, import_player7.usePlayer)();
@@ -2260,11 +2299,11 @@ function ArtistActionBand({ artist, share }) {
2260
2299
  console.error("Failed to load artist tracks:", error);
2261
2300
  });
2262
2301
  };
2263
- return /* @__PURE__ */ (0, import_jsx_runtime38.jsx)(import_ui14.PanelBleed, { children: /* @__PURE__ */ (0, import_jsx_runtime38.jsxs)(import_ui14.ActionBand, { children: [
2264
- /* @__PURE__ */ (0, import_jsx_runtime38.jsx)(
2302
+ return /* @__PURE__ */ (0, import_jsx_runtime39.jsx)(import_ui14.PanelBleed, { children: /* @__PURE__ */ (0, import_jsx_runtime39.jsxs)(import_ui14.ActionBand, { children: [
2303
+ /* @__PURE__ */ (0, import_jsx_runtime39.jsx)(
2265
2304
  import_ui14.ActionButton,
2266
2305
  {
2267
- icon: playing ? /* @__PURE__ */ (0, import_jsx_runtime38.jsx)(import_lucide_react14.Pause, { "aria-hidden": "true", className: "w-5 h-5" }) : /* @__PURE__ */ (0, import_jsx_runtime38.jsx)(import_lucide_react14.Play, { "aria-hidden": "true", className: "w-5 h-5" }),
2306
+ icon: playing ? /* @__PURE__ */ (0, import_jsx_runtime39.jsx)(import_lucide_react14.Pause, { "aria-hidden": "true", className: "w-5 h-5" }) : /* @__PURE__ */ (0, import_jsx_runtime39.jsx)(import_lucide_react14.Play, { "aria-hidden": "true", className: "w-5 h-5" }),
2268
2307
  label: playing ? "Pause" : "Play",
2269
2308
  caption: String(artist.playableTracksCount),
2270
2309
  disabled: artist.playableTracksCount === 0,
@@ -2283,7 +2322,7 @@ var import_ui16 = require("@behindthemusictree/ui");
2283
2322
  var import_lucide_react15 = require("lucide-react");
2284
2323
  var import_si3 = require("react-icons/si");
2285
2324
  var import_ui15 = require("@behindthemusictree/ui");
2286
- var import_jsx_runtime39 = require("react/jsx-runtime");
2325
+ var import_jsx_runtime40 = require("react/jsx-runtime");
2287
2326
  var LINK_KINDS = {
2288
2327
  official: { label: "Official website", Icon: import_lucide_react15.Globe },
2289
2328
  wikipedia: { label: "Wikipedia", Icon: import_si3.SiWikipedia },
@@ -2320,7 +2359,7 @@ function artistLinks(artist) {
2320
2359
  return links;
2321
2360
  }
2322
2361
  function ArtistLinks({ links }) {
2323
- return /* @__PURE__ */ (0, import_jsx_runtime39.jsx)("ul", { className: "flex flex-wrap gap-1.5", children: links.map(({ key, url, Icon, label }) => /* @__PURE__ */ (0, import_jsx_runtime39.jsx)("li", { children: /* @__PURE__ */ (0, import_jsx_runtime39.jsx)(import_ui15.Chip, { href: url, icon: /* @__PURE__ */ (0, import_jsx_runtime39.jsx)(Icon, { className: "h-3.5 w-3.5 shrink-0", "aria-hidden": "true" }), children: label }) }, key)) });
2362
+ return /* @__PURE__ */ (0, import_jsx_runtime40.jsx)("ul", { className: "flex flex-wrap gap-1.5", children: links.map(({ key, url, Icon, label }) => /* @__PURE__ */ (0, import_jsx_runtime40.jsx)("li", { children: /* @__PURE__ */ (0, import_jsx_runtime40.jsx)(import_ui15.Chip, { href: url, icon: /* @__PURE__ */ (0, import_jsx_runtime40.jsx)(Icon, { className: "h-3.5 w-3.5 shrink-0", "aria-hidden": "true" }), children: label }) }, key)) });
2324
2363
  }
2325
2364
 
2326
2365
  // src/genre-tree/artist/formatYears.ts
@@ -2331,7 +2370,7 @@ function formatYears(beginYear, endYear, ended) {
2331
2370
  }
2332
2371
 
2333
2372
  // src/genre-tree/artist/ArtistHeader.tsx
2334
- var import_jsx_runtime40 = require("react/jsx-runtime");
2373
+ var import_jsx_runtime41 = require("react/jsx-runtime");
2335
2374
  var countryNames = new Intl.DisplayNames(["en"], { type: "region" });
2336
2375
  function ArtistHeader({ artist }) {
2337
2376
  const fields = [];
@@ -2341,35 +2380,23 @@ function ArtistHeader({ artist }) {
2341
2380
  const years = formatYears(artist.beginYear, artist.endYear, artist.ended);
2342
2381
  if (years) fields.push({ label: "Active", value: years });
2343
2382
  const links = artistLinks(artist);
2344
- if (links.length > 0) fields.push({ label: "Links", value: /* @__PURE__ */ (0, import_jsx_runtime40.jsx)(ArtistLinks, { links }) });
2383
+ if (links.length > 0) fields.push({ label: "Links", value: /* @__PURE__ */ (0, import_jsx_runtime41.jsx)(ArtistLinks, { links }) });
2345
2384
  if (fields.length === 0) return null;
2346
- return /* @__PURE__ */ (0, import_jsx_runtime40.jsx)(import_ui16.FieldList, { items: fields });
2385
+ return /* @__PURE__ */ (0, import_jsx_runtime41.jsx)(import_ui16.FieldList, { items: fields });
2347
2386
  }
2348
2387
 
2349
2388
  // src/genre-tree/artist/ArtistPhoto.tsx
2350
- var import_jsx_runtime41 = require("react/jsx-runtime");
2351
- function ExternalLink({ href, children }) {
2352
- return /* @__PURE__ */ (0, import_jsx_runtime41.jsx)("a", { href, target: "_blank", rel: "noopener noreferrer", className: "underline-offset-2 hover:underline", children });
2353
- }
2389
+ var import_jsx_runtime42 = require("react/jsx-runtime");
2354
2390
  function ArtistPhoto({ image, name }) {
2355
- return /* @__PURE__ */ (0, import_jsx_runtime41.jsxs)("figure", { className: "flex flex-col gap-1.5", children: [
2356
- /* @__PURE__ */ (0, import_jsx_runtime41.jsx)("img", { src: image.url, alt: name, className: "h-64 w-full object-contain bg-[var(--color-surface-muted)] rounded" }),
2357
- /* @__PURE__ */ (0, import_jsx_runtime41.jsxs)("figcaption", { className: "text-caption text-[var(--color-text-muted)]", children: [
2358
- "Photo: ",
2359
- /* @__PURE__ */ (0, import_jsx_runtime41.jsx)(ExternalLink, { href: image.pageUrl, children: image.author ?? "Wikimedia Commons" }),
2360
- ",",
2361
- " ",
2362
- image.licenseUrl ? /* @__PURE__ */ (0, import_jsx_runtime41.jsx)(ExternalLink, { href: image.licenseUrl, children: image.license }) : image.license
2363
- ] })
2364
- ] });
2391
+ return /* @__PURE__ */ (0, import_jsx_runtime42.jsx)(CommonsPhoto, { image, alt: name });
2365
2392
  }
2366
2393
 
2367
2394
  // src/genre-tree/artist/ArtistMembers.tsx
2368
2395
  var import_ui17 = require("@behindthemusictree/ui");
2369
- var import_jsx_runtime42 = require("react/jsx-runtime");
2396
+ var import_jsx_runtime43 = require("react/jsx-runtime");
2370
2397
  function MembershipList({ title, memberships, renderArtistLink }) {
2371
2398
  if (memberships.length === 0) return null;
2372
- return /* @__PURE__ */ (0, import_jsx_runtime42.jsx)(import_ui17.PanelGroup, { title, children: /* @__PURE__ */ (0, import_jsx_runtime42.jsx)("ul", { children: memberships.map((membership, index) => {
2399
+ return /* @__PURE__ */ (0, import_jsx_runtime43.jsx)(import_ui17.PanelGroup, { title, children: /* @__PURE__ */ (0, import_jsx_runtime43.jsx)("ul", { children: memberships.map((membership, index) => {
2373
2400
  const details = [
2374
2401
  formatYears(membership.beginYear, membership.endYear, membership.ended),
2375
2402
  ...membership.roles
@@ -2377,9 +2404,9 @@ function MembershipList({ title, memberships, renderArtistLink }) {
2377
2404
  const detailsText = details.filter(Boolean).join(" \xB7 ");
2378
2405
  return (
2379
2406
  // A member can have several periods in the same group.
2380
- /* @__PURE__ */ (0, import_jsx_runtime42.jsxs)("li", { className: "truncate py-0.5", title: detailsText || void 0, children: [
2381
- /* @__PURE__ */ (0, import_jsx_runtime42.jsx)("span", { className: "font-medium text-[var(--color-text)]", children: renderArtistLink(membership.artist) }),
2382
- detailsText && /* @__PURE__ */ (0, import_jsx_runtime42.jsxs)("span", { className: "text-[var(--color-text-muted)]", children: [
2407
+ /* @__PURE__ */ (0, import_jsx_runtime43.jsxs)("li", { className: "truncate py-0.5", title: detailsText || void 0, children: [
2408
+ /* @__PURE__ */ (0, import_jsx_runtime43.jsx)("span", { className: "font-medium text-[var(--color-text)]", children: renderArtistLink(membership.artist) }),
2409
+ detailsText && /* @__PURE__ */ (0, import_jsx_runtime43.jsxs)("span", { className: "text-[var(--color-text-muted)]", children: [
2383
2410
  " \u2014 ",
2384
2411
  detailsText
2385
2412
  ] })
@@ -2390,9 +2417,9 @@ function MembershipList({ title, memberships, renderArtistLink }) {
2390
2417
  function ArtistMembers({ title, memberships, renderArtistLink }) {
2391
2418
  if (memberships.length === 0) return null;
2392
2419
  const isCurrent = (membership) => !membership.ended && membership.endYear === null;
2393
- return /* @__PURE__ */ (0, import_jsx_runtime42.jsxs)(import_ui17.PanelSection, { title, children: [
2394
- /* @__PURE__ */ (0, import_jsx_runtime42.jsx)(MembershipList, { title: "Current", memberships: memberships.filter(isCurrent), renderArtistLink }),
2395
- /* @__PURE__ */ (0, import_jsx_runtime42.jsx)(
2420
+ return /* @__PURE__ */ (0, import_jsx_runtime43.jsxs)(import_ui17.PanelSection, { title, children: [
2421
+ /* @__PURE__ */ (0, import_jsx_runtime43.jsx)(MembershipList, { title: "Current", memberships: memberships.filter(isCurrent), renderArtistLink }),
2422
+ /* @__PURE__ */ (0, import_jsx_runtime43.jsx)(
2396
2423
  MembershipList,
2397
2424
  {
2398
2425
  title: "Past",
@@ -2405,7 +2432,7 @@ function ArtistMembers({ title, memberships, renderArtistLink }) {
2405
2432
 
2406
2433
  // src/genre-tree/artist/ArtistDiscography.tsx
2407
2434
  var import_ui18 = require("@behindthemusictree/ui");
2408
- var import_jsx_runtime43 = require("react/jsx-runtime");
2435
+ var import_jsx_runtime44 = require("react/jsx-runtime");
2409
2436
  var GROUPS = ["Albums", "EPs", "Singles", "Live", "Compilations", "Other"];
2410
2437
  var PRIMARY_GROUPS = { album: "Albums", ep: "EPs", single: "Singles" };
2411
2438
  function discographyGroup({ primaryType, secondaryTypes }) {
@@ -2425,12 +2452,12 @@ function byYear(a, b) {
2425
2452
  function ArtistDiscography({ albums }) {
2426
2453
  if (albums.length === 0) return null;
2427
2454
  const sorted = [...albums].sort(byYear);
2428
- return /* @__PURE__ */ (0, import_jsx_runtime43.jsx)(import_ui18.PanelSection, { title: "Discography", children: GROUPS.map((group) => {
2455
+ return /* @__PURE__ */ (0, import_jsx_runtime44.jsx)(import_ui18.PanelSection, { title: "Discography", children: GROUPS.map((group) => {
2429
2456
  const groupAlbums = sorted.filter((album) => discographyGroup(album) === group);
2430
2457
  if (groupAlbums.length === 0) return null;
2431
- return /* @__PURE__ */ (0, import_jsx_runtime43.jsx)(import_ui18.PanelGroup, { title: group, children: /* @__PURE__ */ (0, import_jsx_runtime43.jsx)("ul", { children: groupAlbums.map((album) => /* @__PURE__ */ (0, import_jsx_runtime43.jsxs)("li", { className: "grid grid-cols-[2.5rem_minmax(0,1fr)] gap-2 py-0.5", children: [
2432
- /* @__PURE__ */ (0, import_jsx_runtime43.jsx)("span", { className: "tabular-nums text-[var(--color-text-muted)]", children: album.year ?? "\u2014" }),
2433
- /* @__PURE__ */ (0, import_jsx_runtime43.jsx)("span", { className: "truncate text-[var(--color-text)]", title: album.name, children: album.name })
2458
+ return /* @__PURE__ */ (0, import_jsx_runtime44.jsx)(import_ui18.PanelGroup, { title: group, children: /* @__PURE__ */ (0, import_jsx_runtime44.jsx)("ul", { children: groupAlbums.map((album) => /* @__PURE__ */ (0, import_jsx_runtime44.jsxs)("li", { className: "grid grid-cols-[2.5rem_minmax(0,1fr)] gap-2 py-0.5", children: [
2459
+ /* @__PURE__ */ (0, import_jsx_runtime44.jsx)("span", { className: "tabular-nums text-[var(--color-text-muted)]", children: album.year ?? "\u2014" }),
2460
+ /* @__PURE__ */ (0, import_jsx_runtime44.jsx)("span", { className: "truncate text-[var(--color-text)]", title: album.name, children: album.name })
2434
2461
  ] }, album.uuid)) }) }, group);
2435
2462
  }) });
2436
2463
  }
@@ -2438,7 +2465,6 @@ function ArtistDiscography({ albums }) {
2438
2465
  // src/genre-tree/artist/ArtistSongs.tsx
2439
2466
  var import_react17 = require("react");
2440
2467
  var import_react_query2 = require("@tanstack/react-query");
2441
- var import_lucide_react16 = require("lucide-react");
2442
2468
  var import_ui19 = require("@behindthemusictree/ui");
2443
2469
  var import_player11 = require("@behindthemusictree/app-kit/player");
2444
2470
  var import_genre_tree26 = require("@behindthemusictree/app-kit/genre-tree");
@@ -2452,7 +2478,7 @@ var import_genre_tree23 = require("@behindthemusictree/app-kit/genre-tree");
2452
2478
  var import_fa5 = require("react-icons/fa");
2453
2479
  var import_player8 = require("@behindthemusictree/app-kit/player");
2454
2480
  var import_player9 = require("@behindthemusictree/app-kit/player");
2455
- var import_jsx_runtime44 = require("react/jsx-runtime");
2481
+ var import_jsx_runtime45 = require("react/jsx-runtime");
2456
2482
  function TrackPositionPlayPause({
2457
2483
  position,
2458
2484
  uuid,
@@ -2466,14 +2492,14 @@ function TrackPositionPlayPause({
2466
2492
  const isPlaying = isThisTrack && isConfirmedPlayable && playState === import_player9.PlayStates.PLAYING;
2467
2493
  const isPaused = isThisTrack && isConfirmedPlayable && playState === import_player9.PlayStates.PAUSED;
2468
2494
  const isCurrentTrackActive = isPlaying || isPaused;
2469
- return /* @__PURE__ */ (0, import_jsx_runtime44.jsxs)(
2495
+ return /* @__PURE__ */ (0, import_jsx_runtime45.jsxs)(
2470
2496
  "div",
2471
2497
  {
2472
2498
  className: "track-position-play-pause relative flex items-center justify-center gap-2 text-lg w-16 cursor-pointer",
2473
2499
  style: { minWidth: "4rem", maxWidth: "4rem" },
2474
2500
  onClick: handlePlayPauseClick,
2475
2501
  children: [
2476
- /* @__PURE__ */ (0, import_jsx_runtime44.jsx)("div", { className: "track-position-play-pause__status group-hover:hidden flex items-center justify-center", children: isAwaitingPlayback ? /* @__PURE__ */ (0, import_jsx_runtime44.jsx)("span", { className: "track-position-play-pause__spinner", role: "status", "aria-label": "Loading track" }) : isCurrentTrackActive ? /* @__PURE__ */ (0, import_jsx_runtime44.jsxs)(
2502
+ /* @__PURE__ */ (0, import_jsx_runtime45.jsx)("div", { className: "track-position-play-pause__status group-hover:hidden flex items-center justify-center", children: isAwaitingPlayback ? /* @__PURE__ */ (0, import_jsx_runtime45.jsx)("span", { className: "track-position-play-pause__spinner", role: "status", "aria-label": "Loading track" }) : isCurrentTrackActive ? /* @__PURE__ */ (0, import_jsx_runtime45.jsxs)(
2477
2503
  "span",
2478
2504
  {
2479
2505
  className: "track-position-play-pause__bars",
@@ -2481,18 +2507,18 @@ function TrackPositionPlayPause({
2481
2507
  role: "img",
2482
2508
  "aria-label": isPlaying ? "Playing" : "Paused",
2483
2509
  children: [
2484
- /* @__PURE__ */ (0, import_jsx_runtime44.jsx)("span", { className: "track-position-play-pause__bar" }),
2485
- /* @__PURE__ */ (0, import_jsx_runtime44.jsx)("span", { className: "track-position-play-pause__bar" }),
2486
- /* @__PURE__ */ (0, import_jsx_runtime44.jsx)("span", { className: "track-position-play-pause__bar" })
2510
+ /* @__PURE__ */ (0, import_jsx_runtime45.jsx)("span", { className: "track-position-play-pause__bar" }),
2511
+ /* @__PURE__ */ (0, import_jsx_runtime45.jsx)("span", { className: "track-position-play-pause__bar" }),
2512
+ /* @__PURE__ */ (0, import_jsx_runtime45.jsx)("span", { className: "track-position-play-pause__bar" })
2487
2513
  ]
2488
2514
  }
2489
2515
  ) : position }),
2490
- /* @__PURE__ */ (0, import_jsx_runtime44.jsx)(
2516
+ /* @__PURE__ */ (0, import_jsx_runtime45.jsx)(
2491
2517
  "div",
2492
2518
  {
2493
2519
  className: `hidden group-hover:flex items-center justify-center ${isCurrentTrackActive ? "" : "absolute inset-0"}`,
2494
2520
  "aria-hidden": "true",
2495
- children: isAwaitingPlayback ? /* @__PURE__ */ (0, import_jsx_runtime44.jsx)("span", { className: "track-position-play-pause__spinner" }) : isPlaying ? /* @__PURE__ */ (0, import_jsx_runtime44.jsx)(import_fa5.FaPause, {}) : /* @__PURE__ */ (0, import_jsx_runtime44.jsx)(import_fa5.FaPlay, {})
2521
+ children: isAwaitingPlayback ? /* @__PURE__ */ (0, import_jsx_runtime45.jsx)("span", { className: "track-position-play-pause__spinner" }) : isPlaying ? /* @__PURE__ */ (0, import_jsx_runtime45.jsx)(import_fa5.FaPause, {}) : /* @__PURE__ */ (0, import_jsx_runtime45.jsx)(import_fa5.FaPlay, {})
2496
2522
  }
2497
2523
  )
2498
2524
  ]
@@ -2503,7 +2529,7 @@ function TrackPositionPlayPause({
2503
2529
  // src/genre-tree/track-list-sidebar/TrackItem.tsx
2504
2530
  var import_genre_tree24 = require("@behindthemusictree/app-kit/genre-tree");
2505
2531
  var import_genre_tree25 = require("@behindthemusictree/app-kit/genre-tree");
2506
- var import_jsx_runtime45 = require("react/jsx-runtime");
2532
+ var import_jsx_runtime46 = require("react/jsx-runtime");
2507
2533
  function TrackItem({
2508
2534
  track,
2509
2535
  position,
@@ -2527,12 +2553,12 @@ function TrackItem({
2527
2553
  loadTrackForPlayer(track.uuid);
2528
2554
  }
2529
2555
  };
2530
- return /* @__PURE__ */ (0, import_jsx_runtime45.jsxs)(
2556
+ return /* @__PURE__ */ (0, import_jsx_runtime46.jsxs)(
2531
2557
  "div",
2532
2558
  {
2533
2559
  className: `track-item flex h-14 text-[var(--color-text)] ${isUnplayable ? "opacity-50" : "hover:bg-[var(--color-surface-muted)] group"}`,
2534
2560
  children: [
2535
- isUnplayable ? /* @__PURE__ */ (0, import_jsx_runtime45.jsx)(
2561
+ isUnplayable ? /* @__PURE__ */ (0, import_jsx_runtime46.jsx)(
2536
2562
  "div",
2537
2563
  {
2538
2564
  className: "flex items-center justify-center text-lg w-16",
@@ -2540,7 +2566,7 @@ function TrackItem({
2540
2566
  "aria-disabled": "true",
2541
2567
  children: position
2542
2568
  }
2543
- ) : /* @__PURE__ */ (0, import_jsx_runtime45.jsx)(
2569
+ ) : /* @__PURE__ */ (0, import_jsx_runtime46.jsx)(
2544
2570
  TrackPositionPlayPause,
2545
2571
  {
2546
2572
  position,
@@ -2549,12 +2575,12 @@ function TrackItem({
2549
2575
  isSelected: selectedTrack?.uuid === track.uuid
2550
2576
  }
2551
2577
  ),
2552
- /* @__PURE__ */ (0, import_jsx_runtime45.jsxs)("div", { className: "title-artist-container flex flex-col items-start justify-center min-w-0 flex-1", children: [
2553
- /* @__PURE__ */ (0, import_jsx_runtime45.jsx)("div", { className: "flex max-w-full items-center gap-2", children: /* @__PURE__ */ (0, import_jsx_runtime45.jsx)("div", { className: "title text-lg font-bold text-[var(--color-text)] max-w-full truncate", children: renderSongLink ? renderSongLink(track) : (0, import_genre_tree24.isSong)(track) ? /* @__PURE__ */ (0, import_jsx_runtime45.jsx)(MusicBrainzRecordingLink, { song: track, className: "hover:underline focus-visible:underline", children: track.title }) : track.title }) }),
2554
- track.artists && track.artists.length > 0 ? /* @__PURE__ */ (0, import_jsx_runtime45.jsx)("div", { className: "artist text-base max-w-full truncate text-[var(--color-text-muted)]", children: /* @__PURE__ */ (0, import_jsx_runtime45.jsx)(import_genre_tree25.ArtistNames, { artists: track.artists, renderArtistLink }) }) : ""
2578
+ /* @__PURE__ */ (0, import_jsx_runtime46.jsxs)("div", { className: "title-artist-container flex flex-col items-start justify-center min-w-0 flex-1", children: [
2579
+ /* @__PURE__ */ (0, import_jsx_runtime46.jsx)("div", { className: "flex max-w-full items-center gap-2", children: /* @__PURE__ */ (0, import_jsx_runtime46.jsx)("div", { className: "title text-lg font-bold text-[var(--color-text)] max-w-full truncate", children: renderSongLink ? renderSongLink(track) : (0, import_genre_tree24.isSong)(track) ? /* @__PURE__ */ (0, import_jsx_runtime46.jsx)(MusicBrainzRecordingLink, { song: track, className: "hover:underline focus-visible:underline", children: track.title }) : track.title }) }),
2580
+ track.artists && track.artists.length > 0 ? /* @__PURE__ */ (0, import_jsx_runtime46.jsx)("div", { className: "artist text-base max-w-full truncate text-[var(--color-text-muted)]", children: /* @__PURE__ */ (0, import_jsx_runtime46.jsx)(import_genre_tree25.ArtistNames, { artists: track.artists, renderArtistLink }) }) : ""
2555
2581
  ] }),
2556
- /* @__PURE__ */ (0, import_jsx_runtime45.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_runtime45.jsx)("div", { className: "genre-name max-w-full whitespace-normal break-words p-1 text-caption font-bold", children: track.genre.name }) : "" }),
2557
- renderDuration && /* @__PURE__ */ (0, import_jsx_runtime45.jsx)(
2582
+ /* @__PURE__ */ (0, import_jsx_runtime46.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_runtime46.jsx)("div", { className: "genre-name max-w-full whitespace-normal break-words p-1 text-caption font-bold", children: track.genre.name }) : "" }),
2583
+ renderDuration && /* @__PURE__ */ (0, import_jsx_runtime46.jsx)(
2558
2584
  "div",
2559
2585
  {
2560
2586
  className: "duration flex text-base w-16 items-center justify-center",
@@ -2562,14 +2588,14 @@ function TrackItem({
2562
2588
  children: renderDuration(track)
2563
2589
  }
2564
2590
  ),
2565
- renderActions && /* @__PURE__ */ (0, import_jsx_runtime45.jsx)("div", { className: "edit flex text-base w-6 items-center justify-center mr-2 cursor-pointer", children: renderActions(track) })
2591
+ renderActions && /* @__PURE__ */ (0, import_jsx_runtime46.jsx)("div", { className: "edit flex text-base w-6 items-center justify-center mr-2 cursor-pointer", children: renderActions(track) })
2566
2592
  ]
2567
2593
  }
2568
2594
  );
2569
2595
  }
2570
2596
 
2571
2597
  // src/genre-tree/artist/ArtistSongs.tsx
2572
- var import_jsx_runtime46 = require("react/jsx-runtime");
2598
+ var import_jsx_runtime47 = require("react/jsx-runtime");
2573
2599
  var SongPageSchema2 = (0, import_transport8.PaginatedResponseSchema)(import_genre_tree26.SongDetailedSchema);
2574
2600
  var SORTS = [
2575
2601
  { ordering: "title", label: "Title" },
@@ -2602,36 +2628,22 @@ function ArtistSongs({ artist, getBackendBaseUrl, renderArtistLink, renderSongLi
2602
2628
  event.stopPropagation();
2603
2629
  void play(song.uuid);
2604
2630
  };
2605
- return /* @__PURE__ */ (0, import_jsx_runtime46.jsx)(import_ui19.PanelSection, { title: "Songs", children: isPending ? /* @__PURE__ */ (0, import_jsx_runtime46.jsx)(import_ui19.Skeleton, { className: "h-14", "aria-busy": "true" }) : isError ? /* @__PURE__ */ (0, import_jsx_runtime46.jsx)("p", { className: "text-red-600", children: "Could not load songs." }) : data.results.length === 0 ? /* @__PURE__ */ (0, import_jsx_runtime46.jsx)("p", { className: "text-[var(--color-text-muted)]", children: "No songs yet." }) : /* @__PURE__ */ (0, import_jsx_runtime46.jsxs)(import_jsx_runtime46.Fragment, { children: [
2606
- /* @__PURE__ */ (0, import_jsx_runtime46.jsxs)("div", { className: "flex flex-wrap items-center justify-between gap-2 mb-2", children: [
2607
- /* @__PURE__ */ (0, import_jsx_runtime46.jsxs)(
2608
- "button",
2609
- {
2610
- type: "button",
2611
- onClick: () => void play(),
2612
- className: "flex w-fit items-center gap-1.5 px-3 py-1 text-caption font-medium bg-gray-900 text-white rounded-full hover:bg-gray-700",
2613
- children: [
2614
- /* @__PURE__ */ (0, import_jsx_runtime46.jsx)(import_lucide_react16.Play, { className: "h-3.5 w-3.5", "aria-hidden": "true" }),
2615
- "Play all"
2616
- ]
2617
- }
2618
- ),
2619
- /* @__PURE__ */ (0, import_jsx_runtime46.jsx)("div", { role: "group", "aria-label": "Sort songs by", className: "flex gap-1 text-caption", children: SORTS.map((sort) => /* @__PURE__ */ (0, import_jsx_runtime46.jsx)(
2620
- "button",
2621
- {
2622
- type: "button",
2623
- "aria-pressed": ordering === sort.ordering,
2624
- onClick: () => {
2625
- setOrdering(sort.ordering);
2626
- setPage(1);
2627
- },
2628
- className: "px-2.5 py-0.5 text-[var(--color-text)] rounded-full hover:bg-[var(--color-surface-muted)] aria-pressed:bg-gray-900 aria-pressed:text-white",
2629
- children: sort.label
2631
+ return /* @__PURE__ */ (0, import_jsx_runtime47.jsx)(import_ui19.PanelSection, { title: "Songs", children: isPending ? /* @__PURE__ */ (0, import_jsx_runtime47.jsx)(import_ui19.Skeleton, { className: "h-14", "aria-busy": "true" }) : isError ? /* @__PURE__ */ (0, import_jsx_runtime47.jsx)("p", { className: "text-red-600", children: "Could not load songs." }) : data.results.length === 0 ? /* @__PURE__ */ (0, import_jsx_runtime47.jsx)("p", { className: "text-[var(--color-text-muted)]", children: "No songs yet." }) : /* @__PURE__ */ (0, import_jsx_runtime47.jsxs)(import_jsx_runtime47.Fragment, { children: [
2632
+ /* @__PURE__ */ (0, import_jsx_runtime47.jsx)("div", { className: "flex flex-wrap items-center justify-end gap-2 mb-2", children: /* @__PURE__ */ (0, import_jsx_runtime47.jsx)("div", { role: "group", "aria-label": "Sort songs by", className: "flex gap-1 text-caption", children: SORTS.map((sort) => /* @__PURE__ */ (0, import_jsx_runtime47.jsx)(
2633
+ "button",
2634
+ {
2635
+ type: "button",
2636
+ "aria-pressed": ordering === sort.ordering,
2637
+ onClick: () => {
2638
+ setOrdering(sort.ordering);
2639
+ setPage(1);
2630
2640
  },
2631
- sort.ordering
2632
- )) })
2633
- ] }),
2634
- /* @__PURE__ */ (0, import_jsx_runtime46.jsx)("ul", { className: "overflow-hidden mb-2 bg-zinc-950 rounded-lg", children: data.results.map((song, index) => /* @__PURE__ */ (0, import_jsx_runtime46.jsx)("li", { onClickCapture: (event) => handleRowClickCapture(event, song), children: /* @__PURE__ */ (0, import_jsx_runtime46.jsx)(
2641
+ className: "px-2.5 py-0.5 text-[var(--color-text)] rounded-full hover:bg-[var(--color-surface-muted)] aria-pressed:bg-gray-900 aria-pressed:text-white",
2642
+ children: sort.label
2643
+ },
2644
+ sort.ordering
2645
+ )) }) }),
2646
+ /* @__PURE__ */ (0, import_jsx_runtime47.jsx)("ul", { className: "overflow-hidden mb-2 bg-zinc-950 rounded-lg", children: data.results.map((song, index) => /* @__PURE__ */ (0, import_jsx_runtime47.jsx)("li", { onClickCapture: (event) => handleRowClickCapture(event, song), children: /* @__PURE__ */ (0, import_jsx_runtime47.jsx)(
2635
2647
  TrackItem,
2636
2648
  {
2637
2649
  track: song,
@@ -2640,12 +2652,12 @@ function ArtistSongs({ artist, getBackendBaseUrl, renderArtistLink, renderSongLi
2640
2652
  renderSongLink
2641
2653
  }
2642
2654
  ) }, song.uuid)) }),
2643
- /* @__PURE__ */ (0, import_jsx_runtime46.jsx)(import_ui19.Pagination, { currentPage: page, totalPages: data.totalPages, onPageChange: setPage })
2655
+ /* @__PURE__ */ (0, import_jsx_runtime47.jsx)(import_ui19.Pagination, { currentPage: page, totalPages: data.totalPages, onPageChange: setPage })
2644
2656
  ] }) });
2645
2657
  }
2646
2658
 
2647
2659
  // src/genre-tree/artist/useArtistPanel.tsx
2648
- var import_jsx_runtime47 = require("react/jsx-runtime");
2660
+ var import_jsx_runtime48 = require("react/jsx-runtime");
2649
2661
  function useArtistPanel({
2650
2662
  uuid,
2651
2663
  scope,
@@ -2666,26 +2678,26 @@ function useArtistPanel({
2666
2678
  return {
2667
2679
  ...panel,
2668
2680
  title: "Artist",
2669
- content: /* @__PURE__ */ (0, import_jsx_runtime47.jsxs)("div", { "aria-busy": "true", className: "flex flex-col gap-2.5", children: [
2670
- /* @__PURE__ */ (0, import_jsx_runtime47.jsx)(import_ui20.Skeleton, { className: "h-3 w-16" }),
2671
- /* @__PURE__ */ (0, import_jsx_runtime47.jsx)(import_ui20.Skeleton, { className: "h-4 w-32" }),
2672
- /* @__PURE__ */ (0, import_jsx_runtime47.jsx)(import_ui20.Skeleton, { className: "h-3 w-16" }),
2673
- /* @__PURE__ */ (0, import_jsx_runtime47.jsx)(import_ui20.Skeleton, { className: "h-4 w-24" }),
2674
- /* @__PURE__ */ (0, import_jsx_runtime47.jsx)(import_ui20.Skeleton, { className: "h-16 mt-2.5" }),
2675
- /* @__PURE__ */ (0, import_jsx_runtime47.jsx)(import_ui20.Skeleton, { className: "h-16" })
2681
+ content: /* @__PURE__ */ (0, import_jsx_runtime48.jsxs)("div", { "aria-busy": "true", className: "flex flex-col gap-2.5", children: [
2682
+ /* @__PURE__ */ (0, import_jsx_runtime48.jsx)(import_ui20.Skeleton, { className: "h-3 w-16" }),
2683
+ /* @__PURE__ */ (0, import_jsx_runtime48.jsx)(import_ui20.Skeleton, { className: "h-4 w-32" }),
2684
+ /* @__PURE__ */ (0, import_jsx_runtime48.jsx)(import_ui20.Skeleton, { className: "h-3 w-16" }),
2685
+ /* @__PURE__ */ (0, import_jsx_runtime48.jsx)(import_ui20.Skeleton, { className: "h-4 w-24" }),
2686
+ /* @__PURE__ */ (0, import_jsx_runtime48.jsx)(import_ui20.Skeleton, { className: "h-16 mt-2.5" }),
2687
+ /* @__PURE__ */ (0, import_jsx_runtime48.jsx)(import_ui20.Skeleton, { className: "h-16" })
2676
2688
  ] })
2677
2689
  };
2678
2690
  case "not-found":
2679
2691
  return {
2680
2692
  ...panel,
2681
2693
  title: "Artist not found",
2682
- content: /* @__PURE__ */ (0, import_jsx_runtime47.jsx)("p", { className: "text-[var(--color-text-muted)]", children: "This artist does not exist." })
2694
+ content: /* @__PURE__ */ (0, import_jsx_runtime48.jsx)("p", { className: "text-[var(--color-text-muted)]", children: "This artist does not exist." })
2683
2695
  };
2684
2696
  case "error":
2685
2697
  return {
2686
2698
  ...panel,
2687
2699
  title: "Artist",
2688
- content: /* @__PURE__ */ (0, import_jsx_runtime47.jsx)("p", { className: "text-red-600", children: "Could not load this artist." })
2700
+ content: /* @__PURE__ */ (0, import_jsx_runtime48.jsx)("p", { className: "text-red-600", children: "Could not load this artist." })
2689
2701
  };
2690
2702
  case "success": {
2691
2703
  const { artist } = result;
@@ -2694,19 +2706,19 @@ function useArtistPanel({
2694
2706
  return {
2695
2707
  ...panel,
2696
2708
  title: artist.name,
2697
- content: /* @__PURE__ */ (0, import_jsx_runtime47.jsxs)(import_jsx_runtime47.Fragment, { children: [
2698
- /* @__PURE__ */ (0, import_jsx_runtime47.jsx)(
2709
+ content: /* @__PURE__ */ (0, import_jsx_runtime48.jsxs)(import_jsx_runtime48.Fragment, { children: [
2710
+ /* @__PURE__ */ (0, import_jsx_runtime48.jsx)(
2699
2711
  ArtistActionBand,
2700
2712
  {
2701
2713
  artist,
2702
- share: shareTarget && shareUrl ? /* @__PURE__ */ (0, import_jsx_runtime47.jsx)(ShareActionButton, { target: shareTarget, url: shareUrl }) : void 0
2714
+ share: shareTarget && shareUrl ? /* @__PURE__ */ (0, import_jsx_runtime48.jsx)(ShareActionButton, { target: shareTarget, url: shareUrl }) : void 0
2703
2715
  }
2704
2716
  ),
2705
- artist.image && /* @__PURE__ */ (0, import_jsx_runtime47.jsx)(ArtistPhoto, { image: artist.image, name: artist.name }),
2706
- /* @__PURE__ */ (0, import_jsx_runtime47.jsx)(ArtistHeader, { artist }),
2707
- /* @__PURE__ */ (0, import_jsx_runtime47.jsx)(ArtistMembers, { title: "Members", memberships: artist.members, renderArtistLink }),
2708
- /* @__PURE__ */ (0, import_jsx_runtime47.jsx)(ArtistMembers, { title: "Member of", memberships: artist.memberOf, renderArtistLink }),
2709
- artist.tracksCount > 0 && /* @__PURE__ */ (0, import_jsx_runtime47.jsx)(
2717
+ artist.image && /* @__PURE__ */ (0, import_jsx_runtime48.jsx)(ArtistPhoto, { image: artist.image, name: artist.name }),
2718
+ /* @__PURE__ */ (0, import_jsx_runtime48.jsx)(ArtistHeader, { artist }),
2719
+ /* @__PURE__ */ (0, import_jsx_runtime48.jsx)(ArtistMembers, { title: "Members", memberships: artist.members, renderArtistLink }),
2720
+ /* @__PURE__ */ (0, import_jsx_runtime48.jsx)(ArtistMembers, { title: "Member of", memberships: artist.memberOf, renderArtistLink }),
2721
+ artist.tracksCount > 0 && /* @__PURE__ */ (0, import_jsx_runtime48.jsx)(
2710
2722
  ArtistSongs,
2711
2723
  {
2712
2724
  artist: { uuid: artist.uuid, name: artist.name },
@@ -2716,7 +2728,7 @@ function useArtistPanel({
2716
2728
  }
2717
2729
  )
2718
2730
  ] }),
2719
- extraContent: artist.albums.length > 0 ? /* @__PURE__ */ (0, import_jsx_runtime47.jsx)(ArtistDiscography, { albums: artist.albums }) : void 0
2731
+ extraContent: artist.albums.length > 0 ? /* @__PURE__ */ (0, import_jsx_runtime48.jsx)(ArtistDiscography, { albums: artist.albums }) : void 0
2720
2732
  };
2721
2733
  }
2722
2734
  }
@@ -2728,11 +2740,11 @@ var import_genre_tree29 = require("@behindthemusictree/app-kit/genre-tree");
2728
2740
 
2729
2741
  // src/genre-tree/song/SongPanelContent.tsx
2730
2742
  var import_react18 = require("react");
2731
- var import_lucide_react17 = require("lucide-react");
2743
+ var import_lucide_react16 = require("lucide-react");
2732
2744
  var import_ui21 = require("@behindthemusictree/ui");
2733
2745
  var import_player12 = require("@behindthemusictree/app-kit/player");
2734
2746
  var import_genre_tree28 = require("@behindthemusictree/app-kit/genre-tree");
2735
- var import_jsx_runtime48 = require("react/jsx-runtime");
2747
+ var import_jsx_runtime49 = require("react/jsx-runtime");
2736
2748
  var numberFormat = new Intl.NumberFormat("en");
2737
2749
  var dateFormat = new Intl.DateTimeFormat("en", { dateStyle: "medium" });
2738
2750
  var externalLink = "text-[var(--color-primary)] hover:underline focus-visible:underline";
@@ -2740,27 +2752,27 @@ function SongHistory({ uuid, getBackendBaseUrl }) {
2740
2752
  const [open, setOpen] = (0, import_react18.useState)(false);
2741
2753
  const history = (0, import_genre_tree28.useSongHistory)(uuid, getBackendBaseUrl, open);
2742
2754
  let body = null;
2743
- if (history.status === "loading") body = /* @__PURE__ */ (0, import_jsx_runtime48.jsx)(import_ui21.Skeleton, { className: "h-10", "aria-busy": "true" });
2744
- else if (history.status === "error") body = /* @__PURE__ */ (0, import_jsx_runtime48.jsx)("p", { className: "text-red-600", children: "Could not load the history." });
2755
+ if (history.status === "loading") body = /* @__PURE__ */ (0, import_jsx_runtime49.jsx)(import_ui21.Skeleton, { className: "h-10", "aria-busy": "true" });
2756
+ else if (history.status === "error") body = /* @__PURE__ */ (0, import_jsx_runtime49.jsx)("p", { className: "text-red-600", children: "Could not load the history." });
2745
2757
  else if (history.status === "success")
2746
- body = history.entries.length === 0 ? /* @__PURE__ */ (0, import_jsx_runtime48.jsx)("p", { className: "text-[var(--color-text-muted)]", children: "No changes yet." }) : /* @__PURE__ */ (0, import_jsx_runtime48.jsx)("ul", { className: "divide-y divide-[var(--color-border)]", children: history.entries.map((entry) => /* @__PURE__ */ (0, import_jsx_runtime48.jsxs)("li", { className: "py-1.5 text-body", children: [
2747
- /* @__PURE__ */ (0, import_jsx_runtime48.jsx)("span", { className: "font-medium", children: entry.action }),
2748
- entry.oldValue !== null || entry.newValue !== null ? /* @__PURE__ */ (0, import_jsx_runtime48.jsxs)("span", { className: "text-[var(--color-text-muted)]", children: [
2758
+ body = history.entries.length === 0 ? /* @__PURE__ */ (0, import_jsx_runtime49.jsx)("p", { className: "text-[var(--color-text-muted)]", children: "No changes yet." }) : /* @__PURE__ */ (0, import_jsx_runtime49.jsx)("ul", { className: "divide-y divide-[var(--color-border)]", children: history.entries.map((entry) => /* @__PURE__ */ (0, import_jsx_runtime49.jsxs)("li", { className: "py-1.5 text-body", children: [
2759
+ /* @__PURE__ */ (0, import_jsx_runtime49.jsx)("span", { className: "font-medium", children: entry.action }),
2760
+ entry.oldValue !== null || entry.newValue !== null ? /* @__PURE__ */ (0, import_jsx_runtime49.jsxs)("span", { className: "text-[var(--color-text-muted)]", children: [
2749
2761
  " ",
2750
2762
  "\u2014 ",
2751
2763
  entry.oldValue ?? "none",
2752
2764
  " \u2192 ",
2753
2765
  entry.newValue ?? "none"
2754
2766
  ] }) : null,
2755
- /* @__PURE__ */ (0, import_jsx_runtime48.jsxs)("span", { className: "block text-caption text-[var(--color-text-muted)]", children: [
2767
+ /* @__PURE__ */ (0, import_jsx_runtime49.jsxs)("span", { className: "block text-caption text-[var(--color-text-muted)]", children: [
2756
2768
  entry.actorPseudo ?? "Anonymous",
2757
2769
  " \xB7 ",
2758
2770
  dateFormat.format(new Date(entry.createdOn))
2759
2771
  ] })
2760
2772
  ] }, entry.uuid)) });
2761
- return /* @__PURE__ */ (0, import_jsx_runtime48.jsx)(import_ui21.PanelBleed, { padded: true, className: "border-t border-[var(--color-border)]", children: /* @__PURE__ */ (0, import_jsx_runtime48.jsxs)("details", { onToggle: (event) => setOpen(event.currentTarget.open), className: "pb-2.5", children: [
2762
- /* @__PURE__ */ (0, import_jsx_runtime48.jsx)("summary", { className: `mt-2.5 cursor-pointer ${import_ui21.PANEL_LABEL_CLASS}`, children: "History" }),
2763
- /* @__PURE__ */ (0, import_jsx_runtime48.jsx)("div", { className: "mt-1.5", children: body })
2773
+ return /* @__PURE__ */ (0, import_jsx_runtime49.jsx)(import_ui21.PanelBleed, { padded: true, className: "border-t border-[var(--color-border)]", children: /* @__PURE__ */ (0, import_jsx_runtime49.jsxs)("details", { onToggle: (event) => setOpen(event.currentTarget.open), className: "pb-2.5", children: [
2774
+ /* @__PURE__ */ (0, import_jsx_runtime49.jsx)("summary", { className: `mt-2.5 cursor-pointer ${import_ui21.PANEL_LABEL_CLASS}`, children: "History" }),
2775
+ /* @__PURE__ */ (0, import_jsx_runtime49.jsx)("div", { className: "mt-1.5", children: body })
2764
2776
  ] }) });
2765
2777
  }
2766
2778
  function SongPanelContent(props) {
@@ -2779,13 +2791,13 @@ function SongPanelContent(props) {
2779
2791
  const genre = song.genre;
2780
2792
  fields.push({
2781
2793
  label: "Genre",
2782
- value: onSelectGenre ? /* @__PURE__ */ (0, import_jsx_runtime48.jsx)("button", { type: "button", onClick: () => onSelectGenre(genre), className: "hover:underline", children: genre.name }) : path ? /* @__PURE__ */ (0, import_jsx_runtime48.jsx)("span", { className: "flex flex-wrap items-center gap-x-1", children: path.map((genrePlaylist, index) => /* @__PURE__ */ (0, import_jsx_runtime48.jsxs)("span", { children: [
2783
- index > 0 && /* @__PURE__ */ (0, import_jsx_runtime48.jsx)("span", { "aria-hidden": "true", children: "\u203A " }),
2784
- /* @__PURE__ */ (0, import_jsx_runtime48.jsx)("button", { type: "button", onClick: () => props.onSelectGenrePlaylist?.(genrePlaylist), className: "hover:underline", children: genrePlaylist.name })
2794
+ value: onSelectGenre ? /* @__PURE__ */ (0, import_jsx_runtime49.jsx)("button", { type: "button", onClick: () => onSelectGenre(genre), className: "hover:underline", children: genre.name }) : path ? /* @__PURE__ */ (0, import_jsx_runtime49.jsx)("span", { className: "flex flex-wrap items-center gap-x-1", children: path.map((genrePlaylist, index) => /* @__PURE__ */ (0, import_jsx_runtime49.jsxs)("span", { children: [
2795
+ index > 0 && /* @__PURE__ */ (0, import_jsx_runtime49.jsx)("span", { "aria-hidden": "true", children: "\u203A " }),
2796
+ /* @__PURE__ */ (0, import_jsx_runtime49.jsx)("button", { type: "button", onClick: () => props.onSelectGenrePlaylist?.(genrePlaylist), className: "hover:underline", children: genrePlaylist.name })
2785
2797
  ] }, genrePlaylist.uuid)) }) : song.genre.name
2786
2798
  });
2787
2799
  } else {
2788
- fields.push({ label: "Genre", value: /* @__PURE__ */ (0, import_jsx_runtime48.jsx)("span", { className: "text-[var(--color-text-muted)]", children: "No genre" }) });
2800
+ fields.push({ label: "Genre", value: /* @__PURE__ */ (0, import_jsx_runtime49.jsx)("span", { className: "text-[var(--color-text-muted)]", children: "No genre" }) });
2789
2801
  }
2790
2802
  if (song.firstReleaseYear) fields.push({ label: "First released", value: song.firstReleaseYear });
2791
2803
  const viewCount = song.youtubeVideo?.viewCount;
@@ -2795,11 +2807,11 @@ function SongPanelContent(props) {
2795
2807
  const links = [];
2796
2808
  if (musicBrainzUrl)
2797
2809
  links.push(
2798
- /* @__PURE__ */ (0, import_jsx_runtime48.jsx)("a", { href: musicBrainzUrl, target: "_blank", rel: "noopener noreferrer", className: externalLink, children: "MusicBrainz" }, "mb")
2810
+ /* @__PURE__ */ (0, import_jsx_runtime49.jsx)("a", { href: musicBrainzUrl, target: "_blank", rel: "noopener noreferrer", className: externalLink, children: "MusicBrainz" }, "mb")
2799
2811
  );
2800
2812
  if (song.youtubeVideo)
2801
2813
  links.push(
2802
- /* @__PURE__ */ (0, import_jsx_runtime48.jsx)(
2814
+ /* @__PURE__ */ (0, import_jsx_runtime49.jsx)(
2803
2815
  "a",
2804
2816
  {
2805
2817
  href: `https://www.youtube.com/watch?v=${encodeURIComponent(song.youtubeVideo.id)}`,
@@ -2814,31 +2826,31 @@ function SongPanelContent(props) {
2814
2826
  if (links.length > 0)
2815
2827
  fields.push({
2816
2828
  label: "Links",
2817
- value: /* @__PURE__ */ (0, import_jsx_runtime48.jsx)("span", { className: "flex gap-3", children: links })
2829
+ value: /* @__PURE__ */ (0, import_jsx_runtime49.jsx)("span", { className: "flex gap-3", children: links })
2818
2830
  });
2819
- return /* @__PURE__ */ (0, import_jsx_runtime48.jsxs)(import_jsx_runtime48.Fragment, { children: [
2820
- /* @__PURE__ */ (0, import_jsx_runtime48.jsx)(import_ui21.PanelBleed, { children: /* @__PURE__ */ (0, import_jsx_runtime48.jsxs)(import_ui21.ActionBand, { children: [
2821
- /* @__PURE__ */ (0, import_jsx_runtime48.jsx)(
2831
+ return /* @__PURE__ */ (0, import_jsx_runtime49.jsxs)(import_jsx_runtime49.Fragment, { children: [
2832
+ /* @__PURE__ */ (0, import_jsx_runtime49.jsx)(import_ui21.PanelBleed, { children: /* @__PURE__ */ (0, import_jsx_runtime49.jsxs)(import_ui21.ActionBand, { children: [
2833
+ /* @__PURE__ */ (0, import_jsx_runtime49.jsx)(
2822
2834
  import_ui21.ActionButton,
2823
2835
  {
2824
- icon: isPlaying ? /* @__PURE__ */ (0, import_jsx_runtime48.jsx)(import_lucide_react17.Pause, { "aria-hidden": "true", className: "w-5 h-5" }) : /* @__PURE__ */ (0, import_jsx_runtime48.jsx)(import_lucide_react17.Play, { "aria-hidden": "true", className: "w-5 h-5" }),
2836
+ icon: isPlaying ? /* @__PURE__ */ (0, import_jsx_runtime49.jsx)(import_lucide_react16.Pause, { "aria-hidden": "true", className: "w-5 h-5" }) : /* @__PURE__ */ (0, import_jsx_runtime49.jsx)(import_lucide_react16.Play, { "aria-hidden": "true", className: "w-5 h-5" }),
2825
2837
  label: isPlaying ? "Pause" : "Play",
2826
2838
  disabled: !!unplayableReason,
2827
2839
  title: unplayableReason ?? void 0,
2828
2840
  onClick: () => isCurrent ? handlePlayPauseAction() : playNewTrackListFromTrackUuid(song, scope)
2829
2841
  }
2830
2842
  ),
2831
- shareUrl && /* @__PURE__ */ (0, import_jsx_runtime48.jsx)(ShareActionButton, { target: shareTarget, url: shareUrl })
2843
+ shareUrl && /* @__PURE__ */ (0, import_jsx_runtime49.jsx)(ShareActionButton, { target: shareTarget, url: shareUrl })
2832
2844
  ] }) }),
2833
- song.artists && song.artists.length > 0 && /* @__PURE__ */ (0, import_jsx_runtime48.jsx)("p", { className: "text-[var(--color-text-muted)]", children: /* @__PURE__ */ (0, import_jsx_runtime48.jsx)(import_genre_tree28.ArtistNames, { artists: song.artists, renderArtistLink }) }),
2834
- /* @__PURE__ */ (0, import_jsx_runtime48.jsx)(import_ui21.FieldList, { items: fields }),
2835
- song.essentialGenres && song.essentialGenres.length > 0 && /* @__PURE__ */ (0, import_jsx_runtime48.jsx)(import_ui21.PanelSection, { title: "Essential in", children: /* @__PURE__ */ (0, import_jsx_runtime48.jsx)("ul", { children: song.essentialGenres.map((genre) => /* @__PURE__ */ (0, import_jsx_runtime48.jsx)("li", { className: "py-0.5", children: genre.name }, genre.uuid)) }) }),
2836
- /* @__PURE__ */ (0, import_jsx_runtime48.jsx)(SongHistory, { uuid: song.uuid, getBackendBaseUrl })
2845
+ song.artists && song.artists.length > 0 && /* @__PURE__ */ (0, import_jsx_runtime49.jsx)("p", { className: "text-[var(--color-text-muted)]", children: /* @__PURE__ */ (0, import_jsx_runtime49.jsx)(import_genre_tree28.ArtistNames, { artists: song.artists, renderArtistLink }) }),
2846
+ /* @__PURE__ */ (0, import_jsx_runtime49.jsx)(import_ui21.FieldList, { items: fields }),
2847
+ song.essentialGenres && song.essentialGenres.length > 0 && /* @__PURE__ */ (0, import_jsx_runtime49.jsx)(import_ui21.PanelSection, { title: "Essential in", children: /* @__PURE__ */ (0, import_jsx_runtime49.jsx)("ul", { children: song.essentialGenres.map((genre) => /* @__PURE__ */ (0, import_jsx_runtime49.jsx)("li", { className: "py-0.5", children: genre.name }, genre.uuid)) }) }),
2848
+ /* @__PURE__ */ (0, import_jsx_runtime49.jsx)(SongHistory, { uuid: song.uuid, getBackendBaseUrl })
2837
2849
  ] });
2838
2850
  }
2839
2851
 
2840
2852
  // src/genre-tree/song/useSongPanel.tsx
2841
- var import_jsx_runtime49 = require("react/jsx-runtime");
2853
+ var import_jsx_runtime50 = require("react/jsx-runtime");
2842
2854
  function useSongPanel({
2843
2855
  uuid,
2844
2856
  scope,
@@ -2859,24 +2871,24 @@ function useSongPanel({
2859
2871
  return {
2860
2872
  ...panel,
2861
2873
  title: "Song",
2862
- content: /* @__PURE__ */ (0, import_jsx_runtime49.jsxs)("div", { "aria-busy": "true", className: "flex flex-col gap-2.5", children: [
2863
- /* @__PURE__ */ (0, import_jsx_runtime49.jsx)(import_ui22.Skeleton, { className: "h-3 w-16" }),
2864
- /* @__PURE__ */ (0, import_jsx_runtime49.jsx)(import_ui22.Skeleton, { className: "h-4 w-32" }),
2865
- /* @__PURE__ */ (0, import_jsx_runtime49.jsx)(import_ui22.Skeleton, { className: "h-3 w-16" }),
2866
- /* @__PURE__ */ (0, import_jsx_runtime49.jsx)(import_ui22.Skeleton, { className: "h-4 w-24" })
2874
+ content: /* @__PURE__ */ (0, import_jsx_runtime50.jsxs)("div", { "aria-busy": "true", className: "flex flex-col gap-2.5", children: [
2875
+ /* @__PURE__ */ (0, import_jsx_runtime50.jsx)(import_ui22.Skeleton, { className: "h-3 w-16" }),
2876
+ /* @__PURE__ */ (0, import_jsx_runtime50.jsx)(import_ui22.Skeleton, { className: "h-4 w-32" }),
2877
+ /* @__PURE__ */ (0, import_jsx_runtime50.jsx)(import_ui22.Skeleton, { className: "h-3 w-16" }),
2878
+ /* @__PURE__ */ (0, import_jsx_runtime50.jsx)(import_ui22.Skeleton, { className: "h-4 w-24" })
2867
2879
  ] })
2868
2880
  };
2869
2881
  case "not-found":
2870
2882
  return {
2871
2883
  ...panel,
2872
2884
  title: "Song not found",
2873
- content: /* @__PURE__ */ (0, import_jsx_runtime49.jsx)("p", { className: "text-[var(--color-text-muted)]", children: "This song does not exist." })
2885
+ content: /* @__PURE__ */ (0, import_jsx_runtime50.jsx)("p", { className: "text-[var(--color-text-muted)]", children: "This song does not exist." })
2874
2886
  };
2875
2887
  case "error":
2876
2888
  return {
2877
2889
  ...panel,
2878
2890
  title: "Song",
2879
- content: /* @__PURE__ */ (0, import_jsx_runtime49.jsx)("p", { className: "text-red-600", children: "Could not load this song." })
2891
+ content: /* @__PURE__ */ (0, import_jsx_runtime50.jsx)("p", { className: "text-red-600", children: "Could not load this song." })
2880
2892
  };
2881
2893
  case "success": {
2882
2894
  const { song } = result;
@@ -2884,7 +2896,7 @@ function useSongPanel({
2884
2896
  return {
2885
2897
  ...panel,
2886
2898
  title: song.title,
2887
- content: /* @__PURE__ */ (0, import_jsx_runtime49.jsx)(
2899
+ content: /* @__PURE__ */ (0, import_jsx_runtime50.jsx)(
2888
2900
  SongPanelContent,
2889
2901
  {
2890
2902
  song,
@@ -2937,12 +2949,12 @@ function useEntityPanel({
2937
2949
  // src/genre-tree/SizeMetricToggle.tsx
2938
2950
  var import_md2 = require("react-icons/md");
2939
2951
  var import_ui23 = require("@behindthemusictree/ui");
2940
- var import_jsx_runtime50 = require("react/jsx-runtime");
2952
+ var import_jsx_runtime51 = require("react/jsx-runtime");
2941
2953
  function SizeMetricToggle({
2942
2954
  value,
2943
2955
  onChange
2944
2956
  }) {
2945
- return /* @__PURE__ */ (0, import_jsx_runtime50.jsx)(
2957
+ return /* @__PURE__ */ (0, import_jsx_runtime51.jsx)(
2946
2958
  import_ui23.SegmentedControl,
2947
2959
  {
2948
2960
  variant: "floating",
@@ -2950,15 +2962,15 @@ function SizeMetricToggle({
2950
2962
  value,
2951
2963
  onChange,
2952
2964
  options: [
2953
- { value: "songs", label: /* @__PURE__ */ (0, import_jsx_runtime50.jsx)(import_md2.MdMusicNote, { size: 18, "aria-hidden": true }), "aria-label": "Size by songs", title: "Node size by song count" },
2954
- { value: "views", label: /* @__PURE__ */ (0, import_jsx_runtime50.jsx)(import_md2.MdVisibility, { size: 18, "aria-hidden": true }), "aria-label": "Size by views", title: "Node size by song views" }
2965
+ { value: "songs", label: /* @__PURE__ */ (0, import_jsx_runtime51.jsx)(import_md2.MdMusicNote, { size: 18, "aria-hidden": true }), "aria-label": "Size by songs", title: "Node size by song count" },
2966
+ { value: "views", label: /* @__PURE__ */ (0, import_jsx_runtime51.jsx)(import_md2.MdVisibility, { size: 18, "aria-hidden": true }), "aria-label": "Size by views", title: "Node size by song views" }
2955
2967
  ]
2956
2968
  }
2957
2969
  );
2958
2970
  }
2959
2971
 
2960
2972
  // src/genre-tree/GenreTreeView.tsx
2961
- var import_jsx_runtime51 = require("react/jsx-runtime");
2973
+ var import_jsx_runtime52 = require("react/jsx-runtime");
2962
2974
  var NO_POP_ROOT_TITLE = "This genre tree has no 'Mainstream Pop' root yet";
2963
2975
  var REGIONS_CLASS = "[--gtv-center-left:var(--layout-center-left)] [--gtv-center-right:var(--layout-center-right)] [--gtv-center-top:var(--layout-band-top)]";
2964
2976
  var noop = () => {
@@ -3066,7 +3078,7 @@ function GenreTreeView({
3066
3078
  allowWheelRotation: internalAllowWheelRotation,
3067
3079
  toggleWheelRotation,
3068
3080
  onNodeHover: handleNodeHover
3069
- } = (0, import_genre_tree30.useGenreTreeViewState)({
3081
+ } = (0, import_genre_tree31.useGenreTreeViewState)({
3070
3082
  scope,
3071
3083
  getBackendBaseUrl,
3072
3084
  viewMode: controlledViewMode,
@@ -3085,12 +3097,12 @@ function GenreTreeView({
3085
3097
  restoreRootUuid = selectedGenrePlaylist.root.uuid;
3086
3098
  setStackedRestoreOwner({ revision: restoreRevision, rootUuid: restoreRootUuid });
3087
3099
  }
3088
- const isGenreUpdating = (0, import_react_query3.useIsMutating)({ mutationKey: import_genre_tree30.genreUpdateMutationKey }) > 0;
3089
- const { mutate: updateGenre } = (0, import_genre_tree30.useUpdateGenre)(scope, getBackendBaseUrl);
3100
+ const isGenreUpdating = (0, import_react_query3.useIsMutating)({ mutationKey: import_genre_tree31.genreUpdateMutationKey }) > 0;
3101
+ const { mutate: updateGenre } = (0, import_genre_tree31.useUpdateGenre)(scope, getBackendBaseUrl);
3090
3102
  const busy = deferredSizeMetric !== sizeMetric || isTreeFetching || isGenreUpdating;
3091
3103
  const isArtistFiltered = artistUuid !== null;
3092
3104
  const canSizeByViews = !isArtistFiltered && hasViewsCount(visibleGenrePlaylists);
3093
- const artistResult = (0, import_genre_tree30.useArtist)(artistUuid, artistSearchBaseUrl ?? getBackendBaseUrl);
3105
+ const artistResult = (0, import_genre_tree31.useArtist)(artistUuid, artistSearchBaseUrl ?? getBackendBaseUrl);
3094
3106
  const filterArtistName = artistResult.status === "success" ? artistResult.artist.name : null;
3095
3107
  const filterArtist = (0, import_react19.useMemo)(
3096
3108
  () => artistUuid !== null && filterArtistName !== null ? { uuid: artistUuid, name: filterArtistName } : null,
@@ -3110,14 +3122,14 @@ function GenreTreeView({
3110
3122
  const openSongRef = (0, import_react19.useRef)(openSong);
3111
3123
  openSongRef.current = openSong;
3112
3124
  const resolvedRenderSongLink = (0, import_react19.useCallback)(
3113
- (song) => renderSongLink ? renderSongLink(song) : /* @__PURE__ */ (0, import_jsx_runtime51.jsx)("button", { type: "button", onClick: () => openSongRef.current(song.uuid), className: "hover:underline", children: song.title }),
3125
+ (song) => renderSongLink ? renderSongLink(song) : /* @__PURE__ */ (0, import_jsx_runtime52.jsx)("button", { type: "button", onClick: () => openSongRef.current(song.uuid), className: "hover:underline", children: song.title }),
3114
3126
  [renderSongLink]
3115
3127
  );
3116
3128
  const [expandedSongUuid, setExpandedSongUuid] = (0, import_react19.useState)(null);
3117
3129
  const [expandedArtistUuid, setExpandedArtistUuid] = (0, import_react19.useState)(null);
3118
3130
  const artistBaseUrl = artistSearchBaseUrl ?? getBackendBaseUrl;
3119
3131
  const songBaseUrl = getBackendBaseUrl;
3120
- const resolvedRenderArtistLink = renderArtistLink ?? ((artist) => artist.isSpecialPurpose ? artist.name : /* @__PURE__ */ (0, import_jsx_runtime51.jsx)("button", { type: "button", onClick: () => openArtist(artist.uuid), className: "hover:underline", children: artist.name }));
3132
+ const resolvedRenderArtistLink = renderArtistLink ?? ((artist) => artist.isSpecialPurpose ? artist.name : /* @__PURE__ */ (0, import_jsx_runtime52.jsx)("button", { type: "button", onClick: () => openArtist(artist.uuid), className: "hover:underline", children: artist.name }));
3121
3133
  const getGenrePath = (genre) => {
3122
3134
  const path = [];
3123
3135
  let current = visibleGenrePlaylists.find((playlist) => playlist.criteria?.uuid === genre.uuid);
@@ -3193,9 +3205,11 @@ function GenreTreeView({
3193
3205
  !overlayPanel && hasSelectedGenreNode ? selectedNodeId : null,
3194
3206
  resolveStyle
3195
3207
  );
3196
- const [selectionHistory, setSelectionHistory] = (0, import_react19.useState)({ current: selectedGenreUuid, previous: null });
3197
- if (selectionHistory.current !== selectedGenreUuid) {
3198
- setSelectionHistory({ current: selectedGenreUuid, previous: selectionHistory.current ?? selectionHistory.previous });
3208
+ const treeStyles = (0, import_react19.useMemo)(() => toGenreNodeStyles(relations, visibleGenrePlaylists), [relations, visibleGenrePlaylists]);
3209
+ const validSelectedGenreUuid = (0, import_genre_tree30.toGenreUuid)(selectedGenreUuid);
3210
+ const [selectionHistory, setSelectionHistory] = (0, import_react19.useState)({ current: validSelectedGenreUuid, previous: null });
3211
+ if (selectionHistory.current !== validSelectedGenreUuid) {
3212
+ setSelectionHistory({ current: validSelectedGenreUuid, previous: selectionHistory.current ?? selectionHistory.previous });
3199
3213
  }
3200
3214
  const handlePrevious = selectionHistory.previous ? () => selectGenre(selectionHistory.previous) : void 0;
3201
3215
  const { showPopup, hidePopup } = (0, import_popup5.usePopup)();
@@ -3227,7 +3241,7 @@ function GenreTreeView({
3227
3241
  const canToggleSide = !readOnly && criteria !== void 0 && criteria !== null && parentPlaylist !== void 0 && parentPlaylist.parent === null;
3228
3242
  const side = criteria?.side ?? "core";
3229
3243
  const popSibling = canToggleSide && side === "core" ? visibleGenrePlaylists.find((sibling) => sibling.parent?.uuid === parentPlaylist.uuid && sibling.uuid !== genrePlaylist?.uuid && sibling.criteria?.side === "pop") : void 0;
3230
- const shareTarget = selectedGenreUuid ? { kind: "genre", genreUuid: selectedGenreUuid, name: criteria?.name ?? "Genre", scope, artistUuid } : void 0;
3244
+ const shareTarget = validSelectedGenreUuid ? { kind: "genre", genreUuid: validSelectedGenreUuid, name: criteria?.name ?? "Genre", scope, artistUuid } : void 0;
3231
3245
  const shareUrl = shareTarget ? getShareUrlRef.current?.(shareTarget) ?? void 0 : void 0;
3232
3246
  const genreContent = useGenreDetailContent({
3233
3247
  genreUuid: selectedGenreUuid,
@@ -3238,7 +3252,7 @@ function GenreTreeView({
3238
3252
  selectGenre(uuid);
3239
3253
  },
3240
3254
  expanded: isExpanded,
3241
- treeRelations: relations,
3255
+ treeStyles,
3242
3256
  playback,
3243
3257
  genrePlaylist: selectedGenrePlaylist,
3244
3258
  band: {
@@ -3249,7 +3263,7 @@ function GenreTreeView({
3249
3263
  onAddChild: readOnly || !criteria ? void 0 : () => handleGenreCreationAction(criteria),
3250
3264
  onRename: readOnly || !criteria ? void 0 : () => handleGenreRenameAction(criteria),
3251
3265
  onChangeParent: readOnly || !genrePlaylist || !criteria ? void 0 : () => showPopup(
3252
- /* @__PURE__ */ (0, import_jsx_runtime51.jsx)(
3266
+ /* @__PURE__ */ (0, import_jsx_runtime52.jsx)(
3253
3267
  ChangeParentPopup,
3254
3268
  {
3255
3269
  genrePlaylist: { ...genrePlaylist, criteria },
@@ -3293,7 +3307,7 @@ function GenreTreeView({
3293
3307
  onClose: otherPanel.onClose,
3294
3308
  searchName: otherPanel.title
3295
3309
  };
3296
- } else if (selectedGenreUuid) {
3310
+ } else if (selectedGenreUuid !== null) {
3297
3311
  activePanel = {
3298
3312
  title: genreContent.title,
3299
3313
  content: genreContent.content,
@@ -3310,19 +3324,19 @@ function GenreTreeView({
3310
3324
  edge: activePanel.expanded ? { label: "Shrink panel", onClick: () => activePanel.onExpandedChange?.(false) } : isCollapsed ? null : { label: "Hide panel", onClick: () => changeCollapsed(true) },
3311
3325
  center: activePanel.expanded ? null : isCollapsed ? { label: "Show panel", onClick: () => changeCollapsed(false) } : activePanel.onExpandedChange ? { label: "Extend panel", onClick: () => activePanel.onExpandedChange?.(true) } : null
3312
3326
  } : void 0;
3313
- const expandButton = placement === "default" && activePanel?.onExpandedChange ? /* @__PURE__ */ (0, import_jsx_runtime51.jsx)(
3327
+ const expandButton = placement === "default" && activePanel?.onExpandedChange ? /* @__PURE__ */ (0, import_jsx_runtime52.jsx)(
3314
3328
  import_ui24.IconButton,
3315
3329
  {
3316
3330
  "aria-label": activePanel.expanded ? "Shrink panel" : "Extend panel",
3317
3331
  style: activePanel.headerAccent && { color: "inherit" },
3318
3332
  onClick: () => activePanel.onExpandedChange?.(!activePanel.expanded),
3319
- children: activePanel.expanded ? /* @__PURE__ */ (0, import_jsx_runtime51.jsx)(import_lucide_react19.Minimize2, { "aria-hidden": "true", className: "h-4 w-4" }) : /* @__PURE__ */ (0, import_jsx_runtime51.jsx)(import_lucide_react19.Maximize2, { "aria-hidden": "true", className: "h-4 w-4" })
3333
+ children: activePanel.expanded ? /* @__PURE__ */ (0, import_jsx_runtime52.jsx)(import_lucide_react18.Minimize2, { "aria-hidden": "true", className: "h-4 w-4" }) : /* @__PURE__ */ (0, import_jsx_runtime52.jsx)(import_lucide_react18.Maximize2, { "aria-hidden": "true", className: "h-4 w-4" })
3320
3334
  }
3321
3335
  ) : null;
3322
3336
  const renderArtistFilter = () => {
3323
3337
  if (isLoading || !filterArtist) return null;
3324
- return /* @__PURE__ */ (0, import_jsx_runtime51.jsxs)("span", { className: "inline-flex items-center overflow-hidden rounded-full bg-[var(--color-neutral-100)] text-body", children: [
3325
- /* @__PURE__ */ (0, import_jsx_runtime51.jsx)(
3338
+ return /* @__PURE__ */ (0, import_jsx_runtime52.jsxs)("span", { className: "inline-flex items-center overflow-hidden rounded-full bg-[var(--color-neutral-100)] text-body", children: [
3339
+ /* @__PURE__ */ (0, import_jsx_runtime52.jsx)(
3326
3340
  "button",
3327
3341
  {
3328
3342
  type: "button",
@@ -3334,7 +3348,7 @@ function GenreTreeView({
3334
3348
  children: filterArtist.name
3335
3349
  }
3336
3350
  ),
3337
- /* @__PURE__ */ (0, import_jsx_runtime51.jsx)(
3351
+ /* @__PURE__ */ (0, import_jsx_runtime52.jsx)(
3338
3352
  "button",
3339
3353
  {
3340
3354
  type: "button",
@@ -3346,8 +3360,8 @@ function GenreTreeView({
3346
3360
  )
3347
3361
  ] });
3348
3362
  };
3349
- const actions = /* @__PURE__ */ (0, import_jsx_runtime51.jsxs)(import_jsx_runtime51.Fragment, { children: [
3350
- !isLoading && !isError && /* @__PURE__ */ (0, import_jsx_runtime51.jsx)(
3363
+ const actions = /* @__PURE__ */ (0, import_jsx_runtime52.jsxs)(import_jsx_runtime52.Fragment, { children: [
3364
+ !isLoading && !isError && /* @__PURE__ */ (0, import_jsx_runtime52.jsx)(
3351
3365
  GenreTreeSearch,
3352
3366
  {
3353
3367
  artistBaseUrl,
@@ -3380,9 +3394,9 @@ function GenreTreeView({
3380
3394
  extraGroups: extraSearchGroups
3381
3395
  }
3382
3396
  ),
3383
- !isLoading && !isError && filterArtist && /* @__PURE__ */ (0, import_jsx_runtime51.jsx)("div", { className: "hidden w-full justify-center max-md:flex", children: renderArtistFilter() }),
3384
- /* @__PURE__ */ (0, import_jsx_runtime51.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: [
3385
- !isLoading && !isError && !isControlled && /* @__PURE__ */ (0, import_jsx_runtime51.jsx)(
3397
+ !isLoading && !isError && filterArtist && /* @__PURE__ */ (0, import_jsx_runtime52.jsx)("div", { className: "hidden w-full justify-center max-md:flex", children: renderArtistFilter() }),
3398
+ /* @__PURE__ */ (0, import_jsx_runtime52.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: [
3399
+ !isLoading && !isError && !isControlled && /* @__PURE__ */ (0, import_jsx_runtime52.jsx)(
3386
3400
  import_ui24.SegmentedControl,
3387
3401
  {
3388
3402
  "aria-label": "Tree view mode",
@@ -3396,13 +3410,13 @@ function GenreTreeView({
3396
3410
  ]
3397
3411
  }
3398
3412
  ),
3399
- !isLoading && !isError && showDisplayOptions && /* @__PURE__ */ (0, import_jsx_runtime51.jsx)(
3413
+ !isLoading && !isError && showDisplayOptions && /* @__PURE__ */ (0, import_jsx_runtime52.jsx)(
3400
3414
  "div",
3401
3415
  {
3402
3416
  className: "flex items-center gap-1",
3403
3417
  role: "group",
3404
3418
  "aria-label": "Tree display options",
3405
- children: !isRotationControlled && viewMode !== "stacked" && viewMode !== "outline" && /* @__PURE__ */ (0, import_jsx_runtime51.jsx)(
3419
+ children: !isRotationControlled && viewMode !== "stacked" && viewMode !== "outline" && /* @__PURE__ */ (0, import_jsx_runtime52.jsx)(
3406
3420
  import_ui24.Button,
3407
3421
  {
3408
3422
  variant: allowWheelRotation ? "default" : "outline",
@@ -3413,48 +3427,48 @@ function GenreTreeView({
3413
3427
  )
3414
3428
  }
3415
3429
  ),
3416
- !isLoading && !isError && !readOnly && showAddRoot && /* @__PURE__ */ (0, import_jsx_runtime51.jsx)(
3430
+ !isLoading && !isError && !readOnly && showAddRoot && /* @__PURE__ */ (0, import_jsx_runtime52.jsx)(
3417
3431
  import_ui24.IconTextButton,
3418
3432
  {
3419
- icon: import_lucide_react18.Plus,
3433
+ icon: import_lucide_react17.Plus,
3420
3434
  text: "Add root",
3421
3435
  onClick: () => handleGenreCreationAction(null)
3422
3436
  }
3423
3437
  )
3424
3438
  ] })
3425
3439
  ] });
3426
- return /* @__PURE__ */ (0, import_jsx_runtime51.jsxs)(
3440
+ return /* @__PURE__ */ (0, import_jsx_runtime52.jsxs)(
3427
3441
  "div",
3428
3442
  {
3429
3443
  "data-left-panel": isPanelShown && !isCollapsed ? void 0 : "hidden",
3430
3444
  className: ["genre-tree-view relative flex flex-col h-full", placement === "regions" && REGIONS_CLASS, className2].filter(Boolean).join(" "),
3431
3445
  children: [
3432
- /* @__PURE__ */ (0, import_jsx_runtime51.jsx)(
3446
+ /* @__PURE__ */ (0, import_jsx_runtime52.jsx)(
3433
3447
  "div",
3434
3448
  {
3435
3449
  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))] md:left-[var(--layout-panel-inset)] md:top-[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"}`,
3436
3450
  children: actions
3437
3451
  }
3438
3452
  ),
3439
- !isLoading && !isError && filterArtist && /* @__PURE__ */ (0, import_jsx_runtime51.jsx)(
3453
+ !isLoading && !isError && filterArtist && /* @__PURE__ */ (0, import_jsx_runtime52.jsx)(
3440
3454
  "div",
3441
3455
  {
3442
3456
  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))] md:top-[var(--layout-panel-inset)]" : "top-5"}`,
3443
3457
  children: renderArtistFilter()
3444
3458
  }
3445
3459
  ),
3446
- /* @__PURE__ */ (0, import_jsx_runtime51.jsx)("div", { className: "content-container flex-1 min-h-0 flex flex-row gap-4", children: /* @__PURE__ */ (0, import_jsx_runtime51.jsxs)("div", { className: "tree-view-container relative flex-1 min-w-0 flex flex-col h-full", children: [
3447
- !isLoading && !isError && viewMode === "outline" && floatingControlsLeading && /* @__PURE__ */ (0, import_jsx_runtime51.jsx)("div", { className: "absolute bottom-3 right-3 z-10", children: floatingControlsLeading }),
3448
- isLoading ? /* @__PURE__ */ (0, import_jsx_runtime51.jsx)(import_genre_tree_view5.GenreTreeViewSkeleton, { viewMode }) : isError ? /* @__PURE__ */ (0, import_jsx_runtime51.jsxs)("p", { role: "alert", className: "m-auto text-center", children: [
3460
+ /* @__PURE__ */ (0, import_jsx_runtime52.jsx)("div", { className: "content-container flex-1 min-h-0 flex flex-row gap-4", children: /* @__PURE__ */ (0, import_jsx_runtime52.jsxs)("div", { className: "tree-view-container relative flex-1 min-w-0 flex flex-col h-full", children: [
3461
+ !isLoading && !isError && viewMode === "outline" && floatingControlsLeading && /* @__PURE__ */ (0, import_jsx_runtime52.jsx)("div", { className: "absolute bottom-3 right-3 z-10", children: floatingControlsLeading }),
3462
+ isLoading ? /* @__PURE__ */ (0, import_jsx_runtime52.jsx)(import_genre_tree_view5.GenreTreeViewSkeleton, { viewMode }) : isError ? /* @__PURE__ */ (0, import_jsx_runtime52.jsxs)("p", { role: "alert", className: "m-auto text-center", children: [
3449
3463
  "Couldn't load the genre tree.",
3450
3464
  " ",
3451
- /* @__PURE__ */ (0, import_jsx_runtime51.jsx)("button", { type: "button", className: "underline", onClick: refetch, children: "Retry" })
3465
+ /* @__PURE__ */ (0, import_jsx_runtime52.jsx)("button", { type: "button", className: "underline", onClick: refetch, children: "Retry" })
3452
3466
  ] }) : viewMode === "wheel" ? (
3453
3467
  // The info panel's top edge aligns with the tree's; the search bar floats `--layout-panel-inset` below it,
3454
3468
  // and the header inset (the bar's height plus that inset) extends the header's fill behind the bar,
3455
3469
  // Google Maps style; the 8px bottom padding mirrors the inset above the bar, and the hidden title's height is zeroed.
3456
3470
  // The panel's own Zoom in button is hidden: the search bar carries it.
3457
- /* @__PURE__ */ (0, import_jsx_runtime51.jsx)("div", { className: "tree-container search-overlay relative flex-1 w-full min-h-0 ", children: /* @__PURE__ */ (0, import_jsx_runtime51.jsx)(GenreTreeWheelHandoff, { skeleton: /* @__PURE__ */ (0, import_jsx_runtime51.jsx)(import_genre_tree_view5.GenreTreeWheelSkeleton, {}), onRevealedChange: setIsWheelRevealed, children: /* @__PURE__ */ (0, import_jsx_runtime51.jsx)(
3471
+ /* @__PURE__ */ (0, import_jsx_runtime52.jsx)("div", { className: "tree-container search-overlay relative flex-1 w-full min-h-0 ", children: /* @__PURE__ */ (0, import_jsx_runtime52.jsx)(GenreTreeWheelHandoff, { skeleton: /* @__PURE__ */ (0, import_jsx_runtime52.jsx)(import_genre_tree_view5.GenreTreeWheelSkeleton, {}), onRevealedChange: setIsWheelRevealed, children: /* @__PURE__ */ (0, import_jsx_runtime52.jsx)(
3458
3472
  GenrePlaylistTreeWheel,
3459
3473
  {
3460
3474
  scope,
@@ -3472,9 +3486,9 @@ function GenreTreeView({
3472
3486
  allowWheelRotation,
3473
3487
  sizeMetric: isArtistFiltered || !canSizeByViews ? "songs" : deferredSizeMetric,
3474
3488
  busy,
3475
- floatingControlsLeading: floatingControlsLeading || canSizeByViews ? /* @__PURE__ */ (0, import_jsx_runtime51.jsxs)("div", { className: "flex items-center gap-2", children: [
3489
+ floatingControlsLeading: floatingControlsLeading || canSizeByViews ? /* @__PURE__ */ (0, import_jsx_runtime52.jsxs)("div", { className: "flex items-center gap-2", children: [
3476
3490
  floatingControlsLeading,
3477
- canSizeByViews && /* @__PURE__ */ (0, import_jsx_runtime51.jsx)(SizeMetricToggle, { value: sizeMetric, onChange: changeSizeMetric })
3491
+ canSizeByViews && /* @__PURE__ */ (0, import_jsx_runtime52.jsx)(SizeMetricToggle, { value: sizeMetric, onChange: changeSizeMetric })
3478
3492
  ] }) : void 0,
3479
3493
  defaultViewport,
3480
3494
  viewportRestore,
@@ -3483,7 +3497,7 @@ function GenreTreeView({
3483
3497
  ) }) })
3484
3498
  ) : viewMode === "pop-core" ? (
3485
3499
  // Same as the wheel: the header inset extends its fill behind the floating search bar, Google Maps style.
3486
- /* @__PURE__ */ (0, import_jsx_runtime51.jsx)("div", { className: "tree-container search-overlay relative flex-1 w-full min-h-0 ", children: /* @__PURE__ */ (0, import_jsx_runtime51.jsx)(GenreTreeWheelHandoff, { skeleton: /* @__PURE__ */ (0, import_jsx_runtime51.jsx)(import_genre_tree_view5.GenreTreeWheelSkeleton, {}), onRevealedChange: setIsWheelRevealed, children: /* @__PURE__ */ (0, import_jsx_runtime51.jsx)(
3500
+ /* @__PURE__ */ (0, import_jsx_runtime52.jsx)("div", { className: "tree-container search-overlay relative flex-1 w-full min-h-0 ", children: /* @__PURE__ */ (0, import_jsx_runtime52.jsx)(GenreTreeWheelHandoff, { skeleton: /* @__PURE__ */ (0, import_jsx_runtime52.jsx)(import_genre_tree_view5.GenreTreeWheelSkeleton, {}), onRevealedChange: setIsWheelRevealed, children: /* @__PURE__ */ (0, import_jsx_runtime52.jsx)(
3487
3501
  GenrePlaylistTreeWheelRadialPopCore,
3488
3502
  {
3489
3503
  scope,
@@ -3501,9 +3515,9 @@ function GenreTreeView({
3501
3515
  allowWheelRotation,
3502
3516
  sizeMetric: isArtistFiltered || !canSizeByViews ? "songs" : deferredSizeMetric,
3503
3517
  busy,
3504
- floatingControlsLeading: floatingControlsLeading || canSizeByViews ? /* @__PURE__ */ (0, import_jsx_runtime51.jsxs)("div", { className: "flex items-center gap-2", children: [
3518
+ floatingControlsLeading: floatingControlsLeading || canSizeByViews ? /* @__PURE__ */ (0, import_jsx_runtime52.jsxs)("div", { className: "flex items-center gap-2", children: [
3505
3519
  floatingControlsLeading,
3506
- canSizeByViews && /* @__PURE__ */ (0, import_jsx_runtime51.jsx)(SizeMetricToggle, { value: sizeMetric, onChange: changeSizeMetric })
3520
+ canSizeByViews && /* @__PURE__ */ (0, import_jsx_runtime52.jsx)(SizeMetricToggle, { value: sizeMetric, onChange: changeSizeMetric })
3507
3521
  ] }) : void 0,
3508
3522
  defaultViewport,
3509
3523
  viewportRestore,
@@ -3511,7 +3525,7 @@ function GenreTreeView({
3511
3525
  ...focus
3512
3526
  }
3513
3527
  ) }) })
3514
- ) : viewMode === "outline" ? /* @__PURE__ */ (0, import_jsx_runtime51.jsx)("div", { className: "tree-container flex-1 min-h-0 w-full overflow-y-auto relative", children: /* @__PURE__ */ (0, import_jsx_runtime51.jsx)(
3528
+ ) : viewMode === "outline" ? /* @__PURE__ */ (0, import_jsx_runtime52.jsx)("div", { className: "tree-container flex-1 min-h-0 w-full overflow-y-auto relative", children: /* @__PURE__ */ (0, import_jsx_runtime52.jsx)(
3515
3529
  GenrePlaylistTreeWheelRadialPopCore,
3516
3530
  {
3517
3531
  outline: true,
@@ -3528,13 +3542,13 @@ function GenreTreeView({
3528
3542
  selectedNodeId,
3529
3543
  readOnly
3530
3544
  }
3531
- ) }) : /* @__PURE__ */ (0, import_jsx_runtime51.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(
3545
+ ) }) : /* @__PURE__ */ (0, import_jsx_runtime52.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(
3532
3546
  ([uuid, genrePlaylistTreePerRoot]) => {
3533
- return /* @__PURE__ */ (0, import_jsx_runtime51.jsx)(
3547
+ return /* @__PURE__ */ (0, import_jsx_runtime52.jsx)(
3534
3548
  "div",
3535
3549
  {
3536
3550
  className: "tree-per-root-container relative shrink-0 h-[28rem] mt-2 mr-16 p-2 bg-[var(--color-neutral-100)] rounded-lg",
3537
- children: /* @__PURE__ */ (0, import_jsx_runtime51.jsx)("div", { className: "graph-container relative z-10 h-full", children: /* @__PURE__ */ (0, import_jsx_runtime51.jsx)(
3551
+ children: /* @__PURE__ */ (0, import_jsx_runtime52.jsx)("div", { className: "graph-container relative z-10 h-full", children: /* @__PURE__ */ (0, import_jsx_runtime52.jsx)(
3538
3552
  GenrePlaylistTreePerRoot,
3539
3553
  {
3540
3554
  scope,
@@ -3561,7 +3575,7 @@ function GenreTreeView({
3561
3575
  );
3562
3576
  }
3563
3577
  ) }),
3564
- /* @__PURE__ */ (0, import_jsx_runtime51.jsx)(
3578
+ /* @__PURE__ */ (0, import_jsx_runtime52.jsx)(
3565
3579
  import_ui24.DetailPanel,
3566
3580
  {
3567
3581
  ref: panelRef,
@@ -3576,7 +3590,7 @@ function GenreTreeView({
3576
3590
  onExpandedChange: activePanel?.onExpandedChange,
3577
3591
  aside: activePanel?.aside,
3578
3592
  edgeTab: detailTab,
3579
- children: activePanel?.aside && !activePanel.expanded ? /* @__PURE__ */ (0, import_jsx_runtime51.jsxs)(import_jsx_runtime51.Fragment, { children: [
3593
+ children: activePanel?.aside && !activePanel.expanded ? /* @__PURE__ */ (0, import_jsx_runtime52.jsxs)(import_jsx_runtime52.Fragment, { children: [
3580
3594
  activePanel.content,
3581
3595
  activePanel.aside
3582
3596
  ] }) : activePanel?.content
@@ -3593,11 +3607,11 @@ var import_genre_tree_view6 = require("@behindthemusictree/genre-tree-view");
3593
3607
 
3594
3608
  // src/genre-tree/track-list-sidebar/TrackListSidebar.tsx
3595
3609
  var import_react20 = require("react");
3596
- var import_lucide_react20 = require("lucide-react");
3597
- var import_genre_tree31 = require("@behindthemusictree/app-kit/genre-tree");
3610
+ var import_lucide_react19 = require("lucide-react");
3598
3611
  var import_genre_tree32 = require("@behindthemusictree/app-kit/genre-tree");
3612
+ var import_genre_tree33 = require("@behindthemusictree/app-kit/genre-tree");
3599
3613
  var import_track_noun4 = require("@behindthemusictree/app-kit/track-noun");
3600
- var import_jsx_runtime52 = require("react/jsx-runtime");
3614
+ var import_jsx_runtime53 = require("react/jsx-runtime");
3601
3615
  var useBrowserLayoutEffect = typeof window === "undefined" ? import_react20.useEffect : import_react20.useLayoutEffect;
3602
3616
  function TrackListSidebar({
3603
3617
  className: className2 = "",
@@ -3610,7 +3624,7 @@ function TrackListSidebar({
3610
3624
  renderArtistLink,
3611
3625
  renderSongLink
3612
3626
  }) {
3613
- const { trackList, loadMore, autoAdvanceScrollRequest } = (0, import_genre_tree31.useTrackList)();
3627
+ const { trackList, loadMore, autoAdvanceScrollRequest } = (0, import_genre_tree32.useTrackList)();
3614
3628
  const trackNoun = (0, import_track_noun4.useTrackNoun)();
3615
3629
  const trackListRef = (0, import_react20.useRef)(null);
3616
3630
  const previousOriginRef = (0, import_react20.useRef)(trackList?.origin ?? null);
@@ -3699,25 +3713,25 @@ function TrackListSidebar({
3699
3713
  }
3700
3714
  };
3701
3715
  const positionClasses = layout === "inline" ? "relative w-full flex h-full flex-col" : "fixed right-0 w-[50rem] flex flex-col";
3702
- return trackList ? /* @__PURE__ */ (0, import_jsx_runtime52.jsxs)(
3716
+ return trackList ? /* @__PURE__ */ (0, import_jsx_runtime53.jsxs)(
3703
3717
  "div",
3704
3718
  {
3705
3719
  "data-theme": "dark",
3706
3720
  className: `track-list-sidebar ${positionClasses} rounded-2xl bg-[var(--color-surface)] pb-1 ${className2}`,
3707
3721
  style: layout === "fixed" ? { top: "63.5px", maxHeight: "calc(100vh - 63.5px - 79px)" } : void 0,
3708
3722
  children: [
3709
- /* @__PURE__ */ (0, import_jsx_runtime52.jsxs)("div", { className: "header flex h-16 items-center px-4 py-2 text-[var(--color-text)]", children: [
3710
- /* @__PURE__ */ (0, import_jsx_runtime52.jsxs)("div", { className: "min-w-0 flex flex-1 flex-col justify-center", children: [
3711
- /* @__PURE__ */ (0, import_jsx_runtime52.jsxs)("div", { className: "origin flex text-xl", children: [
3712
- /* @__PURE__ */ (0, import_jsx_runtime52.jsx)("div", { className: "from h-auto flex flex-col justify-center items-center mr-2", children: "From" }),
3713
- /* @__PURE__ */ (0, import_jsx_runtime52.jsx)("div", { className: "name-container flex flex-col justify-center items-center text-[var(--color-text)] font-bold pr-2 max-w-[27.5rem]", children: /* @__PURE__ */ (0, import_jsx_runtime52.jsx)("div", { className: "name max-w-full truncate", children: trackList.origin.label }) })
3723
+ /* @__PURE__ */ (0, import_jsx_runtime53.jsxs)("div", { className: "header flex h-16 items-center px-4 py-2 text-[var(--color-text)]", children: [
3724
+ /* @__PURE__ */ (0, import_jsx_runtime53.jsxs)("div", { className: "min-w-0 flex flex-1 flex-col justify-center", children: [
3725
+ /* @__PURE__ */ (0, import_jsx_runtime53.jsxs)("div", { className: "origin flex text-xl", children: [
3726
+ /* @__PURE__ */ (0, import_jsx_runtime53.jsx)("div", { className: "from h-auto flex flex-col justify-center items-center mr-2", children: "From" }),
3727
+ /* @__PURE__ */ (0, import_jsx_runtime53.jsx)("div", { className: "name-container flex flex-col justify-center items-center text-[var(--color-text)] font-bold pr-2 max-w-[27.5rem]", children: /* @__PURE__ */ (0, import_jsx_runtime53.jsx)("div", { className: "name max-w-full truncate", children: trackList.origin.label }) })
3714
3728
  ] }),
3715
- /* @__PURE__ */ (0, import_jsx_runtime52.jsxs)("div", { className: "info flex items-center text-body text-[var(--color-text-muted)]", children: [
3716
- trackList.origin.type === import_genre_tree32.TrackListOriginType.GENRE_PLAYLIST ? "Genre \u2022 " : trackList.origin.type === import_genre_tree32.TrackListOriginType.ARTIST ? "Artist \u2022 " : `${trackNoun.singular} playlist \u2022 `,
3729
+ /* @__PURE__ */ (0, import_jsx_runtime53.jsxs)("div", { className: "info flex items-center text-body text-[var(--color-text-muted)]", children: [
3730
+ trackList.origin.type === import_genre_tree33.TrackListOriginType.GENRE_PLAYLIST ? "Genre \u2022 " : trackList.origin.type === import_genre_tree33.TrackListOriginType.ARTIST ? "Artist \u2022 " : `${trackNoun.singular} playlist \u2022 `,
3717
3731
  `${trackList.total} ${trackList.total > 1 ? trackNoun.plural : trackNoun.singular}`
3718
3732
  ] })
3719
3733
  ] }),
3720
- (isCollapsed && onExpand || !isCollapsed && onMinimize) && /* @__PURE__ */ (0, import_jsx_runtime52.jsx)(
3734
+ (isCollapsed && onExpand || !isCollapsed && onMinimize) && /* @__PURE__ */ (0, import_jsx_runtime53.jsx)(
3721
3735
  "button",
3722
3736
  {
3723
3737
  type: "button",
@@ -3726,11 +3740,11 @@ function TrackListSidebar({
3726
3740
  "aria-expanded": !isCollapsed,
3727
3741
  className: `ml-2 flex h-8 w-8 shrink-0 items-center justify-center rounded-md hover:bg-[var(--color-surface-muted)] ${isCollapsed ? "text-[var(--color-text-muted)]" : "text-white"}`,
3728
3742
  onClick: isCollapsed ? onExpand : onMinimize,
3729
- children: /* @__PURE__ */ (0, import_jsx_runtime52.jsx)(import_lucide_react20.ListMusic, { "aria-hidden": "true", className: "h-5 w-5" })
3743
+ children: /* @__PURE__ */ (0, import_jsx_runtime53.jsx)(import_lucide_react19.ListMusic, { "aria-hidden": "true", className: "h-5 w-5" })
3730
3744
  }
3731
3745
  )
3732
3746
  ] }),
3733
- !isCollapsed && /* @__PURE__ */ (0, import_jsx_runtime52.jsxs)(
3747
+ !isCollapsed && /* @__PURE__ */ (0, import_jsx_runtime53.jsxs)(
3734
3748
  "ul",
3735
3749
  {
3736
3750
  ref: trackListRef,
@@ -3740,7 +3754,7 @@ function TrackListSidebar({
3740
3754
  onPointerDown: markUserScrollIntent,
3741
3755
  className: layout === "inline" ? "track-list flex-1 min-h-0 overflow-auto list-none p-0 m-0" : "track-list flex-1 min-h-0 overflow-auto list-none p-0 m-0",
3742
3756
  children: [
3743
- trackList.tracks.map((track, index) => /* @__PURE__ */ (0, import_jsx_runtime52.jsx)("li", { "data-track-uuid": track.uuid, children: /* @__PURE__ */ (0, import_jsx_runtime52.jsx)(
3757
+ trackList.tracks.map((track, index) => /* @__PURE__ */ (0, import_jsx_runtime53.jsx)("li", { "data-track-uuid": track.uuid, children: /* @__PURE__ */ (0, import_jsx_runtime53.jsx)(
3744
3758
  TrackItem,
3745
3759
  {
3746
3760
  track,
@@ -3751,7 +3765,7 @@ function TrackListSidebar({
3751
3765
  renderSongLink
3752
3766
  }
3753
3767
  ) }, track.uuid)),
3754
- hasMore && /* @__PURE__ */ (0, import_jsx_runtime52.jsx)("li", { ref: loadMoreSentinelRef, "aria-hidden": "true", className: "h-px" })
3768
+ hasMore && /* @__PURE__ */ (0, import_jsx_runtime53.jsx)("li", { ref: loadMoreSentinelRef, "aria-hidden": "true", className: "h-px" })
3755
3769
  ]
3756
3770
  }
3757
3771
  )
@@ -3764,14 +3778,14 @@ function TrackListSidebar({
3764
3778
  var import_react21 = require("react");
3765
3779
  var import_tailwind_merge = require("tailwind-merge");
3766
3780
  var import_player14 = require("@behindthemusictree/app-kit/player");
3767
- var import_genre_tree34 = require("@behindthemusictree/app-kit/genre-tree");
3781
+ var import_genre_tree35 = require("@behindthemusictree/app-kit/genre-tree");
3768
3782
  var import_ui25 = require("@behindthemusictree/ui");
3769
3783
 
3770
3784
  // src/genre-tree/PlayerControls.tsx
3771
- var import_lucide_react21 = require("lucide-react");
3772
- var import_genre_tree33 = require("@behindthemusictree/app-kit/genre-tree");
3785
+ var import_lucide_react20 = require("lucide-react");
3786
+ var import_genre_tree34 = require("@behindthemusictree/app-kit/genre-tree");
3773
3787
  var import_player13 = require("@behindthemusictree/app-kit/player");
3774
- var import_jsx_runtime53 = require("react/jsx-runtime");
3788
+ var import_jsx_runtime54 = require("react/jsx-runtime");
3775
3789
  function PlayerControls() {
3776
3790
  const {
3777
3791
  duration,
@@ -3783,22 +3797,22 @@ function PlayerControls() {
3783
3797
  setVolume,
3784
3798
  volume
3785
3799
  } = (0, import_player13.usePlayer)();
3786
- const { hasNextTrack, hasPreviousTrack, playNextTrack, playPreviousTrack } = (0, import_genre_tree33.useTrackList)();
3800
+ const { hasNextTrack, hasPreviousTrack, playNextTrack, playPreviousTrack } = (0, import_genre_tree34.useTrackList)();
3787
3801
  const currentTime = (0, import_player13.useCurrentTime)();
3788
3802
  const isBusy = isLoading || playState === import_player13.PlayStates.LOADING;
3789
3803
  const isReady = !!playerTrackObject?.isReady && !isBusy && !playerTrackObject.loadError;
3790
3804
  const safeDuration = Number.isFinite(duration) && duration > 0 ? duration : 0;
3791
3805
  const safeCurrentTime = Number.isFinite(currentTime) ? Math.max(0, Math.min(currentTime, safeDuration)) : 0;
3792
3806
  const isPlaying = playState === import_player13.PlayStates.PLAYING;
3793
- return /* @__PURE__ */ (0, import_jsx_runtime53.jsxs)(
3807
+ return /* @__PURE__ */ (0, import_jsx_runtime54.jsxs)(
3794
3808
  "section",
3795
3809
  {
3796
3810
  "aria-label": "Player controls",
3797
3811
  className: "player-controls flex shrink-0 flex-col gap-2 border-b border-zinc-800 px-3 py-2 text-zinc-200",
3798
3812
  children: [
3799
- /* @__PURE__ */ (0, import_jsx_runtime53.jsxs)("div", { className: "flex min-w-0 items-center gap-2 text-xs tabular-nums text-zinc-400", children: [
3800
- /* @__PURE__ */ (0, import_jsx_runtime53.jsx)("span", { "aria-label": "Elapsed time", children: (0, import_genre_tree33.formatTime)(safeCurrentTime) }),
3801
- /* @__PURE__ */ (0, import_jsx_runtime53.jsx)(
3813
+ /* @__PURE__ */ (0, import_jsx_runtime54.jsxs)("div", { className: "flex min-w-0 items-center gap-2 text-xs tabular-nums text-zinc-400", children: [
3814
+ /* @__PURE__ */ (0, import_jsx_runtime54.jsx)("span", { "aria-label": "Elapsed time", children: (0, import_genre_tree34.formatTime)(safeCurrentTime) }),
3815
+ /* @__PURE__ */ (0, import_jsx_runtime54.jsx)(
3802
3816
  "input",
3803
3817
  {
3804
3818
  "aria-label": "Seek",
@@ -3812,11 +3826,11 @@ function PlayerControls() {
3812
3826
  onChange: (event) => seekTo(Number(event.currentTarget.value))
3813
3827
  }
3814
3828
  ),
3815
- /* @__PURE__ */ (0, import_jsx_runtime53.jsx)("span", { "aria-label": "Duration", children: (0, import_genre_tree33.formatTime)(safeDuration) })
3829
+ /* @__PURE__ */ (0, import_jsx_runtime54.jsx)("span", { "aria-label": "Duration", children: (0, import_genre_tree34.formatTime)(safeDuration) })
3816
3830
  ] }),
3817
- /* @__PURE__ */ (0, import_jsx_runtime53.jsxs)("div", { className: "grid min-h-9 grid-cols-[1fr_auto_1fr] items-center gap-3", children: [
3818
- /* @__PURE__ */ (0, import_jsx_runtime53.jsxs)("div", { className: "col-start-2 flex items-center justify-center gap-2", children: [
3819
- /* @__PURE__ */ (0, import_jsx_runtime53.jsx)(
3831
+ /* @__PURE__ */ (0, import_jsx_runtime54.jsxs)("div", { className: "grid min-h-9 grid-cols-[1fr_auto_1fr] items-center gap-3", children: [
3832
+ /* @__PURE__ */ (0, import_jsx_runtime54.jsxs)("div", { className: "col-start-2 flex items-center justify-center gap-2", children: [
3833
+ /* @__PURE__ */ (0, import_jsx_runtime54.jsx)(
3820
3834
  "button",
3821
3835
  {
3822
3836
  type: "button",
@@ -3825,10 +3839,10 @@ function PlayerControls() {
3825
3839
  disabled: !isReady || !hasPreviousTrack,
3826
3840
  onClick: playPreviousTrack,
3827
3841
  className: "flex h-8 w-8 shrink-0 items-center justify-center rounded-full text-zinc-300 hover:bg-zinc-800 hover:text-white disabled:cursor-not-allowed disabled:opacity-40",
3828
- children: /* @__PURE__ */ (0, import_jsx_runtime53.jsx)(import_lucide_react21.SkipBack, { "aria-hidden": "true", className: "h-4 w-4" })
3842
+ children: /* @__PURE__ */ (0, import_jsx_runtime54.jsx)(import_lucide_react20.SkipBack, { "aria-hidden": "true", className: "h-4 w-4" })
3829
3843
  }
3830
3844
  ),
3831
- /* @__PURE__ */ (0, import_jsx_runtime53.jsx)(
3845
+ /* @__PURE__ */ (0, import_jsx_runtime54.jsx)(
3832
3846
  "button",
3833
3847
  {
3834
3848
  type: "button",
@@ -3837,10 +3851,10 @@ function PlayerControls() {
3837
3851
  disabled: !isReady,
3838
3852
  onClick: handlePlayPauseAction,
3839
3853
  className: "flex h-9 w-9 shrink-0 items-center justify-center rounded-full bg-white text-zinc-950 hover:bg-zinc-200 disabled:cursor-not-allowed disabled:opacity-40",
3840
- children: isPlaying ? /* @__PURE__ */ (0, import_jsx_runtime53.jsx)(import_lucide_react21.Pause, { "aria-hidden": "true", className: "h-4 w-4", fill: "currentColor" }) : /* @__PURE__ */ (0, import_jsx_runtime53.jsx)(import_lucide_react21.Play, { "aria-hidden": "true", className: "h-4 w-4", fill: "currentColor" })
3854
+ children: isPlaying ? /* @__PURE__ */ (0, import_jsx_runtime54.jsx)(import_lucide_react20.Pause, { "aria-hidden": "true", className: "h-4 w-4", fill: "currentColor" }) : /* @__PURE__ */ (0, import_jsx_runtime54.jsx)(import_lucide_react20.Play, { "aria-hidden": "true", className: "h-4 w-4", fill: "currentColor" })
3841
3855
  }
3842
3856
  ),
3843
- /* @__PURE__ */ (0, import_jsx_runtime53.jsx)(
3857
+ /* @__PURE__ */ (0, import_jsx_runtime54.jsx)(
3844
3858
  "button",
3845
3859
  {
3846
3860
  type: "button",
@@ -3849,14 +3863,14 @@ function PlayerControls() {
3849
3863
  disabled: !isReady || !hasNextTrack,
3850
3864
  onClick: playNextTrack,
3851
3865
  className: "flex h-8 w-8 shrink-0 items-center justify-center rounded-full text-zinc-300 hover:bg-zinc-800 hover:text-white disabled:cursor-not-allowed disabled:opacity-40",
3852
- children: /* @__PURE__ */ (0, import_jsx_runtime53.jsx)(import_lucide_react21.SkipForward, { "aria-hidden": "true", className: "h-4 w-4" })
3866
+ children: /* @__PURE__ */ (0, import_jsx_runtime54.jsx)(import_lucide_react20.SkipForward, { "aria-hidden": "true", className: "h-4 w-4" })
3853
3867
  }
3854
3868
  )
3855
3869
  ] }),
3856
- /* @__PURE__ */ (0, import_jsx_runtime53.jsxs)("label", { className: "col-start-3 flex w-[4.2rem] shrink-0 items-center gap-[0.35rem] justify-self-end text-zinc-400 sm:w-[4.9rem]", children: [
3857
- volume === 0 ? /* @__PURE__ */ (0, import_jsx_runtime53.jsx)(import_lucide_react21.VolumeX, { "aria-hidden": "true", className: "h-[0.7rem] w-[0.7rem] shrink-0" }) : /* @__PURE__ */ (0, import_jsx_runtime53.jsx)(import_lucide_react21.Volume2, { "aria-hidden": "true", className: "h-[0.7rem] w-[0.7rem] shrink-0" }),
3858
- /* @__PURE__ */ (0, import_jsx_runtime53.jsx)("span", { className: "sr-only", children: "Volume" }),
3859
- /* @__PURE__ */ (0, import_jsx_runtime53.jsx)(
3870
+ /* @__PURE__ */ (0, import_jsx_runtime54.jsxs)("label", { className: "col-start-3 flex w-[4.2rem] shrink-0 items-center gap-[0.35rem] justify-self-end text-zinc-400 sm:w-[4.9rem]", children: [
3871
+ volume === 0 ? /* @__PURE__ */ (0, import_jsx_runtime54.jsx)(import_lucide_react20.VolumeX, { "aria-hidden": "true", className: "h-[0.7rem] w-[0.7rem] shrink-0" }) : /* @__PURE__ */ (0, import_jsx_runtime54.jsx)(import_lucide_react20.Volume2, { "aria-hidden": "true", className: "h-[0.7rem] w-[0.7rem] shrink-0" }),
3872
+ /* @__PURE__ */ (0, import_jsx_runtime54.jsx)("span", { className: "sr-only", children: "Volume" }),
3873
+ /* @__PURE__ */ (0, import_jsx_runtime54.jsx)(
3860
3874
  "input",
3861
3875
  {
3862
3876
  "aria-label": "Volume",
@@ -3878,7 +3892,7 @@ function PlayerControls() {
3878
3892
  }
3879
3893
 
3880
3894
  // src/genre-tree/PlayerPanel.tsx
3881
- var import_jsx_runtime54 = require("react/jsx-runtime");
3895
+ var import_jsx_runtime55 = require("react/jsx-runtime");
3882
3896
  var PANEL_ID = "player-panel";
3883
3897
  var DEFAULT_EXTENDED_WIDTH = "calc(100vw - var(--layout-center-left) - var(--layout-gutter))";
3884
3898
  function PlayerPanel({
@@ -3891,7 +3905,7 @@ function PlayerPanel({
3891
3905
  renderSongLink
3892
3906
  }) {
3893
3907
  const { playerTrackObject, retryPlayback } = (0, import_player14.usePlayer)();
3894
- const { toggleTrackListSidebar, isTrackListSidebarVisible } = (0, import_genre_tree34.useTrackListSidebarVisibility)();
3908
+ const { toggleTrackListSidebar, isTrackListSidebarVisible } = (0, import_genre_tree35.useTrackListSidebarVisibility)();
3895
3909
  const [lastLoadedTrackTitle, setLastLoadedTrackTitle] = (0, import_react21.useState)("");
3896
3910
  const loadedTrackTitle = playerTrackObject?.track.title;
3897
3911
  (0, import_react21.useEffect)(() => {
@@ -3918,7 +3932,7 @@ function PlayerPanel({
3918
3932
  onClick: () => setIsExtendRequested(true),
3919
3933
  expanded: false
3920
3934
  } : null;
3921
- return /* @__PURE__ */ (0, import_jsx_runtime54.jsxs)(
3935
+ return /* @__PURE__ */ (0, import_jsx_runtime55.jsxs)(
3922
3936
  "div",
3923
3937
  {
3924
3938
  id: PANEL_ID,
@@ -3931,7 +3945,7 @@ function PlayerPanel({
3931
3945
  ),
3932
3946
  style: isExtended ? { width: extendedWidth } : void 0,
3933
3947
  children: [
3934
- /* @__PURE__ */ (0, import_jsx_runtime54.jsx)(
3948
+ /* @__PURE__ */ (0, import_jsx_runtime55.jsx)(
3935
3949
  import_ui25.PanelEdgeTab,
3936
3950
  {
3937
3951
  side: "right",
@@ -3941,7 +3955,7 @@ function PlayerPanel({
3941
3955
  className: "absolute right-full top-1/2 -translate-y-1/2"
3942
3956
  }
3943
3957
  ),
3944
- /* @__PURE__ */ (0, import_jsx_runtime54.jsxs)(
3958
+ /* @__PURE__ */ (0, import_jsx_runtime55.jsxs)(
3945
3959
  "div",
3946
3960
  {
3947
3961
  className: (0, import_tailwind_merge.twMerge)(
@@ -3949,20 +3963,20 @@ function PlayerPanel({
3949
3963
  isExtended ? "flex-row" : "flex-col"
3950
3964
  ),
3951
3965
  children: [
3952
- /* @__PURE__ */ (0, import_jsx_runtime54.jsxs)("div", { className: "flex w-[var(--layout-right-w)] shrink-0 flex-col", children: [
3953
- /* @__PURE__ */ (0, import_jsx_runtime54.jsx)("div", { className: "flex shrink-0 items-center justify-between gap-2 border-b border-zinc-800 p-2", children: /* @__PURE__ */ (0, import_jsx_runtime54.jsx)("span", { className: "min-w-0 truncate text-sm font-semibold", children: loadedTrackTitle || lastLoadedTrackTitle }) }),
3954
- /* @__PURE__ */ (0, import_jsx_runtime54.jsxs)("div", { className: "relative w-full shrink-0", children: [
3955
- /* @__PURE__ */ (0, import_jsx_runtime54.jsx)(import_player14.PlayerVideoSurface, { className: "aspect-video w-full bg-black" }),
3956
- playerTrackObject?.loadError && /* @__PURE__ */ (0, import_jsx_runtime54.jsxs)("span", { className: "absolute bottom-2 left-2 right-2 text-xs text-red-400", children: [
3966
+ /* @__PURE__ */ (0, import_jsx_runtime55.jsxs)("div", { className: "flex w-[var(--layout-right-w)] shrink-0 flex-col", children: [
3967
+ /* @__PURE__ */ (0, import_jsx_runtime55.jsx)("div", { className: "flex shrink-0 items-center justify-between gap-2 border-b border-zinc-800 p-2", children: /* @__PURE__ */ (0, import_jsx_runtime55.jsx)("span", { className: "min-w-0 truncate text-sm font-semibold", children: loadedTrackTitle || lastLoadedTrackTitle }) }),
3968
+ /* @__PURE__ */ (0, import_jsx_runtime55.jsxs)("div", { className: "relative w-full shrink-0", children: [
3969
+ /* @__PURE__ */ (0, import_jsx_runtime55.jsx)(import_player14.PlayerVideoSurface, { className: "aspect-video w-full bg-black" }),
3970
+ playerTrackObject?.loadError && /* @__PURE__ */ (0, import_jsx_runtime55.jsxs)("span", { className: "absolute bottom-2 left-2 right-2 text-xs text-red-400", children: [
3957
3971
  playerTrackObject.loadError,
3958
- /* @__PURE__ */ (0, import_jsx_runtime54.jsx)("button", { type: "button", className: "ml-2 underline", onClick: () => {
3972
+ /* @__PURE__ */ (0, import_jsx_runtime55.jsx)("button", { type: "button", className: "ml-2 underline", onClick: () => {
3959
3973
  void retryPlayback();
3960
3974
  }, children: "Retry" })
3961
3975
  ] })
3962
3976
  ] }),
3963
- /* @__PURE__ */ (0, import_jsx_runtime54.jsx)(PlayerControls, {})
3977
+ /* @__PURE__ */ (0, import_jsx_runtime55.jsx)(PlayerControls, {})
3964
3978
  ] }),
3965
- /* @__PURE__ */ (0, import_jsx_runtime54.jsx)(
3979
+ /* @__PURE__ */ (0, import_jsx_runtime55.jsx)(
3966
3980
  TrackListSidebar,
3967
3981
  {
3968
3982
  layout: "inline",
@@ -3988,33 +4002,33 @@ function PlayerPanel({
3988
4002
  // src/genre-tree/Rating.tsx
3989
4003
  var import_uuid = require("uuid");
3990
4004
  var import_ai = require("react-icons/ai");
3991
- var import_genre_tree35 = require("@behindthemusictree/app-kit/genre-tree");
3992
- var import_jsx_runtime55 = require("react/jsx-runtime");
4005
+ var import_genre_tree36 = require("@behindthemusictree/app-kit/genre-tree");
4006
+ var import_jsx_runtime56 = require("react/jsx-runtime");
3993
4007
  function Rating({ rating, handleChange }) {
3994
4008
  const instanceId = (0, import_uuid.v4)();
3995
- return /* @__PURE__ */ (0, import_jsx_runtime55.jsx)("div", { className: "flex items-center", children: [...Array(6)].map((_, i) => /* @__PURE__ */ (0, import_jsx_runtime55.jsxs)("div", { className: `relative ${i === 0 ? "mr-4" : "mr-1"}`, children: [
3996
- /* @__PURE__ */ (0, import_jsx_runtime55.jsx)(
4009
+ return /* @__PURE__ */ (0, import_jsx_runtime56.jsx)("div", { className: "flex items-center", children: [...Array(6)].map((_, i) => /* @__PURE__ */ (0, import_jsx_runtime56.jsxs)("div", { className: `relative ${i === 0 ? "mr-4" : "mr-1"}`, children: [
4010
+ /* @__PURE__ */ (0, import_jsx_runtime56.jsx)(
3997
4011
  "input",
3998
4012
  {
3999
4013
  id: `star-${instanceId}-${i}`,
4000
4014
  className: "w-6 h-6 opacity-0",
4001
4015
  type: "radio",
4002
4016
  name: "rating",
4003
- value: i === 0 ? import_genre_tree35.FORM_RATING_NULL_VALUE : i,
4017
+ value: i === 0 ? import_genre_tree36.FORM_RATING_NULL_VALUE : i,
4004
4018
  checked: rating === (i === 0 ? null : i),
4005
4019
  onChange: handleChange
4006
4020
  }
4007
4021
  ),
4008
- /* @__PURE__ */ (0, import_jsx_runtime55.jsx)("label", { htmlFor: `star-${instanceId}-${i}`, className: "cursor-pointer", children: i === 0 ? /* @__PURE__ */ (0, import_jsx_runtime55.jsxs)("div", { className: "flex", children: [
4009
- /* @__PURE__ */ (0, import_jsx_runtime55.jsx)(
4022
+ /* @__PURE__ */ (0, import_jsx_runtime56.jsx)("label", { htmlFor: `star-${instanceId}-${i}`, className: "cursor-pointer", children: i === 0 ? /* @__PURE__ */ (0, import_jsx_runtime56.jsxs)("div", { className: "flex", children: [
4023
+ /* @__PURE__ */ (0, import_jsx_runtime56.jsx)(
4010
4024
  import_ai.AiOutlineStar,
4011
4025
  {
4012
- className: `w-7 h-7 absolute top-0 left-0 ${rating === import_genre_tree35.FORM_RATING_NULL_VALUE ? "text-[var(--color-accent-rating)]" : "text-[var(--color-text-muted)]"}`
4026
+ className: `w-7 h-7 absolute top-0 left-0 ${rating === import_genre_tree36.FORM_RATING_NULL_VALUE ? "text-[var(--color-accent-rating)]" : "text-[var(--color-text-muted)]"}`
4013
4027
  }
4014
4028
  ),
4015
- /* @__PURE__ */ (0, import_jsx_runtime55.jsx)("span", { className: "flex-grow w-8" }),
4029
+ /* @__PURE__ */ (0, import_jsx_runtime56.jsx)("span", { className: "flex-grow w-8" }),
4016
4030
  " "
4017
- ] }) : /* @__PURE__ */ (0, import_jsx_runtime55.jsx)(
4031
+ ] }) : /* @__PURE__ */ (0, import_jsx_runtime56.jsx)(
4018
4032
  import_ai.AiFillStar,
4019
4033
  {
4020
4034
  className: `w-7 h-7 absolute top-0 left-0 ${(rating ?? -1) >= i ? "text-[var(--color-accent-rating)]" : "text-[var(--color-text-muted)]"}`
@@ -4026,7 +4040,7 @@ function Rating({ rating, handleChange }) {
4026
4040
  // src/genre-tree/EntityDetailPanel.tsx
4027
4041
  var import_react22 = require("react");
4028
4042
  var import_ui26 = require("@behindthemusictree/ui");
4029
- var import_jsx_runtime56 = require("react/jsx-runtime");
4043
+ var import_jsx_runtime57 = require("react/jsx-runtime");
4030
4044
  function EntityDetailPanel({
4031
4045
  scope,
4032
4046
  getBackendBaseUrl,
@@ -4049,8 +4063,8 @@ function EntityDetailPanel({
4049
4063
  getBackendBaseUrl,
4050
4064
  getArtistBackendBaseUrl,
4051
4065
  getShareUrl,
4052
- renderArtistLink: renderArtistLink ?? ((artist) => artist.isSpecialPurpose ? artist.name : /* @__PURE__ */ (0, import_jsx_runtime56.jsx)("button", { type: "button", onClick: () => onArtistUuidChange(artist.uuid), className: "hover:underline", children: artist.name })),
4053
- renderSongLink: renderSongLink ?? ((song) => /* @__PURE__ */ (0, import_jsx_runtime56.jsx)("button", { type: "button", onClick: () => onSongUuidChange(song.uuid), className: "hover:underline", children: song.title })),
4066
+ renderArtistLink: renderArtistLink ?? ((artist) => artist.isSpecialPurpose ? artist.name : /* @__PURE__ */ (0, import_jsx_runtime57.jsx)("button", { type: "button", onClick: () => onArtistUuidChange(artist.uuid), className: "hover:underline", children: artist.name })),
4067
+ renderSongLink: renderSongLink ?? ((song) => /* @__PURE__ */ (0, import_jsx_runtime57.jsx)("button", { type: "button", onClick: () => onSongUuidChange(song.uuid), className: "hover:underline", children: song.title })),
4054
4068
  song: {
4055
4069
  uuid: songUuid,
4056
4070
  expanded: songUuid !== null && expandedSongUuid === songUuid,
@@ -4066,7 +4080,7 @@ function EntityDetailPanel({
4066
4080
  ...navigation
4067
4081
  });
4068
4082
  if (!panel) return null;
4069
- return /* @__PURE__ */ (0, import_jsx_runtime56.jsx)(
4083
+ return /* @__PURE__ */ (0, import_jsx_runtime57.jsx)(
4070
4084
  import_ui26.DetailPanel,
4071
4085
  {
4072
4086
  side: "left",
@@ -4078,7 +4092,7 @@ function EntityDetailPanel({
4078
4092
  onExpandedChange: panel.onExpandedChange,
4079
4093
  aside: panel.extraContent,
4080
4094
  edgeTab,
4081
- children: panel.extraContent && !panel.expanded ? /* @__PURE__ */ (0, import_jsx_runtime56.jsxs)(import_jsx_runtime56.Fragment, { children: [
4095
+ children: panel.extraContent && !panel.expanded ? /* @__PURE__ */ (0, import_jsx_runtime57.jsxs)(import_jsx_runtime57.Fragment, { children: [
4082
4096
  panel.content,
4083
4097
  panel.extraContent
4084
4098
  ] }) : panel.content