@behindthemusictree/app-kit-ui 40.1.0 → 42.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
@@ -596,6 +596,7 @@ var import_lucide_react18 = 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
@@ -1700,6 +1701,29 @@ function ClampedText({ children, className: className2 = "" }) {
1700
1701
  ] });
1701
1702
  }
1702
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
+
1703
1727
  // src/genre-tree/GenrePlaylistTracks.tsx
1704
1728
  var import_react13 = require("react");
1705
1729
  var import_react_query = require("@tanstack/react-query");
@@ -1722,15 +1746,15 @@ var import_genre_tree16 = require("@behindthemusictree/app-kit/genre-tree");
1722
1746
 
1723
1747
  // src/genre-tree/MusicBrainzRecordingLink.tsx
1724
1748
  var import_genre_tree11 = require("@behindthemusictree/app-kit/genre-tree");
1725
- var import_jsx_runtime32 = require("react/jsx-runtime");
1749
+ var import_jsx_runtime33 = require("react/jsx-runtime");
1726
1750
  function MusicBrainzRecordingLink({
1727
1751
  song,
1728
1752
  children,
1729
1753
  className: className2 = ""
1730
1754
  }) {
1731
1755
  const url = (0, import_genre_tree11.musicBrainzRecordingUrl)(song);
1732
- if (!url) return /* @__PURE__ */ (0, import_jsx_runtime32.jsx)(import_jsx_runtime32.Fragment, { children });
1733
- 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)(
1734
1758
  "a",
1735
1759
  {
1736
1760
  href: url,
@@ -1747,7 +1771,7 @@ function MusicBrainzRecordingLink({
1747
1771
  // src/genre-tree/GenrePlaylistTracks.tsx
1748
1772
  var import_genre_tree17 = require("@behindthemusictree/app-kit/genre-tree");
1749
1773
  var import_track_noun2 = require("@behindthemusictree/app-kit/track-noun");
1750
- var import_jsx_runtime33 = require("react/jsx-runtime");
1774
+ var import_jsx_runtime34 = require("react/jsx-runtime");
1751
1775
  var CATALOGUE_PAGE_SIZE = 100;
1752
1776
  var SongPageSchema = (0, import_transport5.PaginatedResponseSchema)(import_genre_tree15.SongDetailedSchema).extend({
1753
1777
  overallTotal: import_zod.z.number().nullable(),
@@ -1834,35 +1858,35 @@ function GenrePlaylistTracks({
1834
1858
  started.catch((error) => {
1835
1859
  console.error("Failed to play genre playlist:", error);
1836
1860
  if (error instanceof import_genre_tree16.TracksFailureAlreadyReportedError) return;
1837
- 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 }));
1838
1862
  });
1839
1863
  };
1840
- 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: [
1841
1865
  renderTracksToolbar?.(),
1842
- 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: [
1843
1867
  "Couldn't load ",
1844
1868
  trackNoun.plural,
1845
1869
  ".",
1846
1870
  " ",
1847
- /* @__PURE__ */ (0, import_jsx_runtime33.jsx)("button", { type: "button", className: "underline", onClick: () => void refetch(), children: "Retry" })
1848
- ] }) : 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: [
1849
1873
  tracks.map((track, index) => {
1850
1874
  const label = track.artists?.length ? `${track.title} \u2014 ${track.artists.map((artist2) => artist2.name).join(", ")}` : track.title;
1851
1875
  const isCurrent = playerTrackObject?.track.id === track.uuid;
1852
1876
  const isPlaying = isCurrent && playState === import_player6.PlayStates.PLAYING;
1853
- const text = /* @__PURE__ */ (0, import_jsx_runtime33.jsxs)("span", { className: TEXT_CELL, children: [
1854
- /* @__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 }),
1855
- track.artists?.length ? /* @__PURE__ */ (0, import_jsx_runtime33.jsx)("span", { className: SEPARATOR, children: " \u2014 " }) : null,
1856
- /* @__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 }),
1857
- 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 })
1858
1882
  ] });
1859
1883
  const leading = renderSongLeading?.(track, index);
1860
1884
  const actions = renderSongActions?.(track);
1861
1885
  const isUnplayable = (0, import_genre_tree15.isSong)(track) && (0, import_genre_tree15.songUnplayableReason)(track) !== null;
1862
1886
  if (isUnplayable) {
1863
- 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: [
1864
1888
  leading,
1865
- /* @__PURE__ */ (0, import_jsx_runtime33.jsx)(
1889
+ /* @__PURE__ */ (0, import_jsx_runtime34.jsx)(
1866
1890
  "button",
1867
1891
  {
1868
1892
  type: "button",
@@ -1870,27 +1894,27 @@ function GenrePlaylistTracks({
1870
1894
  "aria-disabled": "true",
1871
1895
  "aria-label": label,
1872
1896
  className: "flex h-6 w-6 shrink-0 items-center justify-center rounded-full opacity-0 cursor-not-allowed",
1873
- 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 })
1874
1898
  }
1875
1899
  ),
1876
1900
  text,
1877
1901
  actions
1878
1902
  ] }, track.uuid);
1879
1903
  }
1880
- return /* @__PURE__ */ (0, import_jsx_runtime33.jsxs)(
1904
+ return /* @__PURE__ */ (0, import_jsx_runtime34.jsxs)(
1881
1905
  "li",
1882
1906
  {
1883
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)]" : ""}`,
1884
1908
  children: [
1885
1909
  leading,
1886
- /* @__PURE__ */ (0, import_jsx_runtime33.jsx)(
1910
+ /* @__PURE__ */ (0, import_jsx_runtime34.jsx)(
1887
1911
  "button",
1888
1912
  {
1889
1913
  type: "button",
1890
1914
  "aria-label": `${isPlaying ? "Pause" : "Play"} ${label}`,
1891
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"}`,
1892
1916
  onClick: () => isCurrent ? handlePlayPauseAction() : play(track.uuid),
1893
- 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 })
1894
1918
  }
1895
1919
  ),
1896
1920
  text,
@@ -1900,7 +1924,7 @@ function GenrePlaylistTracks({
1900
1924
  track.uuid
1901
1925
  );
1902
1926
  }),
1903
- 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" })
1904
1928
  ] })
1905
1929
  ] }) });
1906
1930
  }
@@ -1908,7 +1932,7 @@ function GenrePlaylistTracks({
1908
1932
  // src/genre-tree/GenreDetailExtras.tsx
1909
1933
  var import_track_noun3 = require("@behindthemusictree/app-kit/track-noun");
1910
1934
  var import_si2 = require("react-icons/si");
1911
- var import_jsx_runtime34 = require("react/jsx-runtime");
1935
+ var import_jsx_runtime35 = require("react/jsx-runtime");
1912
1936
  function GenreDetailExtras({
1913
1937
  genreUuid,
1914
1938
  genrePlaylist,
@@ -1934,17 +1958,18 @@ function GenreDetailExtras({
1934
1958
  criteriaOverviewSchema
1935
1959
  );
1936
1960
  if (isPending) {
1937
- return /* @__PURE__ */ (0, import_jsx_runtime34.jsxs)(import_jsx_runtime34.Fragment, { children: [
1938
- /* @__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" }) }) }),
1939
1963
  family
1940
1964
  ] });
1941
1965
  }
1942
- if (!overview) return /* @__PURE__ */ (0, import_jsx_runtime34.jsx)(import_jsx_runtime34.Fragment, { children: family });
1943
- 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;
1944
1968
  const wikidataUrl = wikidataId ? `https://www.wikidata.org/wiki/${encodeURIComponent(wikidataId)}` : null;
1945
- return /* @__PURE__ */ (0, import_jsx_runtime34.jsxs)(import_jsx_runtime34.Fragment, { children: [
1946
- (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: [
1947
- 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)(
1948
1973
  "a",
1949
1974
  {
1950
1975
  href: summarySourceUrl,
@@ -1952,12 +1977,12 @@ function GenreDetailExtras({
1952
1977
  rel: "noopener noreferrer",
1953
1978
  className: "inline-flex items-center gap-1.5 text-[var(--color-text-muted)] hover:text-[var(--color-text)]",
1954
1979
  children: [
1955
- /* @__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" }),
1956
1981
  "Wikipedia"
1957
1982
  ]
1958
1983
  }
1959
1984
  ),
1960
- wikidataUrl && /* @__PURE__ */ (0, import_jsx_runtime34.jsxs)(
1985
+ wikidataUrl && /* @__PURE__ */ (0, import_jsx_runtime35.jsxs)(
1961
1986
  "a",
1962
1987
  {
1963
1988
  href: wikidataUrl,
@@ -1965,17 +1990,17 @@ function GenreDetailExtras({
1965
1990
  rel: "noopener noreferrer",
1966
1991
  className: "inline-flex items-center gap-1.5 text-[var(--color-text-muted)] hover:text-[var(--color-text)]",
1967
1992
  children: [
1968
- /* @__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" }),
1969
1994
  "Wikidata"
1970
1995
  ]
1971
1996
  }
1972
1997
  )
1973
1998
  ] }) }),
1974
- /* @__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) }),
1975
2000
  family,
1976
2001
  renderGenreDetailExtras?.(overview),
1977
- (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" }) }),
1978
- /* @__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)(
1979
2004
  GenrePlaylistTracks,
1980
2005
  {
1981
2006
  genreUuid,
@@ -1993,10 +2018,10 @@ function GenreDetailExtras({
1993
2018
  }
1994
2019
 
1995
2020
  // src/genre-tree/GenreInfoPanelBody.tsx
1996
- var import_jsx_runtime35 = require("react/jsx-runtime");
2021
+ var import_jsx_runtime36 = require("react/jsx-runtime");
1997
2022
  var import_react14 = require("react");
1998
2023
  function GenreNodeActionBand({ genrePlaylist, playback, ...bandProps }) {
1999
- 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 }) });
2000
2025
  }
2001
2026
  function GenreInfoPanelBody({
2002
2027
  toolbar,
@@ -2013,9 +2038,9 @@ function GenreInfoPanelBody({
2013
2038
  }) {
2014
2039
  const summary = scope === "reference" ? import_genre_tree19.GenreCatalogueCountsSchema.parse(genrePlaylist ?? catalogue) : void 0;
2015
2040
  const selection = summary ? (0, import_genre_tree19.defaultGenreMusicSelection)(summary) : void 0;
2016
- 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: [
2017
2042
  toolbar,
2018
- 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 }),
2019
2044
  /* @__PURE__ */ (0, import_react14.createElement)(
2020
2045
  GenreDetailExtras,
2021
2046
  {
@@ -2035,7 +2060,7 @@ function GenreInfoPanelBody({
2035
2060
  }
2036
2061
 
2037
2062
  // src/genre-tree/GenreDetailPanel.tsx
2038
- var import_jsx_runtime36 = require("react/jsx-runtime");
2063
+ var import_jsx_runtime37 = require("react/jsx-runtime");
2039
2064
  var RELATIVE_ITEM_COUNT = 1;
2040
2065
  function toNode(genre, parentId) {
2041
2066
  return {
@@ -2060,7 +2085,8 @@ function useGenreDetailContent({
2060
2085
  expanded,
2061
2086
  treeStyles
2062
2087
  }) {
2063
- 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;
2064
2090
  const defaultPlayback = useGenrePlaylistPlayback({ scope, genrePlaylists: [], artist: extras?.artist });
2065
2091
  const playback = hostPlayback ?? defaultPlayback;
2066
2092
  const view = (0, import_react15.useMemo)(() => {
@@ -2089,22 +2115,27 @@ function useGenreDetailContent({
2089
2115
  )
2090
2116
  };
2091
2117
  }, [detail, scope, treeStyles]);
2092
- if (isError) return {
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 {
2093
2124
  name: null,
2094
2125
  title: "Genre unavailable",
2095
- 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." })
2096
2127
  };
2097
- if (isPending || !detail || !view) return {
2128
+ if (!detail || !view) return {
2098
2129
  name: null,
2099
2130
  title: "Loading genre\u2026",
2100
- content: /* @__PURE__ */ (0, import_jsx_runtime36.jsxs)("div", { className: "flex flex-col gap-3", "aria-busy": "true", "data-testid": "genre-detail-panel-skeleton", children: [
2101
- /* @__PURE__ */ (0, import_jsx_runtime36.jsx)(import_ui13.Skeleton, { className: "h-6 w-2/3" }),
2102
- /* @__PURE__ */ (0, import_jsx_runtime36.jsx)(import_ui13.Skeleton, { className: "h-4 w-full" }),
2103
- /* @__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" })
2104
2135
  ] })
2105
2136
  };
2106
2137
  const selectGenre = (nodeId) => onSelectGenre(import_genre_tree20.GenreUuidSchema.parse(nodeId));
2107
- const family = /* @__PURE__ */ (0, import_jsx_runtime36.jsx)(
2138
+ const family = /* @__PURE__ */ (0, import_jsx_runtime37.jsx)(
2108
2139
  import_genre_tree_view4.GenreNodeFamily,
2109
2140
  {
2110
2141
  node: view.node,
@@ -2115,17 +2146,17 @@ function useGenreDetailContent({
2115
2146
  onSelectNode: selectGenre
2116
2147
  }
2117
2148
  );
2118
- 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 });
2119
2150
  return {
2120
2151
  name: detail.name,
2121
2152
  title: detail.name,
2122
2153
  headerAccent: { background: view.fill, color: view.textColor },
2123
2154
  aside: expanded ? ancestors : void 0,
2124
- content: /* @__PURE__ */ (0, import_jsx_runtime36.jsxs)(import_jsx_runtime36.Fragment, { children: [
2125
- /* @__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)(
2126
2157
  GenreInfoPanelBody,
2127
2158
  {
2128
- 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: [
2129
2160
  family,
2130
2161
  !expanded && ancestors
2131
2162
  ] }),
@@ -2169,7 +2200,7 @@ function GenreDetailPanel({
2169
2200
  expanded,
2170
2201
  band: { onZoomIn, shareUrl, shareTarget: shareUrl ? shareTarget : void 0 }
2171
2202
  });
2172
- return /* @__PURE__ */ (0, import_jsx_runtime36.jsx)(
2203
+ return /* @__PURE__ */ (0, import_jsx_runtime37.jsx)(
2173
2204
  import_ui13.DetailPanel,
2174
2205
  {
2175
2206
  side: "left",
@@ -2182,7 +2213,7 @@ function GenreDetailPanel({
2182
2213
  edgeTab,
2183
2214
  aside: panel.aside,
2184
2215
  titleInSearch,
2185
- 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,
2186
2217
  children: panel.content
2187
2218
  }
2188
2219
  );
@@ -2191,7 +2222,7 @@ function GenreDetailPanel({
2191
2222
  // src/genre-tree/ChangeParentPopup.tsx
2192
2223
  var import_react16 = require("react");
2193
2224
  var import_genre_tree21 = require("@behindthemusictree/app-kit/genre-tree");
2194
- var import_jsx_runtime37 = require("react/jsx-runtime");
2225
+ var import_jsx_runtime38 = require("react/jsx-runtime");
2195
2226
  function ChangeParentPopup({
2196
2227
  genrePlaylist,
2197
2228
  genrePlaylists,
@@ -2226,12 +2257,12 @@ function ChangeParentPopup({
2226
2257
  window.addEventListener("keydown", handleKeyDown);
2227
2258
  return () => window.removeEventListener("keydown", handleKeyDown);
2228
2259
  }, [onClose]);
2229
- return /* @__PURE__ */ (0, import_jsx_runtime37.jsxs)(BasePopup_default, { title: `Change parent of ${genrePlaylist.name}`, onClose, showCancelButton: true, fitContent: true, children: [
2230
- /* @__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: [
2231
2262
  "Current parent: ",
2232
2263
  genrePlaylist.parent?.name ?? "none"
2233
2264
  ] }),
2234
- /* @__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)(
2235
2266
  GenreSearch,
2236
2267
  {
2237
2268
  genrePlaylists: candidates,
@@ -2239,7 +2270,7 @@ function ChangeParentPopup({
2239
2270
  placeholder: "Search a new parent\u2026"
2240
2271
  }
2241
2272
  ) }),
2242
- 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(" ") })
2243
2274
  ] });
2244
2275
  }
2245
2276
 
@@ -2252,7 +2283,7 @@ var import_lucide_react14 = require("lucide-react");
2252
2283
  var import_ui14 = require("@behindthemusictree/ui");
2253
2284
  var import_genre_tree22 = require("@behindthemusictree/app-kit/genre-tree");
2254
2285
  var import_player7 = require("@behindthemusictree/app-kit/player");
2255
- var import_jsx_runtime38 = require("react/jsx-runtime");
2286
+ var import_jsx_runtime39 = require("react/jsx-runtime");
2256
2287
  function ArtistActionBand({ artist, share }) {
2257
2288
  const { trackList, playNewTrackListFromArtist } = (0, import_genre_tree22.useTrackList)();
2258
2289
  const { isPlaying, setIsPlaying } = (0, import_player7.usePlayer)();
@@ -2268,11 +2299,11 @@ function ArtistActionBand({ artist, share }) {
2268
2299
  console.error("Failed to load artist tracks:", error);
2269
2300
  });
2270
2301
  };
2271
- return /* @__PURE__ */ (0, import_jsx_runtime38.jsx)(import_ui14.PanelBleed, { children: /* @__PURE__ */ (0, import_jsx_runtime38.jsxs)(import_ui14.ActionBand, { children: [
2272
- /* @__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)(
2273
2304
  import_ui14.ActionButton,
2274
2305
  {
2275
- 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" }),
2276
2307
  label: playing ? "Pause" : "Play",
2277
2308
  caption: String(artist.playableTracksCount),
2278
2309
  disabled: artist.playableTracksCount === 0,
@@ -2291,7 +2322,7 @@ var import_ui16 = require("@behindthemusictree/ui");
2291
2322
  var import_lucide_react15 = require("lucide-react");
2292
2323
  var import_si3 = require("react-icons/si");
2293
2324
  var import_ui15 = require("@behindthemusictree/ui");
2294
- var import_jsx_runtime39 = require("react/jsx-runtime");
2325
+ var import_jsx_runtime40 = require("react/jsx-runtime");
2295
2326
  var LINK_KINDS = {
2296
2327
  official: { label: "Official website", Icon: import_lucide_react15.Globe },
2297
2328
  wikipedia: { label: "Wikipedia", Icon: import_si3.SiWikipedia },
@@ -2328,7 +2359,7 @@ function artistLinks(artist) {
2328
2359
  return links;
2329
2360
  }
2330
2361
  function ArtistLinks({ links }) {
2331
- 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)) });
2332
2363
  }
2333
2364
 
2334
2365
  // src/genre-tree/artist/formatYears.ts
@@ -2339,45 +2370,59 @@ function formatYears(beginYear, endYear, ended) {
2339
2370
  }
2340
2371
 
2341
2372
  // src/genre-tree/artist/ArtistHeader.tsx
2342
- var import_jsx_runtime40 = require("react/jsx-runtime");
2373
+ var import_jsx_runtime41 = require("react/jsx-runtime");
2343
2374
  var countryNames = new Intl.DisplayNames(["en"], { type: "region" });
2344
- function ArtistHeader({ artist }) {
2375
+ function originLabel(artist, origin) {
2376
+ if (origin.source === "area") return "From";
2377
+ if (artist.type === "person") return "Born in";
2378
+ if (artist.type === "group") return "Formed in";
2379
+ return "From";
2380
+ }
2381
+ function OriginValue({
2382
+ origin,
2383
+ country,
2384
+ renderPlaceLink
2385
+ }) {
2386
+ const place = renderPlaceLink && origin.regionId !== null ? renderPlaceLink({ ...origin, regionId: origin.regionId }) : origin.name;
2387
+ return /* @__PURE__ */ (0, import_jsx_runtime41.jsxs)(import_jsx_runtime41.Fragment, { children: [
2388
+ place,
2389
+ country && origin.kind !== "country" && `, ${country}`
2390
+ ] });
2391
+ }
2392
+ function ArtistHeader({
2393
+ artist,
2394
+ renderPlaceLink
2395
+ }) {
2345
2396
  const fields = [];
2346
2397
  if (artist.type) fields.push({ label: "Type", value: artist.type.charAt(0).toUpperCase() + artist.type.slice(1) });
2347
- const country = artist.country && countryNames.of(artist.country);
2348
- if (country) fields.push({ label: "Country", value: country });
2398
+ const country = artist.country ? countryNames.of(artist.country) : void 0;
2399
+ if (artist.origin) {
2400
+ fields.push({
2401
+ label: originLabel(artist, artist.origin),
2402
+ value: /* @__PURE__ */ (0, import_jsx_runtime41.jsx)(OriginValue, { origin: artist.origin, country, renderPlaceLink })
2403
+ });
2404
+ }
2405
+ if (country && !artist.origin) fields.push({ label: "Country", value: country });
2349
2406
  const years = formatYears(artist.beginYear, artist.endYear, artist.ended);
2350
2407
  if (years) fields.push({ label: "Active", value: years });
2351
2408
  const links = artistLinks(artist);
2352
- if (links.length > 0) fields.push({ label: "Links", value: /* @__PURE__ */ (0, import_jsx_runtime40.jsx)(ArtistLinks, { links }) });
2409
+ if (links.length > 0) fields.push({ label: "Links", value: /* @__PURE__ */ (0, import_jsx_runtime41.jsx)(ArtistLinks, { links }) });
2353
2410
  if (fields.length === 0) return null;
2354
- return /* @__PURE__ */ (0, import_jsx_runtime40.jsx)(import_ui16.FieldList, { items: fields });
2411
+ return /* @__PURE__ */ (0, import_jsx_runtime41.jsx)(import_ui16.FieldList, { items: fields });
2355
2412
  }
2356
2413
 
2357
2414
  // src/genre-tree/artist/ArtistPhoto.tsx
2358
- var import_jsx_runtime41 = require("react/jsx-runtime");
2359
- function ExternalLink({ href, children }) {
2360
- return /* @__PURE__ */ (0, import_jsx_runtime41.jsx)("a", { href, target: "_blank", rel: "noopener noreferrer", className: "underline-offset-2 hover:underline", children });
2361
- }
2415
+ var import_jsx_runtime42 = require("react/jsx-runtime");
2362
2416
  function ArtistPhoto({ image, name }) {
2363
- return /* @__PURE__ */ (0, import_jsx_runtime41.jsxs)("figure", { className: "flex flex-col gap-1.5", children: [
2364
- /* @__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" }),
2365
- /* @__PURE__ */ (0, import_jsx_runtime41.jsxs)("figcaption", { className: "text-caption text-[var(--color-text-muted)]", children: [
2366
- "Photo: ",
2367
- /* @__PURE__ */ (0, import_jsx_runtime41.jsx)(ExternalLink, { href: image.pageUrl, children: image.author ?? "Wikimedia Commons" }),
2368
- ",",
2369
- " ",
2370
- image.licenseUrl ? /* @__PURE__ */ (0, import_jsx_runtime41.jsx)(ExternalLink, { href: image.licenseUrl, children: image.license }) : image.license
2371
- ] })
2372
- ] });
2417
+ return /* @__PURE__ */ (0, import_jsx_runtime42.jsx)(CommonsPhoto, { image, alt: name });
2373
2418
  }
2374
2419
 
2375
2420
  // src/genre-tree/artist/ArtistMembers.tsx
2376
2421
  var import_ui17 = require("@behindthemusictree/ui");
2377
- var import_jsx_runtime42 = require("react/jsx-runtime");
2422
+ var import_jsx_runtime43 = require("react/jsx-runtime");
2378
2423
  function MembershipList({ title, memberships, renderArtistLink }) {
2379
2424
  if (memberships.length === 0) return null;
2380
- return /* @__PURE__ */ (0, import_jsx_runtime42.jsx)(import_ui17.PanelGroup, { title, children: /* @__PURE__ */ (0, import_jsx_runtime42.jsx)("ul", { children: memberships.map((membership, index) => {
2425
+ return /* @__PURE__ */ (0, import_jsx_runtime43.jsx)(import_ui17.PanelGroup, { title, children: /* @__PURE__ */ (0, import_jsx_runtime43.jsx)("ul", { children: memberships.map((membership, index) => {
2381
2426
  const details = [
2382
2427
  formatYears(membership.beginYear, membership.endYear, membership.ended),
2383
2428
  ...membership.roles
@@ -2385,9 +2430,9 @@ function MembershipList({ title, memberships, renderArtistLink }) {
2385
2430
  const detailsText = details.filter(Boolean).join(" \xB7 ");
2386
2431
  return (
2387
2432
  // A member can have several periods in the same group.
2388
- /* @__PURE__ */ (0, import_jsx_runtime42.jsxs)("li", { className: "truncate py-0.5", title: detailsText || void 0, children: [
2389
- /* @__PURE__ */ (0, import_jsx_runtime42.jsx)("span", { className: "font-medium text-[var(--color-text)]", children: renderArtistLink(membership.artist) }),
2390
- detailsText && /* @__PURE__ */ (0, import_jsx_runtime42.jsxs)("span", { className: "text-[var(--color-text-muted)]", children: [
2433
+ /* @__PURE__ */ (0, import_jsx_runtime43.jsxs)("li", { className: "truncate py-0.5", title: detailsText || void 0, children: [
2434
+ /* @__PURE__ */ (0, import_jsx_runtime43.jsx)("span", { className: "font-medium text-[var(--color-text)]", children: renderArtistLink(membership.artist) }),
2435
+ detailsText && /* @__PURE__ */ (0, import_jsx_runtime43.jsxs)("span", { className: "text-[var(--color-text-muted)]", children: [
2391
2436
  " \u2014 ",
2392
2437
  detailsText
2393
2438
  ] })
@@ -2398,9 +2443,9 @@ function MembershipList({ title, memberships, renderArtistLink }) {
2398
2443
  function ArtistMembers({ title, memberships, renderArtistLink }) {
2399
2444
  if (memberships.length === 0) return null;
2400
2445
  const isCurrent = (membership) => !membership.ended && membership.endYear === null;
2401
- return /* @__PURE__ */ (0, import_jsx_runtime42.jsxs)(import_ui17.PanelSection, { title, children: [
2402
- /* @__PURE__ */ (0, import_jsx_runtime42.jsx)(MembershipList, { title: "Current", memberships: memberships.filter(isCurrent), renderArtistLink }),
2403
- /* @__PURE__ */ (0, import_jsx_runtime42.jsx)(
2446
+ return /* @__PURE__ */ (0, import_jsx_runtime43.jsxs)(import_ui17.PanelSection, { title, children: [
2447
+ /* @__PURE__ */ (0, import_jsx_runtime43.jsx)(MembershipList, { title: "Current", memberships: memberships.filter(isCurrent), renderArtistLink }),
2448
+ /* @__PURE__ */ (0, import_jsx_runtime43.jsx)(
2404
2449
  MembershipList,
2405
2450
  {
2406
2451
  title: "Past",
@@ -2413,7 +2458,7 @@ function ArtistMembers({ title, memberships, renderArtistLink }) {
2413
2458
 
2414
2459
  // src/genre-tree/artist/ArtistDiscography.tsx
2415
2460
  var import_ui18 = require("@behindthemusictree/ui");
2416
- var import_jsx_runtime43 = require("react/jsx-runtime");
2461
+ var import_jsx_runtime44 = require("react/jsx-runtime");
2417
2462
  var GROUPS = ["Albums", "EPs", "Singles", "Live", "Compilations", "Other"];
2418
2463
  var PRIMARY_GROUPS = { album: "Albums", ep: "EPs", single: "Singles" };
2419
2464
  function discographyGroup({ primaryType, secondaryTypes }) {
@@ -2433,12 +2478,12 @@ function byYear(a, b) {
2433
2478
  function ArtistDiscography({ albums }) {
2434
2479
  if (albums.length === 0) return null;
2435
2480
  const sorted = [...albums].sort(byYear);
2436
- return /* @__PURE__ */ (0, import_jsx_runtime43.jsx)(import_ui18.PanelSection, { title: "Discography", children: GROUPS.map((group) => {
2481
+ return /* @__PURE__ */ (0, import_jsx_runtime44.jsx)(import_ui18.PanelSection, { title: "Discography", children: GROUPS.map((group) => {
2437
2482
  const groupAlbums = sorted.filter((album) => discographyGroup(album) === group);
2438
2483
  if (groupAlbums.length === 0) return null;
2439
- 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: [
2440
- /* @__PURE__ */ (0, import_jsx_runtime43.jsx)("span", { className: "tabular-nums text-[var(--color-text-muted)]", children: album.year ?? "\u2014" }),
2441
- /* @__PURE__ */ (0, import_jsx_runtime43.jsx)("span", { className: "truncate text-[var(--color-text)]", title: album.name, children: album.name })
2484
+ 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: [
2485
+ /* @__PURE__ */ (0, import_jsx_runtime44.jsx)("span", { className: "tabular-nums text-[var(--color-text-muted)]", children: album.year ?? "\u2014" }),
2486
+ /* @__PURE__ */ (0, import_jsx_runtime44.jsx)("span", { className: "truncate text-[var(--color-text)]", title: album.name, children: album.name })
2442
2487
  ] }, album.uuid)) }) }, group);
2443
2488
  }) });
2444
2489
  }
@@ -2459,7 +2504,7 @@ var import_genre_tree23 = require("@behindthemusictree/app-kit/genre-tree");
2459
2504
  var import_fa5 = require("react-icons/fa");
2460
2505
  var import_player8 = require("@behindthemusictree/app-kit/player");
2461
2506
  var import_player9 = require("@behindthemusictree/app-kit/player");
2462
- var import_jsx_runtime44 = require("react/jsx-runtime");
2507
+ var import_jsx_runtime45 = require("react/jsx-runtime");
2463
2508
  function TrackPositionPlayPause({
2464
2509
  position,
2465
2510
  uuid,
@@ -2473,14 +2518,14 @@ function TrackPositionPlayPause({
2473
2518
  const isPlaying = isThisTrack && isConfirmedPlayable && playState === import_player9.PlayStates.PLAYING;
2474
2519
  const isPaused = isThisTrack && isConfirmedPlayable && playState === import_player9.PlayStates.PAUSED;
2475
2520
  const isCurrentTrackActive = isPlaying || isPaused;
2476
- return /* @__PURE__ */ (0, import_jsx_runtime44.jsxs)(
2521
+ return /* @__PURE__ */ (0, import_jsx_runtime45.jsxs)(
2477
2522
  "div",
2478
2523
  {
2479
2524
  className: "track-position-play-pause relative flex items-center justify-center gap-2 text-lg w-16 cursor-pointer",
2480
2525
  style: { minWidth: "4rem", maxWidth: "4rem" },
2481
2526
  onClick: handlePlayPauseClick,
2482
2527
  children: [
2483
- /* @__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)(
2528
+ /* @__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)(
2484
2529
  "span",
2485
2530
  {
2486
2531
  className: "track-position-play-pause__bars",
@@ -2488,18 +2533,18 @@ function TrackPositionPlayPause({
2488
2533
  role: "img",
2489
2534
  "aria-label": isPlaying ? "Playing" : "Paused",
2490
2535
  children: [
2491
- /* @__PURE__ */ (0, import_jsx_runtime44.jsx)("span", { className: "track-position-play-pause__bar" }),
2492
- /* @__PURE__ */ (0, import_jsx_runtime44.jsx)("span", { className: "track-position-play-pause__bar" }),
2493
- /* @__PURE__ */ (0, import_jsx_runtime44.jsx)("span", { className: "track-position-play-pause__bar" })
2536
+ /* @__PURE__ */ (0, import_jsx_runtime45.jsx)("span", { className: "track-position-play-pause__bar" }),
2537
+ /* @__PURE__ */ (0, import_jsx_runtime45.jsx)("span", { className: "track-position-play-pause__bar" }),
2538
+ /* @__PURE__ */ (0, import_jsx_runtime45.jsx)("span", { className: "track-position-play-pause__bar" })
2494
2539
  ]
2495
2540
  }
2496
2541
  ) : position }),
2497
- /* @__PURE__ */ (0, import_jsx_runtime44.jsx)(
2542
+ /* @__PURE__ */ (0, import_jsx_runtime45.jsx)(
2498
2543
  "div",
2499
2544
  {
2500
2545
  className: `hidden group-hover:flex items-center justify-center ${isCurrentTrackActive ? "" : "absolute inset-0"}`,
2501
2546
  "aria-hidden": "true",
2502
- 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, {})
2547
+ 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, {})
2503
2548
  }
2504
2549
  )
2505
2550
  ]
@@ -2510,7 +2555,7 @@ function TrackPositionPlayPause({
2510
2555
  // src/genre-tree/track-list-sidebar/TrackItem.tsx
2511
2556
  var import_genre_tree24 = require("@behindthemusictree/app-kit/genre-tree");
2512
2557
  var import_genre_tree25 = require("@behindthemusictree/app-kit/genre-tree");
2513
- var import_jsx_runtime45 = require("react/jsx-runtime");
2558
+ var import_jsx_runtime46 = require("react/jsx-runtime");
2514
2559
  function TrackItem({
2515
2560
  track,
2516
2561
  position,
@@ -2534,12 +2579,12 @@ function TrackItem({
2534
2579
  loadTrackForPlayer(track.uuid);
2535
2580
  }
2536
2581
  };
2537
- return /* @__PURE__ */ (0, import_jsx_runtime45.jsxs)(
2582
+ return /* @__PURE__ */ (0, import_jsx_runtime46.jsxs)(
2538
2583
  "div",
2539
2584
  {
2540
2585
  className: `track-item flex h-14 text-[var(--color-text)] ${isUnplayable ? "opacity-50" : "hover:bg-[var(--color-surface-muted)] group"}`,
2541
2586
  children: [
2542
- isUnplayable ? /* @__PURE__ */ (0, import_jsx_runtime45.jsx)(
2587
+ isUnplayable ? /* @__PURE__ */ (0, import_jsx_runtime46.jsx)(
2543
2588
  "div",
2544
2589
  {
2545
2590
  className: "flex items-center justify-center text-lg w-16",
@@ -2547,7 +2592,7 @@ function TrackItem({
2547
2592
  "aria-disabled": "true",
2548
2593
  children: position
2549
2594
  }
2550
- ) : /* @__PURE__ */ (0, import_jsx_runtime45.jsx)(
2595
+ ) : /* @__PURE__ */ (0, import_jsx_runtime46.jsx)(
2551
2596
  TrackPositionPlayPause,
2552
2597
  {
2553
2598
  position,
@@ -2556,12 +2601,12 @@ function TrackItem({
2556
2601
  isSelected: selectedTrack?.uuid === track.uuid
2557
2602
  }
2558
2603
  ),
2559
- /* @__PURE__ */ (0, import_jsx_runtime45.jsxs)("div", { className: "title-artist-container flex flex-col items-start justify-center min-w-0 flex-1", children: [
2560
- /* @__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 }) }),
2561
- 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 }) }) : ""
2604
+ /* @__PURE__ */ (0, import_jsx_runtime46.jsxs)("div", { className: "title-artist-container flex flex-col items-start justify-center min-w-0 flex-1", children: [
2605
+ /* @__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 }) }),
2606
+ 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 }) }) : ""
2562
2607
  ] }),
2563
- /* @__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 }) : "" }),
2564
- renderDuration && /* @__PURE__ */ (0, import_jsx_runtime45.jsx)(
2608
+ /* @__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 }) : "" }),
2609
+ renderDuration && /* @__PURE__ */ (0, import_jsx_runtime46.jsx)(
2565
2610
  "div",
2566
2611
  {
2567
2612
  className: "duration flex text-base w-16 items-center justify-center",
@@ -2569,14 +2614,14 @@ function TrackItem({
2569
2614
  children: renderDuration(track)
2570
2615
  }
2571
2616
  ),
2572
- 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) })
2617
+ 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) })
2573
2618
  ]
2574
2619
  }
2575
2620
  );
2576
2621
  }
2577
2622
 
2578
2623
  // src/genre-tree/artist/ArtistSongs.tsx
2579
- var import_jsx_runtime46 = require("react/jsx-runtime");
2624
+ var import_jsx_runtime47 = require("react/jsx-runtime");
2580
2625
  var SongPageSchema2 = (0, import_transport8.PaginatedResponseSchema)(import_genre_tree26.SongDetailedSchema);
2581
2626
  var SORTS = [
2582
2627
  { ordering: "title", label: "Title" },
@@ -2609,8 +2654,8 @@ function ArtistSongs({ artist, getBackendBaseUrl, renderArtistLink, renderSongLi
2609
2654
  event.stopPropagation();
2610
2655
  void play(song.uuid);
2611
2656
  };
2612
- 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: [
2613
- /* @__PURE__ */ (0, import_jsx_runtime46.jsx)("div", { className: "flex flex-wrap items-center justify-end gap-2 mb-2", children: /* @__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)(
2657
+ 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: [
2658
+ /* @__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)(
2614
2659
  "button",
2615
2660
  {
2616
2661
  type: "button",
@@ -2624,7 +2669,7 @@ function ArtistSongs({ artist, getBackendBaseUrl, renderArtistLink, renderSongLi
2624
2669
  },
2625
2670
  sort.ordering
2626
2671
  )) }) }),
2627
- /* @__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)(
2672
+ /* @__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)(
2628
2673
  TrackItem,
2629
2674
  {
2630
2675
  track: song,
@@ -2633,12 +2678,12 @@ function ArtistSongs({ artist, getBackendBaseUrl, renderArtistLink, renderSongLi
2633
2678
  renderSongLink
2634
2679
  }
2635
2680
  ) }, song.uuid)) }),
2636
- /* @__PURE__ */ (0, import_jsx_runtime46.jsx)(import_ui19.Pagination, { currentPage: page, totalPages: data.totalPages, onPageChange: setPage })
2681
+ /* @__PURE__ */ (0, import_jsx_runtime47.jsx)(import_ui19.Pagination, { currentPage: page, totalPages: data.totalPages, onPageChange: setPage })
2637
2682
  ] }) });
2638
2683
  }
2639
2684
 
2640
2685
  // src/genre-tree/artist/useArtistPanel.tsx
2641
- var import_jsx_runtime47 = require("react/jsx-runtime");
2686
+ var import_jsx_runtime48 = require("react/jsx-runtime");
2642
2687
  function useArtistPanel({
2643
2688
  uuid,
2644
2689
  scope,
@@ -2646,6 +2691,7 @@ function useArtistPanel({
2646
2691
  getShareUrl,
2647
2692
  renderArtistLink,
2648
2693
  renderSongLink,
2694
+ renderPlaceLink,
2649
2695
  expanded,
2650
2696
  onExpandedChange,
2651
2697
  onClose
@@ -2659,26 +2705,26 @@ function useArtistPanel({
2659
2705
  return {
2660
2706
  ...panel,
2661
2707
  title: "Artist",
2662
- content: /* @__PURE__ */ (0, import_jsx_runtime47.jsxs)("div", { "aria-busy": "true", className: "flex flex-col gap-2.5", children: [
2663
- /* @__PURE__ */ (0, import_jsx_runtime47.jsx)(import_ui20.Skeleton, { className: "h-3 w-16" }),
2664
- /* @__PURE__ */ (0, import_jsx_runtime47.jsx)(import_ui20.Skeleton, { className: "h-4 w-32" }),
2665
- /* @__PURE__ */ (0, import_jsx_runtime47.jsx)(import_ui20.Skeleton, { className: "h-3 w-16" }),
2666
- /* @__PURE__ */ (0, import_jsx_runtime47.jsx)(import_ui20.Skeleton, { className: "h-4 w-24" }),
2667
- /* @__PURE__ */ (0, import_jsx_runtime47.jsx)(import_ui20.Skeleton, { className: "h-16 mt-2.5" }),
2668
- /* @__PURE__ */ (0, import_jsx_runtime47.jsx)(import_ui20.Skeleton, { className: "h-16" })
2708
+ content: /* @__PURE__ */ (0, import_jsx_runtime48.jsxs)("div", { "aria-busy": "true", className: "flex flex-col gap-2.5", children: [
2709
+ /* @__PURE__ */ (0, import_jsx_runtime48.jsx)(import_ui20.Skeleton, { className: "h-3 w-16" }),
2710
+ /* @__PURE__ */ (0, import_jsx_runtime48.jsx)(import_ui20.Skeleton, { className: "h-4 w-32" }),
2711
+ /* @__PURE__ */ (0, import_jsx_runtime48.jsx)(import_ui20.Skeleton, { className: "h-3 w-16" }),
2712
+ /* @__PURE__ */ (0, import_jsx_runtime48.jsx)(import_ui20.Skeleton, { className: "h-4 w-24" }),
2713
+ /* @__PURE__ */ (0, import_jsx_runtime48.jsx)(import_ui20.Skeleton, { className: "h-16 mt-2.5" }),
2714
+ /* @__PURE__ */ (0, import_jsx_runtime48.jsx)(import_ui20.Skeleton, { className: "h-16" })
2669
2715
  ] })
2670
2716
  };
2671
2717
  case "not-found":
2672
2718
  return {
2673
2719
  ...panel,
2674
2720
  title: "Artist not found",
2675
- content: /* @__PURE__ */ (0, import_jsx_runtime47.jsx)("p", { className: "text-[var(--color-text-muted)]", children: "This artist does not exist." })
2721
+ content: /* @__PURE__ */ (0, import_jsx_runtime48.jsx)("p", { className: "text-[var(--color-text-muted)]", children: "This artist does not exist." })
2676
2722
  };
2677
2723
  case "error":
2678
2724
  return {
2679
2725
  ...panel,
2680
2726
  title: "Artist",
2681
- content: /* @__PURE__ */ (0, import_jsx_runtime47.jsx)("p", { className: "text-red-600", children: "Could not load this artist." })
2727
+ content: /* @__PURE__ */ (0, import_jsx_runtime48.jsx)("p", { className: "text-red-600", children: "Could not load this artist." })
2682
2728
  };
2683
2729
  case "success": {
2684
2730
  const { artist } = result;
@@ -2687,19 +2733,19 @@ function useArtistPanel({
2687
2733
  return {
2688
2734
  ...panel,
2689
2735
  title: artist.name,
2690
- content: /* @__PURE__ */ (0, import_jsx_runtime47.jsxs)(import_jsx_runtime47.Fragment, { children: [
2691
- /* @__PURE__ */ (0, import_jsx_runtime47.jsx)(
2736
+ content: /* @__PURE__ */ (0, import_jsx_runtime48.jsxs)(import_jsx_runtime48.Fragment, { children: [
2737
+ /* @__PURE__ */ (0, import_jsx_runtime48.jsx)(
2692
2738
  ArtistActionBand,
2693
2739
  {
2694
2740
  artist,
2695
- share: shareTarget && shareUrl ? /* @__PURE__ */ (0, import_jsx_runtime47.jsx)(ShareActionButton, { target: shareTarget, url: shareUrl }) : void 0
2741
+ share: shareTarget && shareUrl ? /* @__PURE__ */ (0, import_jsx_runtime48.jsx)(ShareActionButton, { target: shareTarget, url: shareUrl }) : void 0
2696
2742
  }
2697
2743
  ),
2698
- artist.image && /* @__PURE__ */ (0, import_jsx_runtime47.jsx)(ArtistPhoto, { image: artist.image, name: artist.name }),
2699
- /* @__PURE__ */ (0, import_jsx_runtime47.jsx)(ArtistHeader, { artist }),
2700
- /* @__PURE__ */ (0, import_jsx_runtime47.jsx)(ArtistMembers, { title: "Members", memberships: artist.members, renderArtistLink }),
2701
- /* @__PURE__ */ (0, import_jsx_runtime47.jsx)(ArtistMembers, { title: "Member of", memberships: artist.memberOf, renderArtistLink }),
2702
- artist.tracksCount > 0 && /* @__PURE__ */ (0, import_jsx_runtime47.jsx)(
2744
+ artist.image && /* @__PURE__ */ (0, import_jsx_runtime48.jsx)(ArtistPhoto, { image: artist.image, name: artist.name }),
2745
+ /* @__PURE__ */ (0, import_jsx_runtime48.jsx)(ArtistHeader, { artist, renderPlaceLink }),
2746
+ /* @__PURE__ */ (0, import_jsx_runtime48.jsx)(ArtistMembers, { title: "Members", memberships: artist.members, renderArtistLink }),
2747
+ /* @__PURE__ */ (0, import_jsx_runtime48.jsx)(ArtistMembers, { title: "Member of", memberships: artist.memberOf, renderArtistLink }),
2748
+ artist.tracksCount > 0 && /* @__PURE__ */ (0, import_jsx_runtime48.jsx)(
2703
2749
  ArtistSongs,
2704
2750
  {
2705
2751
  artist: { uuid: artist.uuid, name: artist.name },
@@ -2709,7 +2755,7 @@ function useArtistPanel({
2709
2755
  }
2710
2756
  )
2711
2757
  ] }),
2712
- extraContent: artist.albums.length > 0 ? /* @__PURE__ */ (0, import_jsx_runtime47.jsx)(ArtistDiscography, { albums: artist.albums }) : void 0
2758
+ extraContent: artist.albums.length > 0 ? /* @__PURE__ */ (0, import_jsx_runtime48.jsx)(ArtistDiscography, { albums: artist.albums }) : void 0
2713
2759
  };
2714
2760
  }
2715
2761
  }
@@ -2725,7 +2771,7 @@ var import_lucide_react16 = require("lucide-react");
2725
2771
  var import_ui21 = require("@behindthemusictree/ui");
2726
2772
  var import_player12 = require("@behindthemusictree/app-kit/player");
2727
2773
  var import_genre_tree28 = require("@behindthemusictree/app-kit/genre-tree");
2728
- var import_jsx_runtime48 = require("react/jsx-runtime");
2774
+ var import_jsx_runtime49 = require("react/jsx-runtime");
2729
2775
  var numberFormat = new Intl.NumberFormat("en");
2730
2776
  var dateFormat = new Intl.DateTimeFormat("en", { dateStyle: "medium" });
2731
2777
  var externalLink = "text-[var(--color-primary)] hover:underline focus-visible:underline";
@@ -2733,27 +2779,27 @@ function SongHistory({ uuid, getBackendBaseUrl }) {
2733
2779
  const [open, setOpen] = (0, import_react18.useState)(false);
2734
2780
  const history = (0, import_genre_tree28.useSongHistory)(uuid, getBackendBaseUrl, open);
2735
2781
  let body = null;
2736
- if (history.status === "loading") body = /* @__PURE__ */ (0, import_jsx_runtime48.jsx)(import_ui21.Skeleton, { className: "h-10", "aria-busy": "true" });
2737
- else if (history.status === "error") body = /* @__PURE__ */ (0, import_jsx_runtime48.jsx)("p", { className: "text-red-600", children: "Could not load the history." });
2782
+ if (history.status === "loading") body = /* @__PURE__ */ (0, import_jsx_runtime49.jsx)(import_ui21.Skeleton, { className: "h-10", "aria-busy": "true" });
2783
+ else if (history.status === "error") body = /* @__PURE__ */ (0, import_jsx_runtime49.jsx)("p", { className: "text-red-600", children: "Could not load the history." });
2738
2784
  else if (history.status === "success")
2739
- 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: [
2740
- /* @__PURE__ */ (0, import_jsx_runtime48.jsx)("span", { className: "font-medium", children: entry.action }),
2741
- entry.oldValue !== null || entry.newValue !== null ? /* @__PURE__ */ (0, import_jsx_runtime48.jsxs)("span", { className: "text-[var(--color-text-muted)]", children: [
2785
+ 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: [
2786
+ /* @__PURE__ */ (0, import_jsx_runtime49.jsx)("span", { className: "font-medium", children: entry.action }),
2787
+ entry.oldValue !== null || entry.newValue !== null ? /* @__PURE__ */ (0, import_jsx_runtime49.jsxs)("span", { className: "text-[var(--color-text-muted)]", children: [
2742
2788
  " ",
2743
2789
  "\u2014 ",
2744
2790
  entry.oldValue ?? "none",
2745
2791
  " \u2192 ",
2746
2792
  entry.newValue ?? "none"
2747
2793
  ] }) : null,
2748
- /* @__PURE__ */ (0, import_jsx_runtime48.jsxs)("span", { className: "block text-caption text-[var(--color-text-muted)]", children: [
2794
+ /* @__PURE__ */ (0, import_jsx_runtime49.jsxs)("span", { className: "block text-caption text-[var(--color-text-muted)]", children: [
2749
2795
  entry.actorPseudo ?? "Anonymous",
2750
2796
  " \xB7 ",
2751
2797
  dateFormat.format(new Date(entry.createdOn))
2752
2798
  ] })
2753
2799
  ] }, entry.uuid)) });
2754
- 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: [
2755
- /* @__PURE__ */ (0, import_jsx_runtime48.jsx)("summary", { className: `mt-2.5 cursor-pointer ${import_ui21.PANEL_LABEL_CLASS}`, children: "History" }),
2756
- /* @__PURE__ */ (0, import_jsx_runtime48.jsx)("div", { className: "mt-1.5", children: body })
2800
+ 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: [
2801
+ /* @__PURE__ */ (0, import_jsx_runtime49.jsx)("summary", { className: `mt-2.5 cursor-pointer ${import_ui21.PANEL_LABEL_CLASS}`, children: "History" }),
2802
+ /* @__PURE__ */ (0, import_jsx_runtime49.jsx)("div", { className: "mt-1.5", children: body })
2757
2803
  ] }) });
2758
2804
  }
2759
2805
  function SongPanelContent(props) {
@@ -2772,13 +2818,13 @@ function SongPanelContent(props) {
2772
2818
  const genre = song.genre;
2773
2819
  fields.push({
2774
2820
  label: "Genre",
2775
- 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: [
2776
- index > 0 && /* @__PURE__ */ (0, import_jsx_runtime48.jsx)("span", { "aria-hidden": "true", children: "\u203A " }),
2777
- /* @__PURE__ */ (0, import_jsx_runtime48.jsx)("button", { type: "button", onClick: () => props.onSelectGenrePlaylist?.(genrePlaylist), className: "hover:underline", children: genrePlaylist.name })
2821
+ 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: [
2822
+ index > 0 && /* @__PURE__ */ (0, import_jsx_runtime49.jsx)("span", { "aria-hidden": "true", children: "\u203A " }),
2823
+ /* @__PURE__ */ (0, import_jsx_runtime49.jsx)("button", { type: "button", onClick: () => props.onSelectGenrePlaylist?.(genrePlaylist), className: "hover:underline", children: genrePlaylist.name })
2778
2824
  ] }, genrePlaylist.uuid)) }) : song.genre.name
2779
2825
  });
2780
2826
  } else {
2781
- fields.push({ label: "Genre", value: /* @__PURE__ */ (0, import_jsx_runtime48.jsx)("span", { className: "text-[var(--color-text-muted)]", children: "No genre" }) });
2827
+ fields.push({ label: "Genre", value: /* @__PURE__ */ (0, import_jsx_runtime49.jsx)("span", { className: "text-[var(--color-text-muted)]", children: "No genre" }) });
2782
2828
  }
2783
2829
  if (song.firstReleaseYear) fields.push({ label: "First released", value: song.firstReleaseYear });
2784
2830
  const viewCount = song.youtubeVideo?.viewCount;
@@ -2788,11 +2834,11 @@ function SongPanelContent(props) {
2788
2834
  const links = [];
2789
2835
  if (musicBrainzUrl)
2790
2836
  links.push(
2791
- /* @__PURE__ */ (0, import_jsx_runtime48.jsx)("a", { href: musicBrainzUrl, target: "_blank", rel: "noopener noreferrer", className: externalLink, children: "MusicBrainz" }, "mb")
2837
+ /* @__PURE__ */ (0, import_jsx_runtime49.jsx)("a", { href: musicBrainzUrl, target: "_blank", rel: "noopener noreferrer", className: externalLink, children: "MusicBrainz" }, "mb")
2792
2838
  );
2793
2839
  if (song.youtubeVideo)
2794
2840
  links.push(
2795
- /* @__PURE__ */ (0, import_jsx_runtime48.jsx)(
2841
+ /* @__PURE__ */ (0, import_jsx_runtime49.jsx)(
2796
2842
  "a",
2797
2843
  {
2798
2844
  href: `https://www.youtube.com/watch?v=${encodeURIComponent(song.youtubeVideo.id)}`,
@@ -2807,31 +2853,31 @@ function SongPanelContent(props) {
2807
2853
  if (links.length > 0)
2808
2854
  fields.push({
2809
2855
  label: "Links",
2810
- value: /* @__PURE__ */ (0, import_jsx_runtime48.jsx)("span", { className: "flex gap-3", children: links })
2856
+ value: /* @__PURE__ */ (0, import_jsx_runtime49.jsx)("span", { className: "flex gap-3", children: links })
2811
2857
  });
2812
- return /* @__PURE__ */ (0, import_jsx_runtime48.jsxs)(import_jsx_runtime48.Fragment, { children: [
2813
- /* @__PURE__ */ (0, import_jsx_runtime48.jsx)(import_ui21.PanelBleed, { children: /* @__PURE__ */ (0, import_jsx_runtime48.jsxs)(import_ui21.ActionBand, { children: [
2814
- /* @__PURE__ */ (0, import_jsx_runtime48.jsx)(
2858
+ return /* @__PURE__ */ (0, import_jsx_runtime49.jsxs)(import_jsx_runtime49.Fragment, { children: [
2859
+ /* @__PURE__ */ (0, import_jsx_runtime49.jsx)(import_ui21.PanelBleed, { children: /* @__PURE__ */ (0, import_jsx_runtime49.jsxs)(import_ui21.ActionBand, { children: [
2860
+ /* @__PURE__ */ (0, import_jsx_runtime49.jsx)(
2815
2861
  import_ui21.ActionButton,
2816
2862
  {
2817
- icon: isPlaying ? /* @__PURE__ */ (0, import_jsx_runtime48.jsx)(import_lucide_react16.Pause, { "aria-hidden": "true", className: "w-5 h-5" }) : /* @__PURE__ */ (0, import_jsx_runtime48.jsx)(import_lucide_react16.Play, { "aria-hidden": "true", className: "w-5 h-5" }),
2863
+ 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" }),
2818
2864
  label: isPlaying ? "Pause" : "Play",
2819
2865
  disabled: !!unplayableReason,
2820
2866
  title: unplayableReason ?? void 0,
2821
2867
  onClick: () => isCurrent ? handlePlayPauseAction() : playNewTrackListFromTrackUuid(song, scope)
2822
2868
  }
2823
2869
  ),
2824
- shareUrl && /* @__PURE__ */ (0, import_jsx_runtime48.jsx)(ShareActionButton, { target: shareTarget, url: shareUrl })
2870
+ shareUrl && /* @__PURE__ */ (0, import_jsx_runtime49.jsx)(ShareActionButton, { target: shareTarget, url: shareUrl })
2825
2871
  ] }) }),
2826
- 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 }) }),
2827
- /* @__PURE__ */ (0, import_jsx_runtime48.jsx)(import_ui21.FieldList, { items: fields }),
2828
- 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)) }) }),
2829
- /* @__PURE__ */ (0, import_jsx_runtime48.jsx)(SongHistory, { uuid: song.uuid, getBackendBaseUrl })
2872
+ 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 }) }),
2873
+ /* @__PURE__ */ (0, import_jsx_runtime49.jsx)(import_ui21.FieldList, { items: fields }),
2874
+ 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)) }) }),
2875
+ /* @__PURE__ */ (0, import_jsx_runtime49.jsx)(SongHistory, { uuid: song.uuid, getBackendBaseUrl })
2830
2876
  ] });
2831
2877
  }
2832
2878
 
2833
2879
  // src/genre-tree/song/useSongPanel.tsx
2834
- var import_jsx_runtime49 = require("react/jsx-runtime");
2880
+ var import_jsx_runtime50 = require("react/jsx-runtime");
2835
2881
  function useSongPanel({
2836
2882
  uuid,
2837
2883
  scope,
@@ -2852,24 +2898,24 @@ function useSongPanel({
2852
2898
  return {
2853
2899
  ...panel,
2854
2900
  title: "Song",
2855
- content: /* @__PURE__ */ (0, import_jsx_runtime49.jsxs)("div", { "aria-busy": "true", className: "flex flex-col gap-2.5", children: [
2856
- /* @__PURE__ */ (0, import_jsx_runtime49.jsx)(import_ui22.Skeleton, { className: "h-3 w-16" }),
2857
- /* @__PURE__ */ (0, import_jsx_runtime49.jsx)(import_ui22.Skeleton, { className: "h-4 w-32" }),
2858
- /* @__PURE__ */ (0, import_jsx_runtime49.jsx)(import_ui22.Skeleton, { className: "h-3 w-16" }),
2859
- /* @__PURE__ */ (0, import_jsx_runtime49.jsx)(import_ui22.Skeleton, { className: "h-4 w-24" })
2901
+ content: /* @__PURE__ */ (0, import_jsx_runtime50.jsxs)("div", { "aria-busy": "true", className: "flex flex-col gap-2.5", children: [
2902
+ /* @__PURE__ */ (0, import_jsx_runtime50.jsx)(import_ui22.Skeleton, { className: "h-3 w-16" }),
2903
+ /* @__PURE__ */ (0, import_jsx_runtime50.jsx)(import_ui22.Skeleton, { className: "h-4 w-32" }),
2904
+ /* @__PURE__ */ (0, import_jsx_runtime50.jsx)(import_ui22.Skeleton, { className: "h-3 w-16" }),
2905
+ /* @__PURE__ */ (0, import_jsx_runtime50.jsx)(import_ui22.Skeleton, { className: "h-4 w-24" })
2860
2906
  ] })
2861
2907
  };
2862
2908
  case "not-found":
2863
2909
  return {
2864
2910
  ...panel,
2865
2911
  title: "Song not found",
2866
- content: /* @__PURE__ */ (0, import_jsx_runtime49.jsx)("p", { className: "text-[var(--color-text-muted)]", children: "This song does not exist." })
2912
+ content: /* @__PURE__ */ (0, import_jsx_runtime50.jsx)("p", { className: "text-[var(--color-text-muted)]", children: "This song does not exist." })
2867
2913
  };
2868
2914
  case "error":
2869
2915
  return {
2870
2916
  ...panel,
2871
2917
  title: "Song",
2872
- content: /* @__PURE__ */ (0, import_jsx_runtime49.jsx)("p", { className: "text-red-600", children: "Could not load this song." })
2918
+ content: /* @__PURE__ */ (0, import_jsx_runtime50.jsx)("p", { className: "text-red-600", children: "Could not load this song." })
2873
2919
  };
2874
2920
  case "success": {
2875
2921
  const { song } = result;
@@ -2877,7 +2923,7 @@ function useSongPanel({
2877
2923
  return {
2878
2924
  ...panel,
2879
2925
  title: song.title,
2880
- content: /* @__PURE__ */ (0, import_jsx_runtime49.jsx)(
2926
+ content: /* @__PURE__ */ (0, import_jsx_runtime50.jsx)(
2881
2927
  SongPanelContent,
2882
2928
  {
2883
2929
  song,
@@ -2904,6 +2950,7 @@ function useEntityPanel({
2904
2950
  getShareUrl,
2905
2951
  renderArtistLink,
2906
2952
  renderSongLink,
2953
+ renderPlaceLink,
2907
2954
  song,
2908
2955
  artist,
2909
2956
  ...navigation
@@ -2922,7 +2969,8 @@ function useEntityPanel({
2922
2969
  getBackendBaseUrl: getArtistBackendBaseUrl ?? getBackendBaseUrl,
2923
2970
  getShareUrl,
2924
2971
  renderArtistLink,
2925
- renderSongLink
2972
+ renderSongLink,
2973
+ renderPlaceLink
2926
2974
  });
2927
2975
  return songPanel ?? artistPanel;
2928
2976
  }
@@ -2930,12 +2978,12 @@ function useEntityPanel({
2930
2978
  // src/genre-tree/SizeMetricToggle.tsx
2931
2979
  var import_md2 = require("react-icons/md");
2932
2980
  var import_ui23 = require("@behindthemusictree/ui");
2933
- var import_jsx_runtime50 = require("react/jsx-runtime");
2981
+ var import_jsx_runtime51 = require("react/jsx-runtime");
2934
2982
  function SizeMetricToggle({
2935
2983
  value,
2936
2984
  onChange
2937
2985
  }) {
2938
- return /* @__PURE__ */ (0, import_jsx_runtime50.jsx)(
2986
+ return /* @__PURE__ */ (0, import_jsx_runtime51.jsx)(
2939
2987
  import_ui23.SegmentedControl,
2940
2988
  {
2941
2989
  variant: "floating",
@@ -2943,15 +2991,15 @@ function SizeMetricToggle({
2943
2991
  value,
2944
2992
  onChange,
2945
2993
  options: [
2946
- { 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" },
2947
- { 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" }
2994
+ { 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" },
2995
+ { 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" }
2948
2996
  ]
2949
2997
  }
2950
2998
  );
2951
2999
  }
2952
3000
 
2953
3001
  // src/genre-tree/GenreTreeView.tsx
2954
- var import_jsx_runtime51 = require("react/jsx-runtime");
3002
+ var import_jsx_runtime52 = require("react/jsx-runtime");
2955
3003
  var NO_POP_ROOT_TITLE = "This genre tree has no 'Mainstream Pop' root yet";
2956
3004
  var REGIONS_CLASS = "[--gtv-center-left:var(--layout-center-left)] [--gtv-center-right:var(--layout-center-right)] [--gtv-center-top:var(--layout-band-top)]";
2957
3005
  var noop = () => {
@@ -2999,6 +3047,7 @@ function GenreTreeView({
2999
3047
  songSearch = true,
3000
3048
  onSongSelect,
3001
3049
  renderSongLink,
3050
+ renderPlaceLink,
3002
3051
  renderSummary,
3003
3052
  renderEssentialTracks,
3004
3053
  renderSongLeading,
@@ -3059,7 +3108,7 @@ function GenreTreeView({
3059
3108
  allowWheelRotation: internalAllowWheelRotation,
3060
3109
  toggleWheelRotation,
3061
3110
  onNodeHover: handleNodeHover
3062
- } = (0, import_genre_tree30.useGenreTreeViewState)({
3111
+ } = (0, import_genre_tree31.useGenreTreeViewState)({
3063
3112
  scope,
3064
3113
  getBackendBaseUrl,
3065
3114
  viewMode: controlledViewMode,
@@ -3078,12 +3127,12 @@ function GenreTreeView({
3078
3127
  restoreRootUuid = selectedGenrePlaylist.root.uuid;
3079
3128
  setStackedRestoreOwner({ revision: restoreRevision, rootUuid: restoreRootUuid });
3080
3129
  }
3081
- const isGenreUpdating = (0, import_react_query3.useIsMutating)({ mutationKey: import_genre_tree30.genreUpdateMutationKey }) > 0;
3082
- const { mutate: updateGenre } = (0, import_genre_tree30.useUpdateGenre)(scope, getBackendBaseUrl);
3130
+ const isGenreUpdating = (0, import_react_query3.useIsMutating)({ mutationKey: import_genre_tree31.genreUpdateMutationKey }) > 0;
3131
+ const { mutate: updateGenre } = (0, import_genre_tree31.useUpdateGenre)(scope, getBackendBaseUrl);
3083
3132
  const busy = deferredSizeMetric !== sizeMetric || isTreeFetching || isGenreUpdating;
3084
3133
  const isArtistFiltered = artistUuid !== null;
3085
3134
  const canSizeByViews = !isArtistFiltered && hasViewsCount(visibleGenrePlaylists);
3086
- const artistResult = (0, import_genre_tree30.useArtist)(artistUuid, artistSearchBaseUrl ?? getBackendBaseUrl);
3135
+ const artistResult = (0, import_genre_tree31.useArtist)(artistUuid, artistSearchBaseUrl ?? getBackendBaseUrl);
3087
3136
  const filterArtistName = artistResult.status === "success" ? artistResult.artist.name : null;
3088
3137
  const filterArtist = (0, import_react19.useMemo)(
3089
3138
  () => artistUuid !== null && filterArtistName !== null ? { uuid: artistUuid, name: filterArtistName } : null,
@@ -3103,14 +3152,14 @@ function GenreTreeView({
3103
3152
  const openSongRef = (0, import_react19.useRef)(openSong);
3104
3153
  openSongRef.current = openSong;
3105
3154
  const resolvedRenderSongLink = (0, import_react19.useCallback)(
3106
- (song) => renderSongLink ? renderSongLink(song) : /* @__PURE__ */ (0, import_jsx_runtime51.jsx)("button", { type: "button", onClick: () => openSongRef.current(song.uuid), className: "hover:underline", children: song.title }),
3155
+ (song) => renderSongLink ? renderSongLink(song) : /* @__PURE__ */ (0, import_jsx_runtime52.jsx)("button", { type: "button", onClick: () => openSongRef.current(song.uuid), className: "hover:underline", children: song.title }),
3107
3156
  [renderSongLink]
3108
3157
  );
3109
3158
  const [expandedSongUuid, setExpandedSongUuid] = (0, import_react19.useState)(null);
3110
3159
  const [expandedArtistUuid, setExpandedArtistUuid] = (0, import_react19.useState)(null);
3111
3160
  const artistBaseUrl = artistSearchBaseUrl ?? getBackendBaseUrl;
3112
3161
  const songBaseUrl = getBackendBaseUrl;
3113
- 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 }));
3162
+ 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 }));
3114
3163
  const getGenrePath = (genre) => {
3115
3164
  const path = [];
3116
3165
  let current = visibleGenrePlaylists.find((playlist) => playlist.criteria?.uuid === genre.uuid);
@@ -3128,6 +3177,7 @@ function GenreTreeView({
3128
3177
  getShareUrl: getShareUrlRef.current,
3129
3178
  renderArtistLink: resolvedRenderArtistLink,
3130
3179
  renderSongLink: resolvedRenderSongLink,
3180
+ renderPlaceLink,
3131
3181
  song: {
3132
3182
  uuid: songUuid,
3133
3183
  expanded: controlledSongExpanded ?? (songUuid !== null && expandedSongUuid === songUuid),
@@ -3187,9 +3237,10 @@ function GenreTreeView({
3187
3237
  resolveStyle
3188
3238
  );
3189
3239
  const treeStyles = (0, import_react19.useMemo)(() => toGenreNodeStyles(relations, visibleGenrePlaylists), [relations, visibleGenrePlaylists]);
3190
- const [selectionHistory, setSelectionHistory] = (0, import_react19.useState)({ current: selectedGenreUuid, previous: null });
3191
- if (selectionHistory.current !== selectedGenreUuid) {
3192
- setSelectionHistory({ current: selectedGenreUuid, previous: selectionHistory.current ?? selectionHistory.previous });
3240
+ const validSelectedGenreUuid = (0, import_genre_tree30.toGenreUuid)(selectedGenreUuid);
3241
+ const [selectionHistory, setSelectionHistory] = (0, import_react19.useState)({ current: validSelectedGenreUuid, previous: null });
3242
+ if (selectionHistory.current !== validSelectedGenreUuid) {
3243
+ setSelectionHistory({ current: validSelectedGenreUuid, previous: selectionHistory.current ?? selectionHistory.previous });
3193
3244
  }
3194
3245
  const handlePrevious = selectionHistory.previous ? () => selectGenre(selectionHistory.previous) : void 0;
3195
3246
  const { showPopup, hidePopup } = (0, import_popup5.usePopup)();
@@ -3221,7 +3272,7 @@ function GenreTreeView({
3221
3272
  const canToggleSide = !readOnly && criteria !== void 0 && criteria !== null && parentPlaylist !== void 0 && parentPlaylist.parent === null;
3222
3273
  const side = criteria?.side ?? "core";
3223
3274
  const popSibling = canToggleSide && side === "core" ? visibleGenrePlaylists.find((sibling) => sibling.parent?.uuid === parentPlaylist.uuid && sibling.uuid !== genrePlaylist?.uuid && sibling.criteria?.side === "pop") : void 0;
3224
- const shareTarget = selectedGenreUuid ? { kind: "genre", genreUuid: selectedGenreUuid, name: criteria?.name ?? "Genre", scope, artistUuid } : void 0;
3275
+ const shareTarget = validSelectedGenreUuid ? { kind: "genre", genreUuid: validSelectedGenreUuid, name: criteria?.name ?? "Genre", scope, artistUuid } : void 0;
3225
3276
  const shareUrl = shareTarget ? getShareUrlRef.current?.(shareTarget) ?? void 0 : void 0;
3226
3277
  const genreContent = useGenreDetailContent({
3227
3278
  genreUuid: selectedGenreUuid,
@@ -3243,7 +3294,7 @@ function GenreTreeView({
3243
3294
  onAddChild: readOnly || !criteria ? void 0 : () => handleGenreCreationAction(criteria),
3244
3295
  onRename: readOnly || !criteria ? void 0 : () => handleGenreRenameAction(criteria),
3245
3296
  onChangeParent: readOnly || !genrePlaylist || !criteria ? void 0 : () => showPopup(
3246
- /* @__PURE__ */ (0, import_jsx_runtime51.jsx)(
3297
+ /* @__PURE__ */ (0, import_jsx_runtime52.jsx)(
3247
3298
  ChangeParentPopup,
3248
3299
  {
3249
3300
  genrePlaylist: { ...genrePlaylist, criteria },
@@ -3287,7 +3338,7 @@ function GenreTreeView({
3287
3338
  onClose: otherPanel.onClose,
3288
3339
  searchName: otherPanel.title
3289
3340
  };
3290
- } else if (selectedGenreUuid) {
3341
+ } else if (selectedGenreUuid !== null) {
3291
3342
  activePanel = {
3292
3343
  title: genreContent.title,
3293
3344
  content: genreContent.content,
@@ -3304,19 +3355,19 @@ function GenreTreeView({
3304
3355
  edge: activePanel.expanded ? { label: "Shrink panel", onClick: () => activePanel.onExpandedChange?.(false) } : isCollapsed ? null : { label: "Hide panel", onClick: () => changeCollapsed(true) },
3305
3356
  center: activePanel.expanded ? null : isCollapsed ? { label: "Show panel", onClick: () => changeCollapsed(false) } : activePanel.onExpandedChange ? { label: "Extend panel", onClick: () => activePanel.onExpandedChange?.(true) } : null
3306
3357
  } : void 0;
3307
- const expandButton = placement === "default" && activePanel?.onExpandedChange ? /* @__PURE__ */ (0, import_jsx_runtime51.jsx)(
3358
+ const expandButton = placement === "default" && activePanel?.onExpandedChange ? /* @__PURE__ */ (0, import_jsx_runtime52.jsx)(
3308
3359
  import_ui24.IconButton,
3309
3360
  {
3310
3361
  "aria-label": activePanel.expanded ? "Shrink panel" : "Extend panel",
3311
3362
  style: activePanel.headerAccent && { color: "inherit" },
3312
3363
  onClick: () => activePanel.onExpandedChange?.(!activePanel.expanded),
3313
- children: activePanel.expanded ? /* @__PURE__ */ (0, import_jsx_runtime51.jsx)(import_lucide_react18.Minimize2, { "aria-hidden": "true", className: "h-4 w-4" }) : /* @__PURE__ */ (0, import_jsx_runtime51.jsx)(import_lucide_react18.Maximize2, { "aria-hidden": "true", className: "h-4 w-4" })
3364
+ 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" })
3314
3365
  }
3315
3366
  ) : null;
3316
3367
  const renderArtistFilter = () => {
3317
3368
  if (isLoading || !filterArtist) return null;
3318
- 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: [
3319
- /* @__PURE__ */ (0, import_jsx_runtime51.jsx)(
3369
+ 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: [
3370
+ /* @__PURE__ */ (0, import_jsx_runtime52.jsx)(
3320
3371
  "button",
3321
3372
  {
3322
3373
  type: "button",
@@ -3328,7 +3379,7 @@ function GenreTreeView({
3328
3379
  children: filterArtist.name
3329
3380
  }
3330
3381
  ),
3331
- /* @__PURE__ */ (0, import_jsx_runtime51.jsx)(
3382
+ /* @__PURE__ */ (0, import_jsx_runtime52.jsx)(
3332
3383
  "button",
3333
3384
  {
3334
3385
  type: "button",
@@ -3340,8 +3391,8 @@ function GenreTreeView({
3340
3391
  )
3341
3392
  ] });
3342
3393
  };
3343
- const actions = /* @__PURE__ */ (0, import_jsx_runtime51.jsxs)(import_jsx_runtime51.Fragment, { children: [
3344
- !isLoading && !isError && /* @__PURE__ */ (0, import_jsx_runtime51.jsx)(
3394
+ const actions = /* @__PURE__ */ (0, import_jsx_runtime52.jsxs)(import_jsx_runtime52.Fragment, { children: [
3395
+ !isLoading && !isError && /* @__PURE__ */ (0, import_jsx_runtime52.jsx)(
3345
3396
  GenreTreeSearch,
3346
3397
  {
3347
3398
  artistBaseUrl,
@@ -3374,9 +3425,9 @@ function GenreTreeView({
3374
3425
  extraGroups: extraSearchGroups
3375
3426
  }
3376
3427
  ),
3377
- !isLoading && !isError && filterArtist && /* @__PURE__ */ (0, import_jsx_runtime51.jsx)("div", { className: "hidden w-full justify-center max-md:flex", children: renderArtistFilter() }),
3378
- /* @__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: [
3379
- !isLoading && !isError && !isControlled && /* @__PURE__ */ (0, import_jsx_runtime51.jsx)(
3428
+ !isLoading && !isError && filterArtist && /* @__PURE__ */ (0, import_jsx_runtime52.jsx)("div", { className: "hidden w-full justify-center max-md:flex", children: renderArtistFilter() }),
3429
+ /* @__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: [
3430
+ !isLoading && !isError && !isControlled && /* @__PURE__ */ (0, import_jsx_runtime52.jsx)(
3380
3431
  import_ui24.SegmentedControl,
3381
3432
  {
3382
3433
  "aria-label": "Tree view mode",
@@ -3390,13 +3441,13 @@ function GenreTreeView({
3390
3441
  ]
3391
3442
  }
3392
3443
  ),
3393
- !isLoading && !isError && showDisplayOptions && /* @__PURE__ */ (0, import_jsx_runtime51.jsx)(
3444
+ !isLoading && !isError && showDisplayOptions && /* @__PURE__ */ (0, import_jsx_runtime52.jsx)(
3394
3445
  "div",
3395
3446
  {
3396
3447
  className: "flex items-center gap-1",
3397
3448
  role: "group",
3398
3449
  "aria-label": "Tree display options",
3399
- children: !isRotationControlled && viewMode !== "stacked" && viewMode !== "outline" && /* @__PURE__ */ (0, import_jsx_runtime51.jsx)(
3450
+ children: !isRotationControlled && viewMode !== "stacked" && viewMode !== "outline" && /* @__PURE__ */ (0, import_jsx_runtime52.jsx)(
3400
3451
  import_ui24.Button,
3401
3452
  {
3402
3453
  variant: allowWheelRotation ? "default" : "outline",
@@ -3407,7 +3458,7 @@ function GenreTreeView({
3407
3458
  )
3408
3459
  }
3409
3460
  ),
3410
- !isLoading && !isError && !readOnly && showAddRoot && /* @__PURE__ */ (0, import_jsx_runtime51.jsx)(
3461
+ !isLoading && !isError && !readOnly && showAddRoot && /* @__PURE__ */ (0, import_jsx_runtime52.jsx)(
3411
3462
  import_ui24.IconTextButton,
3412
3463
  {
3413
3464
  icon: import_lucide_react17.Plus,
@@ -3417,38 +3468,38 @@ function GenreTreeView({
3417
3468
  )
3418
3469
  ] })
3419
3470
  ] });
3420
- return /* @__PURE__ */ (0, import_jsx_runtime51.jsxs)(
3471
+ return /* @__PURE__ */ (0, import_jsx_runtime52.jsxs)(
3421
3472
  "div",
3422
3473
  {
3423
3474
  "data-left-panel": isPanelShown && !isCollapsed ? void 0 : "hidden",
3424
3475
  className: ["genre-tree-view relative flex flex-col h-full", placement === "regions" && REGIONS_CLASS, className2].filter(Boolean).join(" "),
3425
3476
  children: [
3426
- /* @__PURE__ */ (0, import_jsx_runtime51.jsx)(
3477
+ /* @__PURE__ */ (0, import_jsx_runtime52.jsx)(
3427
3478
  "div",
3428
3479
  {
3429
3480
  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"}`,
3430
3481
  children: actions
3431
3482
  }
3432
3483
  ),
3433
- !isLoading && !isError && filterArtist && /* @__PURE__ */ (0, import_jsx_runtime51.jsx)(
3484
+ !isLoading && !isError && filterArtist && /* @__PURE__ */ (0, import_jsx_runtime52.jsx)(
3434
3485
  "div",
3435
3486
  {
3436
3487
  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"}`,
3437
3488
  children: renderArtistFilter()
3438
3489
  }
3439
3490
  ),
3440
- /* @__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: [
3441
- !isLoading && !isError && viewMode === "outline" && floatingControlsLeading && /* @__PURE__ */ (0, import_jsx_runtime51.jsx)("div", { className: "absolute bottom-3 right-3 z-10", children: floatingControlsLeading }),
3442
- 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: [
3491
+ /* @__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: [
3492
+ !isLoading && !isError && viewMode === "outline" && floatingControlsLeading && /* @__PURE__ */ (0, import_jsx_runtime52.jsx)("div", { className: "absolute bottom-3 right-3 z-10", children: floatingControlsLeading }),
3493
+ 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: [
3443
3494
  "Couldn't load the genre tree.",
3444
3495
  " ",
3445
- /* @__PURE__ */ (0, import_jsx_runtime51.jsx)("button", { type: "button", className: "underline", onClick: refetch, children: "Retry" })
3496
+ /* @__PURE__ */ (0, import_jsx_runtime52.jsx)("button", { type: "button", className: "underline", onClick: refetch, children: "Retry" })
3446
3497
  ] }) : viewMode === "wheel" ? (
3447
3498
  // The info panel's top edge aligns with the tree's; the search bar floats `--layout-panel-inset` below it,
3448
3499
  // and the header inset (the bar's height plus that inset) extends the header's fill behind the bar,
3449
3500
  // Google Maps style; the 8px bottom padding mirrors the inset above the bar, and the hidden title's height is zeroed.
3450
3501
  // The panel's own Zoom in button is hidden: the search bar carries it.
3451
- /* @__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)(
3502
+ /* @__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)(
3452
3503
  GenrePlaylistTreeWheel,
3453
3504
  {
3454
3505
  scope,
@@ -3466,9 +3517,9 @@ function GenreTreeView({
3466
3517
  allowWheelRotation,
3467
3518
  sizeMetric: isArtistFiltered || !canSizeByViews ? "songs" : deferredSizeMetric,
3468
3519
  busy,
3469
- floatingControlsLeading: floatingControlsLeading || canSizeByViews ? /* @__PURE__ */ (0, import_jsx_runtime51.jsxs)("div", { className: "flex items-center gap-2", children: [
3520
+ floatingControlsLeading: floatingControlsLeading || canSizeByViews ? /* @__PURE__ */ (0, import_jsx_runtime52.jsxs)("div", { className: "flex items-center gap-2", children: [
3470
3521
  floatingControlsLeading,
3471
- canSizeByViews && /* @__PURE__ */ (0, import_jsx_runtime51.jsx)(SizeMetricToggle, { value: sizeMetric, onChange: changeSizeMetric })
3522
+ canSizeByViews && /* @__PURE__ */ (0, import_jsx_runtime52.jsx)(SizeMetricToggle, { value: sizeMetric, onChange: changeSizeMetric })
3472
3523
  ] }) : void 0,
3473
3524
  defaultViewport,
3474
3525
  viewportRestore,
@@ -3477,7 +3528,7 @@ function GenreTreeView({
3477
3528
  ) }) })
3478
3529
  ) : viewMode === "pop-core" ? (
3479
3530
  // Same as the wheel: the header inset extends its fill behind the floating search bar, Google Maps style.
3480
- /* @__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)(
3531
+ /* @__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)(
3481
3532
  GenrePlaylistTreeWheelRadialPopCore,
3482
3533
  {
3483
3534
  scope,
@@ -3495,9 +3546,9 @@ function GenreTreeView({
3495
3546
  allowWheelRotation,
3496
3547
  sizeMetric: isArtistFiltered || !canSizeByViews ? "songs" : deferredSizeMetric,
3497
3548
  busy,
3498
- floatingControlsLeading: floatingControlsLeading || canSizeByViews ? /* @__PURE__ */ (0, import_jsx_runtime51.jsxs)("div", { className: "flex items-center gap-2", children: [
3549
+ floatingControlsLeading: floatingControlsLeading || canSizeByViews ? /* @__PURE__ */ (0, import_jsx_runtime52.jsxs)("div", { className: "flex items-center gap-2", children: [
3499
3550
  floatingControlsLeading,
3500
- canSizeByViews && /* @__PURE__ */ (0, import_jsx_runtime51.jsx)(SizeMetricToggle, { value: sizeMetric, onChange: changeSizeMetric })
3551
+ canSizeByViews && /* @__PURE__ */ (0, import_jsx_runtime52.jsx)(SizeMetricToggle, { value: sizeMetric, onChange: changeSizeMetric })
3501
3552
  ] }) : void 0,
3502
3553
  defaultViewport,
3503
3554
  viewportRestore,
@@ -3505,7 +3556,7 @@ function GenreTreeView({
3505
3556
  ...focus
3506
3557
  }
3507
3558
  ) }) })
3508
- ) : 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)(
3559
+ ) : 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)(
3509
3560
  GenrePlaylistTreeWheelRadialPopCore,
3510
3561
  {
3511
3562
  outline: true,
@@ -3522,13 +3573,13 @@ function GenreTreeView({
3522
3573
  selectedNodeId,
3523
3574
  readOnly
3524
3575
  }
3525
- ) }) : /* @__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(
3576
+ ) }) : /* @__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(
3526
3577
  ([uuid, genrePlaylistTreePerRoot]) => {
3527
- return /* @__PURE__ */ (0, import_jsx_runtime51.jsx)(
3578
+ return /* @__PURE__ */ (0, import_jsx_runtime52.jsx)(
3528
3579
  "div",
3529
3580
  {
3530
3581
  className: "tree-per-root-container relative shrink-0 h-[28rem] mt-2 mr-16 p-2 bg-[var(--color-neutral-100)] rounded-lg",
3531
- children: /* @__PURE__ */ (0, import_jsx_runtime51.jsx)("div", { className: "graph-container relative z-10 h-full", children: /* @__PURE__ */ (0, import_jsx_runtime51.jsx)(
3582
+ children: /* @__PURE__ */ (0, import_jsx_runtime52.jsx)("div", { className: "graph-container relative z-10 h-full", children: /* @__PURE__ */ (0, import_jsx_runtime52.jsx)(
3532
3583
  GenrePlaylistTreePerRoot,
3533
3584
  {
3534
3585
  scope,
@@ -3555,7 +3606,7 @@ function GenreTreeView({
3555
3606
  );
3556
3607
  }
3557
3608
  ) }),
3558
- /* @__PURE__ */ (0, import_jsx_runtime51.jsx)(
3609
+ /* @__PURE__ */ (0, import_jsx_runtime52.jsx)(
3559
3610
  import_ui24.DetailPanel,
3560
3611
  {
3561
3612
  ref: panelRef,
@@ -3570,7 +3621,7 @@ function GenreTreeView({
3570
3621
  onExpandedChange: activePanel?.onExpandedChange,
3571
3622
  aside: activePanel?.aside,
3572
3623
  edgeTab: detailTab,
3573
- children: activePanel?.aside && !activePanel.expanded ? /* @__PURE__ */ (0, import_jsx_runtime51.jsxs)(import_jsx_runtime51.Fragment, { children: [
3624
+ children: activePanel?.aside && !activePanel.expanded ? /* @__PURE__ */ (0, import_jsx_runtime52.jsxs)(import_jsx_runtime52.Fragment, { children: [
3574
3625
  activePanel.content,
3575
3626
  activePanel.aside
3576
3627
  ] }) : activePanel?.content
@@ -3588,10 +3639,10 @@ var import_genre_tree_view6 = require("@behindthemusictree/genre-tree-view");
3588
3639
  // src/genre-tree/track-list-sidebar/TrackListSidebar.tsx
3589
3640
  var import_react20 = require("react");
3590
3641
  var import_lucide_react19 = require("lucide-react");
3591
- var import_genre_tree31 = require("@behindthemusictree/app-kit/genre-tree");
3592
3642
  var import_genre_tree32 = require("@behindthemusictree/app-kit/genre-tree");
3643
+ var import_genre_tree33 = require("@behindthemusictree/app-kit/genre-tree");
3593
3644
  var import_track_noun4 = require("@behindthemusictree/app-kit/track-noun");
3594
- var import_jsx_runtime52 = require("react/jsx-runtime");
3645
+ var import_jsx_runtime53 = require("react/jsx-runtime");
3595
3646
  var useBrowserLayoutEffect = typeof window === "undefined" ? import_react20.useEffect : import_react20.useLayoutEffect;
3596
3647
  function TrackListSidebar({
3597
3648
  className: className2 = "",
@@ -3604,7 +3655,7 @@ function TrackListSidebar({
3604
3655
  renderArtistLink,
3605
3656
  renderSongLink
3606
3657
  }) {
3607
- const { trackList, loadMore, autoAdvanceScrollRequest } = (0, import_genre_tree31.useTrackList)();
3658
+ const { trackList, loadMore, autoAdvanceScrollRequest } = (0, import_genre_tree32.useTrackList)();
3608
3659
  const trackNoun = (0, import_track_noun4.useTrackNoun)();
3609
3660
  const trackListRef = (0, import_react20.useRef)(null);
3610
3661
  const previousOriginRef = (0, import_react20.useRef)(trackList?.origin ?? null);
@@ -3693,25 +3744,25 @@ function TrackListSidebar({
3693
3744
  }
3694
3745
  };
3695
3746
  const positionClasses = layout === "inline" ? "relative w-full flex h-full flex-col" : "fixed right-0 w-[50rem] flex flex-col";
3696
- return trackList ? /* @__PURE__ */ (0, import_jsx_runtime52.jsxs)(
3747
+ return trackList ? /* @__PURE__ */ (0, import_jsx_runtime53.jsxs)(
3697
3748
  "div",
3698
3749
  {
3699
3750
  "data-theme": "dark",
3700
3751
  className: `track-list-sidebar ${positionClasses} rounded-2xl bg-[var(--color-surface)] pb-1 ${className2}`,
3701
3752
  style: layout === "fixed" ? { top: "63.5px", maxHeight: "calc(100vh - 63.5px - 79px)" } : void 0,
3702
3753
  children: [
3703
- /* @__PURE__ */ (0, import_jsx_runtime52.jsxs)("div", { className: "header flex h-16 items-center px-4 py-2 text-[var(--color-text)]", children: [
3704
- /* @__PURE__ */ (0, import_jsx_runtime52.jsxs)("div", { className: "min-w-0 flex flex-1 flex-col justify-center", children: [
3705
- /* @__PURE__ */ (0, import_jsx_runtime52.jsxs)("div", { className: "origin flex text-xl", children: [
3706
- /* @__PURE__ */ (0, import_jsx_runtime52.jsx)("div", { className: "from h-auto flex flex-col justify-center items-center mr-2", children: "From" }),
3707
- /* @__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 }) })
3754
+ /* @__PURE__ */ (0, import_jsx_runtime53.jsxs)("div", { className: "header flex h-16 items-center px-4 py-2 text-[var(--color-text)]", children: [
3755
+ /* @__PURE__ */ (0, import_jsx_runtime53.jsxs)("div", { className: "min-w-0 flex flex-1 flex-col justify-center", children: [
3756
+ /* @__PURE__ */ (0, import_jsx_runtime53.jsxs)("div", { className: "origin flex text-xl", children: [
3757
+ /* @__PURE__ */ (0, import_jsx_runtime53.jsx)("div", { className: "from h-auto flex flex-col justify-center items-center mr-2", children: "From" }),
3758
+ /* @__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 }) })
3708
3759
  ] }),
3709
- /* @__PURE__ */ (0, import_jsx_runtime52.jsxs)("div", { className: "info flex items-center text-body text-[var(--color-text-muted)]", children: [
3710
- 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 `,
3760
+ /* @__PURE__ */ (0, import_jsx_runtime53.jsxs)("div", { className: "info flex items-center text-body text-[var(--color-text-muted)]", children: [
3761
+ 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 `,
3711
3762
  `${trackList.total} ${trackList.total > 1 ? trackNoun.plural : trackNoun.singular}`
3712
3763
  ] })
3713
3764
  ] }),
3714
- (isCollapsed && onExpand || !isCollapsed && onMinimize) && /* @__PURE__ */ (0, import_jsx_runtime52.jsx)(
3765
+ (isCollapsed && onExpand || !isCollapsed && onMinimize) && /* @__PURE__ */ (0, import_jsx_runtime53.jsx)(
3715
3766
  "button",
3716
3767
  {
3717
3768
  type: "button",
@@ -3720,11 +3771,11 @@ function TrackListSidebar({
3720
3771
  "aria-expanded": !isCollapsed,
3721
3772
  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"}`,
3722
3773
  onClick: isCollapsed ? onExpand : onMinimize,
3723
- children: /* @__PURE__ */ (0, import_jsx_runtime52.jsx)(import_lucide_react19.ListMusic, { "aria-hidden": "true", className: "h-5 w-5" })
3774
+ children: /* @__PURE__ */ (0, import_jsx_runtime53.jsx)(import_lucide_react19.ListMusic, { "aria-hidden": "true", className: "h-5 w-5" })
3724
3775
  }
3725
3776
  )
3726
3777
  ] }),
3727
- !isCollapsed && /* @__PURE__ */ (0, import_jsx_runtime52.jsxs)(
3778
+ !isCollapsed && /* @__PURE__ */ (0, import_jsx_runtime53.jsxs)(
3728
3779
  "ul",
3729
3780
  {
3730
3781
  ref: trackListRef,
@@ -3734,7 +3785,7 @@ function TrackListSidebar({
3734
3785
  onPointerDown: markUserScrollIntent,
3735
3786
  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",
3736
3787
  children: [
3737
- trackList.tracks.map((track, index) => /* @__PURE__ */ (0, import_jsx_runtime52.jsx)("li", { "data-track-uuid": track.uuid, children: /* @__PURE__ */ (0, import_jsx_runtime52.jsx)(
3788
+ trackList.tracks.map((track, index) => /* @__PURE__ */ (0, import_jsx_runtime53.jsx)("li", { "data-track-uuid": track.uuid, children: /* @__PURE__ */ (0, import_jsx_runtime53.jsx)(
3738
3789
  TrackItem,
3739
3790
  {
3740
3791
  track,
@@ -3745,7 +3796,7 @@ function TrackListSidebar({
3745
3796
  renderSongLink
3746
3797
  }
3747
3798
  ) }, track.uuid)),
3748
- hasMore && /* @__PURE__ */ (0, import_jsx_runtime52.jsx)("li", { ref: loadMoreSentinelRef, "aria-hidden": "true", className: "h-px" })
3799
+ hasMore && /* @__PURE__ */ (0, import_jsx_runtime53.jsx)("li", { ref: loadMoreSentinelRef, "aria-hidden": "true", className: "h-px" })
3749
3800
  ]
3750
3801
  }
3751
3802
  )
@@ -3758,14 +3809,14 @@ function TrackListSidebar({
3758
3809
  var import_react21 = require("react");
3759
3810
  var import_tailwind_merge = require("tailwind-merge");
3760
3811
  var import_player14 = require("@behindthemusictree/app-kit/player");
3761
- var import_genre_tree34 = require("@behindthemusictree/app-kit/genre-tree");
3812
+ var import_genre_tree35 = require("@behindthemusictree/app-kit/genre-tree");
3762
3813
  var import_ui25 = require("@behindthemusictree/ui");
3763
3814
 
3764
3815
  // src/genre-tree/PlayerControls.tsx
3765
3816
  var import_lucide_react20 = require("lucide-react");
3766
- var import_genre_tree33 = require("@behindthemusictree/app-kit/genre-tree");
3817
+ var import_genre_tree34 = require("@behindthemusictree/app-kit/genre-tree");
3767
3818
  var import_player13 = require("@behindthemusictree/app-kit/player");
3768
- var import_jsx_runtime53 = require("react/jsx-runtime");
3819
+ var import_jsx_runtime54 = require("react/jsx-runtime");
3769
3820
  function PlayerControls() {
3770
3821
  const {
3771
3822
  duration,
@@ -3777,22 +3828,22 @@ function PlayerControls() {
3777
3828
  setVolume,
3778
3829
  volume
3779
3830
  } = (0, import_player13.usePlayer)();
3780
- const { hasNextTrack, hasPreviousTrack, playNextTrack, playPreviousTrack } = (0, import_genre_tree33.useTrackList)();
3831
+ const { hasNextTrack, hasPreviousTrack, playNextTrack, playPreviousTrack } = (0, import_genre_tree34.useTrackList)();
3781
3832
  const currentTime = (0, import_player13.useCurrentTime)();
3782
3833
  const isBusy = isLoading || playState === import_player13.PlayStates.LOADING;
3783
3834
  const isReady = !!playerTrackObject?.isReady && !isBusy && !playerTrackObject.loadError;
3784
3835
  const safeDuration = Number.isFinite(duration) && duration > 0 ? duration : 0;
3785
3836
  const safeCurrentTime = Number.isFinite(currentTime) ? Math.max(0, Math.min(currentTime, safeDuration)) : 0;
3786
3837
  const isPlaying = playState === import_player13.PlayStates.PLAYING;
3787
- return /* @__PURE__ */ (0, import_jsx_runtime53.jsxs)(
3838
+ return /* @__PURE__ */ (0, import_jsx_runtime54.jsxs)(
3788
3839
  "section",
3789
3840
  {
3790
3841
  "aria-label": "Player controls",
3791
3842
  className: "player-controls flex shrink-0 flex-col gap-2 border-b border-zinc-800 px-3 py-2 text-zinc-200",
3792
3843
  children: [
3793
- /* @__PURE__ */ (0, import_jsx_runtime53.jsxs)("div", { className: "flex min-w-0 items-center gap-2 text-xs tabular-nums text-zinc-400", children: [
3794
- /* @__PURE__ */ (0, import_jsx_runtime53.jsx)("span", { "aria-label": "Elapsed time", children: (0, import_genre_tree33.formatTime)(safeCurrentTime) }),
3795
- /* @__PURE__ */ (0, import_jsx_runtime53.jsx)(
3844
+ /* @__PURE__ */ (0, import_jsx_runtime54.jsxs)("div", { className: "flex min-w-0 items-center gap-2 text-xs tabular-nums text-zinc-400", children: [
3845
+ /* @__PURE__ */ (0, import_jsx_runtime54.jsx)("span", { "aria-label": "Elapsed time", children: (0, import_genre_tree34.formatTime)(safeCurrentTime) }),
3846
+ /* @__PURE__ */ (0, import_jsx_runtime54.jsx)(
3796
3847
  "input",
3797
3848
  {
3798
3849
  "aria-label": "Seek",
@@ -3806,11 +3857,11 @@ function PlayerControls() {
3806
3857
  onChange: (event) => seekTo(Number(event.currentTarget.value))
3807
3858
  }
3808
3859
  ),
3809
- /* @__PURE__ */ (0, import_jsx_runtime53.jsx)("span", { "aria-label": "Duration", children: (0, import_genre_tree33.formatTime)(safeDuration) })
3860
+ /* @__PURE__ */ (0, import_jsx_runtime54.jsx)("span", { "aria-label": "Duration", children: (0, import_genre_tree34.formatTime)(safeDuration) })
3810
3861
  ] }),
3811
- /* @__PURE__ */ (0, import_jsx_runtime53.jsxs)("div", { className: "grid min-h-9 grid-cols-[1fr_auto_1fr] items-center gap-3", children: [
3812
- /* @__PURE__ */ (0, import_jsx_runtime53.jsxs)("div", { className: "col-start-2 flex items-center justify-center gap-2", children: [
3813
- /* @__PURE__ */ (0, import_jsx_runtime53.jsx)(
3862
+ /* @__PURE__ */ (0, import_jsx_runtime54.jsxs)("div", { className: "grid min-h-9 grid-cols-[1fr_auto_1fr] items-center gap-3", children: [
3863
+ /* @__PURE__ */ (0, import_jsx_runtime54.jsxs)("div", { className: "col-start-2 flex items-center justify-center gap-2", children: [
3864
+ /* @__PURE__ */ (0, import_jsx_runtime54.jsx)(
3814
3865
  "button",
3815
3866
  {
3816
3867
  type: "button",
@@ -3819,10 +3870,10 @@ function PlayerControls() {
3819
3870
  disabled: !isReady || !hasPreviousTrack,
3820
3871
  onClick: playPreviousTrack,
3821
3872
  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",
3822
- children: /* @__PURE__ */ (0, import_jsx_runtime53.jsx)(import_lucide_react20.SkipBack, { "aria-hidden": "true", className: "h-4 w-4" })
3873
+ children: /* @__PURE__ */ (0, import_jsx_runtime54.jsx)(import_lucide_react20.SkipBack, { "aria-hidden": "true", className: "h-4 w-4" })
3823
3874
  }
3824
3875
  ),
3825
- /* @__PURE__ */ (0, import_jsx_runtime53.jsx)(
3876
+ /* @__PURE__ */ (0, import_jsx_runtime54.jsx)(
3826
3877
  "button",
3827
3878
  {
3828
3879
  type: "button",
@@ -3831,10 +3882,10 @@ function PlayerControls() {
3831
3882
  disabled: !isReady,
3832
3883
  onClick: handlePlayPauseAction,
3833
3884
  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",
3834
- children: isPlaying ? /* @__PURE__ */ (0, import_jsx_runtime53.jsx)(import_lucide_react20.Pause, { "aria-hidden": "true", className: "h-4 w-4", fill: "currentColor" }) : /* @__PURE__ */ (0, import_jsx_runtime53.jsx)(import_lucide_react20.Play, { "aria-hidden": "true", className: "h-4 w-4", fill: "currentColor" })
3885
+ 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" })
3835
3886
  }
3836
3887
  ),
3837
- /* @__PURE__ */ (0, import_jsx_runtime53.jsx)(
3888
+ /* @__PURE__ */ (0, import_jsx_runtime54.jsx)(
3838
3889
  "button",
3839
3890
  {
3840
3891
  type: "button",
@@ -3843,14 +3894,14 @@ function PlayerControls() {
3843
3894
  disabled: !isReady || !hasNextTrack,
3844
3895
  onClick: playNextTrack,
3845
3896
  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",
3846
- children: /* @__PURE__ */ (0, import_jsx_runtime53.jsx)(import_lucide_react20.SkipForward, { "aria-hidden": "true", className: "h-4 w-4" })
3897
+ children: /* @__PURE__ */ (0, import_jsx_runtime54.jsx)(import_lucide_react20.SkipForward, { "aria-hidden": "true", className: "h-4 w-4" })
3847
3898
  }
3848
3899
  )
3849
3900
  ] }),
3850
- /* @__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: [
3851
- volume === 0 ? /* @__PURE__ */ (0, import_jsx_runtime53.jsx)(import_lucide_react20.VolumeX, { "aria-hidden": "true", className: "h-[0.7rem] w-[0.7rem] shrink-0" }) : /* @__PURE__ */ (0, import_jsx_runtime53.jsx)(import_lucide_react20.Volume2, { "aria-hidden": "true", className: "h-[0.7rem] w-[0.7rem] shrink-0" }),
3852
- /* @__PURE__ */ (0, import_jsx_runtime53.jsx)("span", { className: "sr-only", children: "Volume" }),
3853
- /* @__PURE__ */ (0, import_jsx_runtime53.jsx)(
3901
+ /* @__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: [
3902
+ 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" }),
3903
+ /* @__PURE__ */ (0, import_jsx_runtime54.jsx)("span", { className: "sr-only", children: "Volume" }),
3904
+ /* @__PURE__ */ (0, import_jsx_runtime54.jsx)(
3854
3905
  "input",
3855
3906
  {
3856
3907
  "aria-label": "Volume",
@@ -3872,7 +3923,7 @@ function PlayerControls() {
3872
3923
  }
3873
3924
 
3874
3925
  // src/genre-tree/PlayerPanel.tsx
3875
- var import_jsx_runtime54 = require("react/jsx-runtime");
3926
+ var import_jsx_runtime55 = require("react/jsx-runtime");
3876
3927
  var PANEL_ID = "player-panel";
3877
3928
  var DEFAULT_EXTENDED_WIDTH = "calc(100vw - var(--layout-center-left) - var(--layout-gutter))";
3878
3929
  function PlayerPanel({
@@ -3885,7 +3936,7 @@ function PlayerPanel({
3885
3936
  renderSongLink
3886
3937
  }) {
3887
3938
  const { playerTrackObject, retryPlayback } = (0, import_player14.usePlayer)();
3888
- const { toggleTrackListSidebar, isTrackListSidebarVisible } = (0, import_genre_tree34.useTrackListSidebarVisibility)();
3939
+ const { toggleTrackListSidebar, isTrackListSidebarVisible } = (0, import_genre_tree35.useTrackListSidebarVisibility)();
3889
3940
  const [lastLoadedTrackTitle, setLastLoadedTrackTitle] = (0, import_react21.useState)("");
3890
3941
  const loadedTrackTitle = playerTrackObject?.track.title;
3891
3942
  (0, import_react21.useEffect)(() => {
@@ -3912,7 +3963,7 @@ function PlayerPanel({
3912
3963
  onClick: () => setIsExtendRequested(true),
3913
3964
  expanded: false
3914
3965
  } : null;
3915
- return /* @__PURE__ */ (0, import_jsx_runtime54.jsxs)(
3966
+ return /* @__PURE__ */ (0, import_jsx_runtime55.jsxs)(
3916
3967
  "div",
3917
3968
  {
3918
3969
  id: PANEL_ID,
@@ -3925,7 +3976,7 @@ function PlayerPanel({
3925
3976
  ),
3926
3977
  style: isExtended ? { width: extendedWidth } : void 0,
3927
3978
  children: [
3928
- /* @__PURE__ */ (0, import_jsx_runtime54.jsx)(
3979
+ /* @__PURE__ */ (0, import_jsx_runtime55.jsx)(
3929
3980
  import_ui25.PanelEdgeTab,
3930
3981
  {
3931
3982
  side: "right",
@@ -3935,7 +3986,7 @@ function PlayerPanel({
3935
3986
  className: "absolute right-full top-1/2 -translate-y-1/2"
3936
3987
  }
3937
3988
  ),
3938
- /* @__PURE__ */ (0, import_jsx_runtime54.jsxs)(
3989
+ /* @__PURE__ */ (0, import_jsx_runtime55.jsxs)(
3939
3990
  "div",
3940
3991
  {
3941
3992
  className: (0, import_tailwind_merge.twMerge)(
@@ -3943,20 +3994,20 @@ function PlayerPanel({
3943
3994
  isExtended ? "flex-row" : "flex-col"
3944
3995
  ),
3945
3996
  children: [
3946
- /* @__PURE__ */ (0, import_jsx_runtime54.jsxs)("div", { className: "flex w-[var(--layout-right-w)] shrink-0 flex-col", children: [
3947
- /* @__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 }) }),
3948
- /* @__PURE__ */ (0, import_jsx_runtime54.jsxs)("div", { className: "relative w-full shrink-0", children: [
3949
- /* @__PURE__ */ (0, import_jsx_runtime54.jsx)(import_player14.PlayerVideoSurface, { className: "aspect-video w-full bg-black" }),
3950
- playerTrackObject?.loadError && /* @__PURE__ */ (0, import_jsx_runtime54.jsxs)("span", { className: "absolute bottom-2 left-2 right-2 text-xs text-red-400", children: [
3997
+ /* @__PURE__ */ (0, import_jsx_runtime55.jsxs)("div", { className: "flex w-[var(--layout-right-w)] shrink-0 flex-col", children: [
3998
+ /* @__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 }) }),
3999
+ /* @__PURE__ */ (0, import_jsx_runtime55.jsxs)("div", { className: "relative w-full shrink-0", children: [
4000
+ /* @__PURE__ */ (0, import_jsx_runtime55.jsx)(import_player14.PlayerVideoSurface, { className: "aspect-video w-full bg-black" }),
4001
+ playerTrackObject?.loadError && /* @__PURE__ */ (0, import_jsx_runtime55.jsxs)("span", { className: "absolute bottom-2 left-2 right-2 text-xs text-red-400", children: [
3951
4002
  playerTrackObject.loadError,
3952
- /* @__PURE__ */ (0, import_jsx_runtime54.jsx)("button", { type: "button", className: "ml-2 underline", onClick: () => {
4003
+ /* @__PURE__ */ (0, import_jsx_runtime55.jsx)("button", { type: "button", className: "ml-2 underline", onClick: () => {
3953
4004
  void retryPlayback();
3954
4005
  }, children: "Retry" })
3955
4006
  ] })
3956
4007
  ] }),
3957
- /* @__PURE__ */ (0, import_jsx_runtime54.jsx)(PlayerControls, {})
4008
+ /* @__PURE__ */ (0, import_jsx_runtime55.jsx)(PlayerControls, {})
3958
4009
  ] }),
3959
- /* @__PURE__ */ (0, import_jsx_runtime54.jsx)(
4010
+ /* @__PURE__ */ (0, import_jsx_runtime55.jsx)(
3960
4011
  TrackListSidebar,
3961
4012
  {
3962
4013
  layout: "inline",
@@ -3982,33 +4033,33 @@ function PlayerPanel({
3982
4033
  // src/genre-tree/Rating.tsx
3983
4034
  var import_uuid = require("uuid");
3984
4035
  var import_ai = require("react-icons/ai");
3985
- var import_genre_tree35 = require("@behindthemusictree/app-kit/genre-tree");
3986
- var import_jsx_runtime55 = require("react/jsx-runtime");
4036
+ var import_genre_tree36 = require("@behindthemusictree/app-kit/genre-tree");
4037
+ var import_jsx_runtime56 = require("react/jsx-runtime");
3987
4038
  function Rating({ rating, handleChange }) {
3988
4039
  const instanceId = (0, import_uuid.v4)();
3989
- 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: [
3990
- /* @__PURE__ */ (0, import_jsx_runtime55.jsx)(
4040
+ 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: [
4041
+ /* @__PURE__ */ (0, import_jsx_runtime56.jsx)(
3991
4042
  "input",
3992
4043
  {
3993
4044
  id: `star-${instanceId}-${i}`,
3994
4045
  className: "w-6 h-6 opacity-0",
3995
4046
  type: "radio",
3996
4047
  name: "rating",
3997
- value: i === 0 ? import_genre_tree35.FORM_RATING_NULL_VALUE : i,
4048
+ value: i === 0 ? import_genre_tree36.FORM_RATING_NULL_VALUE : i,
3998
4049
  checked: rating === (i === 0 ? null : i),
3999
4050
  onChange: handleChange
4000
4051
  }
4001
4052
  ),
4002
- /* @__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: [
4003
- /* @__PURE__ */ (0, import_jsx_runtime55.jsx)(
4053
+ /* @__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: [
4054
+ /* @__PURE__ */ (0, import_jsx_runtime56.jsx)(
4004
4055
  import_ai.AiOutlineStar,
4005
4056
  {
4006
- 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)]"}`
4057
+ 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)]"}`
4007
4058
  }
4008
4059
  ),
4009
- /* @__PURE__ */ (0, import_jsx_runtime55.jsx)("span", { className: "flex-grow w-8" }),
4060
+ /* @__PURE__ */ (0, import_jsx_runtime56.jsx)("span", { className: "flex-grow w-8" }),
4010
4061
  " "
4011
- ] }) : /* @__PURE__ */ (0, import_jsx_runtime55.jsx)(
4062
+ ] }) : /* @__PURE__ */ (0, import_jsx_runtime56.jsx)(
4012
4063
  import_ai.AiFillStar,
4013
4064
  {
4014
4065
  className: `w-7 h-7 absolute top-0 left-0 ${(rating ?? -1) >= i ? "text-[var(--color-accent-rating)]" : "text-[var(--color-text-muted)]"}`
@@ -4020,7 +4071,7 @@ function Rating({ rating, handleChange }) {
4020
4071
  // src/genre-tree/EntityDetailPanel.tsx
4021
4072
  var import_react22 = require("react");
4022
4073
  var import_ui26 = require("@behindthemusictree/ui");
4023
- var import_jsx_runtime56 = require("react/jsx-runtime");
4074
+ var import_jsx_runtime57 = require("react/jsx-runtime");
4024
4075
  function EntityDetailPanel({
4025
4076
  scope,
4026
4077
  getBackendBaseUrl,
@@ -4032,6 +4083,7 @@ function EntityDetailPanel({
4032
4083
  onArtistUuidChange,
4033
4084
  renderArtistLink,
4034
4085
  renderSongLink,
4086
+ renderPlaceLink,
4035
4087
  collapsed,
4036
4088
  edgeTab,
4037
4089
  ...navigation
@@ -4043,8 +4095,9 @@ function EntityDetailPanel({
4043
4095
  getBackendBaseUrl,
4044
4096
  getArtistBackendBaseUrl,
4045
4097
  getShareUrl,
4046
- 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 })),
4047
- renderSongLink: renderSongLink ?? ((song) => /* @__PURE__ */ (0, import_jsx_runtime56.jsx)("button", { type: "button", onClick: () => onSongUuidChange(song.uuid), className: "hover:underline", children: song.title })),
4098
+ renderPlaceLink,
4099
+ 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 })),
4100
+ renderSongLink: renderSongLink ?? ((song) => /* @__PURE__ */ (0, import_jsx_runtime57.jsx)("button", { type: "button", onClick: () => onSongUuidChange(song.uuid), className: "hover:underline", children: song.title })),
4048
4101
  song: {
4049
4102
  uuid: songUuid,
4050
4103
  expanded: songUuid !== null && expandedSongUuid === songUuid,
@@ -4060,7 +4113,7 @@ function EntityDetailPanel({
4060
4113
  ...navigation
4061
4114
  });
4062
4115
  if (!panel) return null;
4063
- return /* @__PURE__ */ (0, import_jsx_runtime56.jsx)(
4116
+ return /* @__PURE__ */ (0, import_jsx_runtime57.jsx)(
4064
4117
  import_ui26.DetailPanel,
4065
4118
  {
4066
4119
  side: "left",
@@ -4072,7 +4125,7 @@ function EntityDetailPanel({
4072
4125
  onExpandedChange: panel.onExpandedChange,
4073
4126
  aside: panel.extraContent,
4074
4127
  edgeTab,
4075
- children: panel.extraContent && !panel.expanded ? /* @__PURE__ */ (0, import_jsx_runtime56.jsxs)(import_jsx_runtime56.Fragment, { children: [
4128
+ children: panel.extraContent && !panel.expanded ? /* @__PURE__ */ (0, import_jsx_runtime57.jsxs)(import_jsx_runtime57.Fragment, { children: [
4076
4129
  panel.content,
4077
4130
  panel.extraContent
4078
4131
  ] }) : panel.content