@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.
@@ -55,6 +55,7 @@ var import_lucide_react14 = require("lucide-react");
55
55
  var import_ui21 = require("@behindthemusictree/ui");
56
56
  var import_genre_tree_view5 = require("@behindthemusictree/genre-tree-view");
57
57
  var import_genre_tree30 = require("@behindthemusictree/app-kit/genre-tree");
58
+ var import_genre_tree31 = require("@behindthemusictree/app-kit/genre-tree");
58
59
  var import_popup4 = require("@behindthemusictree/app-kit/popup");
59
60
 
60
61
  // src/genre-tree/playlist-tree/size-metric.ts
@@ -1402,6 +1403,29 @@ function ClampedText({ children, className: className2 = "" }) {
1402
1403
  ] });
1403
1404
  }
1404
1405
 
1406
+ // src/genre-tree/CommonsPhoto.tsx
1407
+ var import_jsx_runtime26 = require("react/jsx-runtime");
1408
+ function ExternalLink({ href, children }) {
1409
+ return /* @__PURE__ */ (0, import_jsx_runtime26.jsx)("a", { href, target: "_blank", rel: "noopener noreferrer", className: "underline-offset-2 hover:underline", children });
1410
+ }
1411
+ function CommonsPhoto({
1412
+ image,
1413
+ alt,
1414
+ creditLabel = "Photo"
1415
+ }) {
1416
+ return /* @__PURE__ */ (0, import_jsx_runtime26.jsxs)("figure", { className: "flex flex-col gap-1.5", children: [
1417
+ /* @__PURE__ */ (0, import_jsx_runtime26.jsx)("img", { src: image.url, alt, className: "h-64 w-full object-contain bg-[var(--color-surface-muted)] rounded" }),
1418
+ /* @__PURE__ */ (0, import_jsx_runtime26.jsxs)("figcaption", { className: "text-caption text-[var(--color-text-muted)]", children: [
1419
+ creditLabel,
1420
+ ": ",
1421
+ /* @__PURE__ */ (0, import_jsx_runtime26.jsx)(ExternalLink, { href: image.pageUrl, children: image.author ?? "Wikimedia Commons" }),
1422
+ ",",
1423
+ " ",
1424
+ image.licenseUrl ? /* @__PURE__ */ (0, import_jsx_runtime26.jsx)(ExternalLink, { href: image.licenseUrl, children: image.license }) : image.license
1425
+ ] })
1426
+ ] });
1427
+ }
1428
+
1405
1429
  // src/genre-tree/GenrePlaylistTracks.tsx
1406
1430
  var import_react13 = require("react");
1407
1431
  var import_react_query = require("@tanstack/react-query");
@@ -1424,15 +1448,15 @@ var import_genre_tree16 = require("@behindthemusictree/app-kit/genre-tree");
1424
1448
 
1425
1449
  // src/genre-tree/MusicBrainzRecordingLink.tsx
1426
1450
  var import_genre_tree11 = require("@behindthemusictree/app-kit/genre-tree");
1427
- var import_jsx_runtime26 = require("react/jsx-runtime");
1451
+ var import_jsx_runtime27 = require("react/jsx-runtime");
1428
1452
  function MusicBrainzRecordingLink({
1429
1453
  song,
1430
1454
  children,
1431
1455
  className: className2 = ""
1432
1456
  }) {
1433
1457
  const url = (0, import_genre_tree11.musicBrainzRecordingUrl)(song);
1434
- if (!url) return /* @__PURE__ */ (0, import_jsx_runtime26.jsx)(import_jsx_runtime26.Fragment, { children });
1435
- return /* @__PURE__ */ (0, import_jsx_runtime26.jsx)(
1458
+ if (!url) return /* @__PURE__ */ (0, import_jsx_runtime27.jsx)(import_jsx_runtime27.Fragment, { children });
1459
+ return /* @__PURE__ */ (0, import_jsx_runtime27.jsx)(
1436
1460
  "a",
1437
1461
  {
1438
1462
  href: url,
@@ -1449,7 +1473,7 @@ function MusicBrainzRecordingLink({
1449
1473
  // src/genre-tree/GenrePlaylistTracks.tsx
1450
1474
  var import_genre_tree17 = require("@behindthemusictree/app-kit/genre-tree");
1451
1475
  var import_track_noun = require("@behindthemusictree/app-kit/track-noun");
1452
- var import_jsx_runtime27 = require("react/jsx-runtime");
1476
+ var import_jsx_runtime28 = require("react/jsx-runtime");
1453
1477
  var CATALOGUE_PAGE_SIZE = 100;
1454
1478
  var SongPageSchema = (0, import_transport3.PaginatedResponseSchema)(import_genre_tree15.SongDetailedSchema).extend({
1455
1479
  overallTotal: import_zod.z.number().nullable(),
@@ -1536,35 +1560,35 @@ function GenrePlaylistTracks({
1536
1560
  started.catch((error) => {
1537
1561
  console.error("Failed to play genre playlist:", error);
1538
1562
  if (error instanceof import_genre_tree16.TracksFailureAlreadyReportedError) return;
1539
- showPopup(/* @__PURE__ */ (0, import_jsx_runtime27.jsx)(InternalErrorPopup, { errorCode: error instanceof import_transport5.AppError ? error.code : import_transport5.ErrorCode.CLIENT_UNKNOWN }));
1563
+ showPopup(/* @__PURE__ */ (0, import_jsx_runtime28.jsx)(InternalErrorPopup, { errorCode: error instanceof import_transport5.AppError ? error.code : import_transport5.ErrorCode.CLIENT_UNKNOWN }));
1540
1564
  });
1541
1565
  };
1542
- return /* @__PURE__ */ (0, import_jsx_runtime27.jsx)(import_ui8.PanelSection, { title: `${(0, import_track_noun.capitalize)(trackNoun.plural)}${total !== void 0 ? ` (${total})` : ""}`, children: /* @__PURE__ */ (0, import_jsx_runtime27.jsxs)("div", { children: [
1566
+ return /* @__PURE__ */ (0, import_jsx_runtime28.jsx)(import_ui8.PanelSection, { title: `${(0, import_track_noun.capitalize)(trackNoun.plural)}${total !== void 0 ? ` (${total})` : ""}`, children: /* @__PURE__ */ (0, import_jsx_runtime28.jsxs)("div", { children: [
1543
1567
  renderTracksToolbar?.(),
1544
- scope === "me" && !genrePlaylist ? /* @__PURE__ */ (0, import_jsx_runtime27.jsx)("p", { children: "\u2014" }) : isPending ? /* @__PURE__ */ (0, import_jsx_runtime27.jsx)(import_ui8.Skeleton, { className: "h-4 w-3/4", "aria-busy": "true" }) : isError && tracks.length === 0 ? /* @__PURE__ */ (0, import_jsx_runtime27.jsxs)("p", { role: "alert", children: [
1568
+ scope === "me" && !genrePlaylist ? /* @__PURE__ */ (0, import_jsx_runtime28.jsx)("p", { children: "\u2014" }) : isPending ? /* @__PURE__ */ (0, import_jsx_runtime28.jsx)(import_ui8.Skeleton, { className: "h-4 w-3/4", "aria-busy": "true" }) : isError && tracks.length === 0 ? /* @__PURE__ */ (0, import_jsx_runtime28.jsxs)("p", { role: "alert", children: [
1545
1569
  "Couldn't load ",
1546
1570
  trackNoun.plural,
1547
1571
  ".",
1548
1572
  " ",
1549
- /* @__PURE__ */ (0, import_jsx_runtime27.jsx)("button", { type: "button", className: "underline", onClick: () => void refetch(), children: "Retry" })
1550
- ] }) : tracks.length === 0 ? /* @__PURE__ */ (0, import_jsx_runtime27.jsx)("p", { children: "\u2014" }) : /* @__PURE__ */ (0, import_jsx_runtime27.jsxs)("ul", { className: "mt-1 max-h-64 overflow-y-auto text-body [container-type:inline-size]", "aria-label": `${genreName} ${trackNoun.plural}`, children: [
1573
+ /* @__PURE__ */ (0, import_jsx_runtime28.jsx)("button", { type: "button", className: "underline", onClick: () => void refetch(), children: "Retry" })
1574
+ ] }) : tracks.length === 0 ? /* @__PURE__ */ (0, import_jsx_runtime28.jsx)("p", { children: "\u2014" }) : /* @__PURE__ */ (0, import_jsx_runtime28.jsxs)("ul", { className: "mt-1 max-h-64 overflow-y-auto text-body [container-type:inline-size]", "aria-label": `${genreName} ${trackNoun.plural}`, children: [
1551
1575
  tracks.map((track, index) => {
1552
1576
  const label = track.artists?.length ? `${track.title} \u2014 ${track.artists.map((artist2) => artist2.name).join(", ")}` : track.title;
1553
1577
  const isCurrent = playerTrackObject?.track.id === track.uuid;
1554
1578
  const isPlaying = isCurrent && playState === import_player6.PlayStates.PLAYING;
1555
- const text = /* @__PURE__ */ (0, import_jsx_runtime27.jsxs)("span", { className: TEXT_CELL, children: [
1556
- /* @__PURE__ */ (0, import_jsx_runtime27.jsx)("span", { className: TITLE_CELL, children: renderSongLink ? renderSongLink(track) : (0, import_genre_tree15.isSong)(track) ? /* @__PURE__ */ (0, import_jsx_runtime27.jsx)(MusicBrainzRecordingLink, { song: track, className: "hover:underline focus-visible:underline", children: track.title }) : track.title }),
1557
- track.artists?.length ? /* @__PURE__ */ (0, import_jsx_runtime27.jsx)("span", { className: SEPARATOR, children: " \u2014 " }) : null,
1558
- /* @__PURE__ */ (0, import_jsx_runtime27.jsx)("span", { className: COLUMN_CELL, children: track.artists?.length ? /* @__PURE__ */ (0, import_jsx_runtime27.jsx)(import_genre_tree17.ArtistNames, { artists: track.artists, renderArtistLink }) : null }),
1559
- track.album && /* @__PURE__ */ (0, import_jsx_runtime27.jsx)("span", { className: ALBUM_CELL, children: track.album.name })
1579
+ const text = /* @__PURE__ */ (0, import_jsx_runtime28.jsxs)("span", { className: TEXT_CELL, children: [
1580
+ /* @__PURE__ */ (0, import_jsx_runtime28.jsx)("span", { className: TITLE_CELL, children: renderSongLink ? renderSongLink(track) : (0, import_genre_tree15.isSong)(track) ? /* @__PURE__ */ (0, import_jsx_runtime28.jsx)(MusicBrainzRecordingLink, { song: track, className: "hover:underline focus-visible:underline", children: track.title }) : track.title }),
1581
+ track.artists?.length ? /* @__PURE__ */ (0, import_jsx_runtime28.jsx)("span", { className: SEPARATOR, children: " \u2014 " }) : null,
1582
+ /* @__PURE__ */ (0, import_jsx_runtime28.jsx)("span", { className: COLUMN_CELL, children: track.artists?.length ? /* @__PURE__ */ (0, import_jsx_runtime28.jsx)(import_genre_tree17.ArtistNames, { artists: track.artists, renderArtistLink }) : null }),
1583
+ track.album && /* @__PURE__ */ (0, import_jsx_runtime28.jsx)("span", { className: ALBUM_CELL, children: track.album.name })
1560
1584
  ] });
1561
1585
  const leading = renderSongLeading?.(track, index);
1562
1586
  const actions = renderSongActions?.(track);
1563
1587
  const isUnplayable = (0, import_genre_tree15.isSong)(track) && (0, import_genre_tree15.songUnplayableReason)(track) !== null;
1564
1588
  if (isUnplayable) {
1565
- return /* @__PURE__ */ (0, import_jsx_runtime27.jsxs)("li", { className: "flex min-h-8 items-center gap-1 px-1 opacity-50", children: [
1589
+ return /* @__PURE__ */ (0, import_jsx_runtime28.jsxs)("li", { className: "flex min-h-8 items-center gap-1 px-1 opacity-50", children: [
1566
1590
  leading,
1567
- /* @__PURE__ */ (0, import_jsx_runtime27.jsx)(
1591
+ /* @__PURE__ */ (0, import_jsx_runtime28.jsx)(
1568
1592
  "button",
1569
1593
  {
1570
1594
  type: "button",
@@ -1572,27 +1596,27 @@ function GenrePlaylistTracks({
1572
1596
  "aria-disabled": "true",
1573
1597
  "aria-label": label,
1574
1598
  className: "flex h-6 w-6 shrink-0 items-center justify-center rounded-full opacity-0 cursor-not-allowed",
1575
- children: /* @__PURE__ */ (0, import_jsx_runtime27.jsx)(import_fa2.FaPlay, { size: 10 })
1599
+ children: /* @__PURE__ */ (0, import_jsx_runtime28.jsx)(import_fa2.FaPlay, { size: 10 })
1576
1600
  }
1577
1601
  ),
1578
1602
  text,
1579
1603
  actions
1580
1604
  ] }, track.uuid);
1581
1605
  }
1582
- return /* @__PURE__ */ (0, import_jsx_runtime27.jsxs)(
1606
+ return /* @__PURE__ */ (0, import_jsx_runtime28.jsxs)(
1583
1607
  "li",
1584
1608
  {
1585
1609
  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)]" : ""}`,
1586
1610
  children: [
1587
1611
  leading,
1588
- /* @__PURE__ */ (0, import_jsx_runtime27.jsx)(
1612
+ /* @__PURE__ */ (0, import_jsx_runtime28.jsx)(
1589
1613
  "button",
1590
1614
  {
1591
1615
  type: "button",
1592
1616
  "aria-label": `${isPlaying ? "Pause" : "Play"} ${label}`,
1593
1617
  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"}`,
1594
1618
  onClick: () => isCurrent ? handlePlayPauseAction() : play(track.uuid),
1595
- children: isCurrent && playState === import_player6.PlayStates.LOADING ? /* @__PURE__ */ (0, import_jsx_runtime27.jsx)(import_ui8.RingLoader, { size: 10 }) : isPlaying ? /* @__PURE__ */ (0, import_jsx_runtime27.jsx)(import_fa2.FaPause, { size: 10 }) : /* @__PURE__ */ (0, import_jsx_runtime27.jsx)(import_fa2.FaPlay, { size: 10 })
1619
+ children: isCurrent && playState === import_player6.PlayStates.LOADING ? /* @__PURE__ */ (0, import_jsx_runtime28.jsx)(import_ui8.RingLoader, { size: 10 }) : isPlaying ? /* @__PURE__ */ (0, import_jsx_runtime28.jsx)(import_fa2.FaPause, { size: 10 }) : /* @__PURE__ */ (0, import_jsx_runtime28.jsx)(import_fa2.FaPlay, { size: 10 })
1596
1620
  }
1597
1621
  ),
1598
1622
  text,
@@ -1602,7 +1626,7 @@ function GenrePlaylistTracks({
1602
1626
  track.uuid
1603
1627
  );
1604
1628
  }),
1605
- hasNextPage && /* @__PURE__ */ (0, import_jsx_runtime27.jsx)("li", { ref: setSentinel, "aria-hidden": "true", className: "h-4" })
1629
+ hasNextPage && /* @__PURE__ */ (0, import_jsx_runtime28.jsx)("li", { ref: setSentinel, "aria-hidden": "true", className: "h-4" })
1606
1630
  ] })
1607
1631
  ] }) });
1608
1632
  }
@@ -1610,7 +1634,7 @@ function GenrePlaylistTracks({
1610
1634
  // src/genre-tree/GenreDetailExtras.tsx
1611
1635
  var import_track_noun2 = require("@behindthemusictree/app-kit/track-noun");
1612
1636
  var import_si2 = require("react-icons/si");
1613
- var import_jsx_runtime28 = require("react/jsx-runtime");
1637
+ var import_jsx_runtime29 = require("react/jsx-runtime");
1614
1638
  function GenreDetailExtras({
1615
1639
  genreUuid,
1616
1640
  genrePlaylist,
@@ -1636,17 +1660,18 @@ function GenreDetailExtras({
1636
1660
  criteriaOverviewSchema
1637
1661
  );
1638
1662
  if (isPending) {
1639
- return /* @__PURE__ */ (0, import_jsx_runtime28.jsxs)(import_jsx_runtime28.Fragment, { children: [
1640
- /* @__PURE__ */ (0, import_jsx_runtime28.jsx)(import_ui9.PanelSection, { title: "Summary", children: /* @__PURE__ */ (0, import_jsx_runtime28.jsx)("div", { "aria-busy": "true", "data-testid": "genre-detail-extras-skeleton", children: /* @__PURE__ */ (0, import_jsx_runtime28.jsx)(import_ui9.Skeleton, { className: "h-4 w-3/4" }) }) }),
1663
+ return /* @__PURE__ */ (0, import_jsx_runtime29.jsxs)(import_jsx_runtime29.Fragment, { children: [
1664
+ /* @__PURE__ */ (0, import_jsx_runtime29.jsx)(import_ui9.PanelSection, { title: "Summary", children: /* @__PURE__ */ (0, import_jsx_runtime29.jsx)("div", { "aria-busy": "true", "data-testid": "genre-detail-extras-skeleton", children: /* @__PURE__ */ (0, import_jsx_runtime29.jsx)(import_ui9.Skeleton, { className: "h-4 w-3/4" }) }) }),
1641
1665
  family
1642
1666
  ] });
1643
1667
  }
1644
- if (!overview) return /* @__PURE__ */ (0, import_jsx_runtime28.jsx)(import_jsx_runtime28.Fragment, { children: family });
1645
- const { summary, summarySourceUrl, essentialSongs } = overview;
1668
+ if (!overview) return /* @__PURE__ */ (0, import_jsx_runtime29.jsx)(import_jsx_runtime29.Fragment, { children: family });
1669
+ const { summary, summarySourceUrl, essentialSongs, image } = overview;
1646
1670
  const wikidataUrl = wikidataId ? `https://www.wikidata.org/wiki/${encodeURIComponent(wikidataId)}` : null;
1647
- return /* @__PURE__ */ (0, import_jsx_runtime28.jsxs)(import_jsx_runtime28.Fragment, { children: [
1648
- (summarySourceUrl || wikidataUrl) && /* @__PURE__ */ (0, import_jsx_runtime28.jsx)(import_ui9.PanelSection, { title: "Links", children: /* @__PURE__ */ (0, import_jsx_runtime28.jsxs)("div", { className: "flex flex-wrap gap-x-3 gap-y-1 text-caption text-[var(--color-text)]", children: [
1649
- summarySourceUrl && /* @__PURE__ */ (0, import_jsx_runtime28.jsxs)(
1671
+ return /* @__PURE__ */ (0, import_jsx_runtime29.jsxs)(import_jsx_runtime29.Fragment, { children: [
1672
+ image && /* @__PURE__ */ (0, import_jsx_runtime29.jsx)(CommonsPhoto, { image, alt: overview.name, creditLabel: "Image" }),
1673
+ (summarySourceUrl || wikidataUrl) && /* @__PURE__ */ (0, import_jsx_runtime29.jsx)(import_ui9.PanelSection, { title: "Links", children: /* @__PURE__ */ (0, import_jsx_runtime29.jsxs)("div", { className: "flex flex-wrap gap-x-3 gap-y-1 text-caption text-[var(--color-text)]", children: [
1674
+ summarySourceUrl && /* @__PURE__ */ (0, import_jsx_runtime29.jsxs)(
1650
1675
  "a",
1651
1676
  {
1652
1677
  href: summarySourceUrl,
@@ -1654,12 +1679,12 @@ function GenreDetailExtras({
1654
1679
  rel: "noopener noreferrer",
1655
1680
  className: "inline-flex items-center gap-1.5 text-[var(--color-text-muted)] hover:text-[var(--color-text)]",
1656
1681
  children: [
1657
- /* @__PURE__ */ (0, import_jsx_runtime28.jsx)(import_si2.SiWikipedia, { "aria-hidden": "true", className: "h-3.5 w-3.5" }),
1682
+ /* @__PURE__ */ (0, import_jsx_runtime29.jsx)(import_si2.SiWikipedia, { "aria-hidden": "true", className: "h-3.5 w-3.5" }),
1658
1683
  "Wikipedia"
1659
1684
  ]
1660
1685
  }
1661
1686
  ),
1662
- wikidataUrl && /* @__PURE__ */ (0, import_jsx_runtime28.jsxs)(
1687
+ wikidataUrl && /* @__PURE__ */ (0, import_jsx_runtime29.jsxs)(
1663
1688
  "a",
1664
1689
  {
1665
1690
  href: wikidataUrl,
@@ -1667,17 +1692,17 @@ function GenreDetailExtras({
1667
1692
  rel: "noopener noreferrer",
1668
1693
  className: "inline-flex items-center gap-1.5 text-[var(--color-text-muted)] hover:text-[var(--color-text)]",
1669
1694
  children: [
1670
- /* @__PURE__ */ (0, import_jsx_runtime28.jsx)(import_si2.SiWikidata, { "aria-hidden": "true", className: "h-3.5 w-3.5" }),
1695
+ /* @__PURE__ */ (0, import_jsx_runtime29.jsx)(import_si2.SiWikidata, { "aria-hidden": "true", className: "h-3.5 w-3.5" }),
1671
1696
  "Wikidata"
1672
1697
  ]
1673
1698
  }
1674
1699
  )
1675
1700
  ] }) }),
1676
- /* @__PURE__ */ (0, import_jsx_runtime28.jsx)(import_ui9.PanelSection, { title: "Summary", children: renderSummary ? renderSummary(overview) : /* @__PURE__ */ (0, import_jsx_runtime28.jsx)(ClampedText, { className: "text-body text-[var(--color-text)]", children: summary ?? "\u2014" }, genreUuid) }),
1701
+ /* @__PURE__ */ (0, import_jsx_runtime29.jsx)(import_ui9.PanelSection, { title: "Summary", children: renderSummary ? renderSummary(overview) : /* @__PURE__ */ (0, import_jsx_runtime29.jsx)(ClampedText, { className: "text-body text-[var(--color-text)]", children: summary ?? "\u2014" }, genreUuid) }),
1677
1702
  family,
1678
1703
  renderGenreDetailExtras?.(overview),
1679
- (renderEssentialTracks || essentialSongs) && /* @__PURE__ */ (0, import_jsx_runtime28.jsx)(import_ui9.PanelSection, { title: `Essential ${trackNoun.plural}`, children: renderEssentialTracks ? renderEssentialTracks(overview) : essentialSongs && essentialSongs.length > 0 ? /* @__PURE__ */ (0, import_jsx_runtime28.jsx)("ul", { className: "text-body text-[var(--color-text)]", children: essentialSongs.map((track) => /* @__PURE__ */ (0, import_jsx_runtime28.jsx)("li", { className: "py-1", children: track.title }, track.uuid)) }) : /* @__PURE__ */ (0, import_jsx_runtime28.jsx)("p", { className: "text-body text-[var(--color-text)]", children: "\u2014" }) }),
1680
- /* @__PURE__ */ (0, import_jsx_runtime28.jsx)(
1704
+ (renderEssentialTracks || essentialSongs) && /* @__PURE__ */ (0, import_jsx_runtime29.jsx)(import_ui9.PanelSection, { title: `Essential ${trackNoun.plural}`, children: renderEssentialTracks ? renderEssentialTracks(overview) : essentialSongs && essentialSongs.length > 0 ? /* @__PURE__ */ (0, import_jsx_runtime29.jsx)("ul", { className: "text-body text-[var(--color-text)]", children: essentialSongs.map((track) => /* @__PURE__ */ (0, import_jsx_runtime29.jsx)("li", { className: "py-1", children: track.title }, track.uuid)) }) : /* @__PURE__ */ (0, import_jsx_runtime29.jsx)("p", { className: "text-body text-[var(--color-text)]", children: "\u2014" }) }),
1705
+ /* @__PURE__ */ (0, import_jsx_runtime29.jsx)(
1681
1706
  GenrePlaylistTracks,
1682
1707
  {
1683
1708
  genreUuid,
@@ -1695,10 +1720,10 @@ function GenreDetailExtras({
1695
1720
  }
1696
1721
 
1697
1722
  // src/genre-tree/GenreInfoPanelBody.tsx
1698
- var import_jsx_runtime29 = require("react/jsx-runtime");
1723
+ var import_jsx_runtime30 = require("react/jsx-runtime");
1699
1724
  var import_react14 = require("react");
1700
1725
  function GenreNodeActionBand({ genrePlaylist, playback, ...bandProps }) {
1701
- return /* @__PURE__ */ (0, import_jsx_runtime29.jsx)(GenreActionBand, { ...bandProps, playActions: /* @__PURE__ */ (0, import_jsx_runtime29.jsx)(GenrePlayActions, { genrePlaylist, playback }) });
1726
+ return /* @__PURE__ */ (0, import_jsx_runtime30.jsx)(GenreActionBand, { ...bandProps, playActions: /* @__PURE__ */ (0, import_jsx_runtime30.jsx)(GenrePlayActions, { genrePlaylist, playback }) });
1702
1727
  }
1703
1728
  function GenreInfoPanelBody({
1704
1729
  toolbar,
@@ -1715,9 +1740,9 @@ function GenreInfoPanelBody({
1715
1740
  }) {
1716
1741
  const summary = scope === "reference" ? import_genre_tree19.GenreCatalogueCountsSchema.parse(genrePlaylist ?? catalogue) : void 0;
1717
1742
  const selection = summary ? (0, import_genre_tree19.defaultGenreMusicSelection)(summary) : void 0;
1718
- return /* @__PURE__ */ (0, import_jsx_runtime29.jsxs)(import_jsx_runtime29.Fragment, { children: [
1743
+ return /* @__PURE__ */ (0, import_jsx_runtime30.jsxs)(import_jsx_runtime30.Fragment, { children: [
1719
1744
  toolbar,
1720
- genrePlaylist ? /* @__PURE__ */ (0, import_jsx_runtime29.jsx)(GenreNodeActionBand, { ...band, genrePlaylist, playback }) : /* @__PURE__ */ (0, import_jsx_runtime29.jsx)(GenreActionBand, { ...band }),
1745
+ genrePlaylist ? /* @__PURE__ */ (0, import_jsx_runtime30.jsx)(GenreNodeActionBand, { ...band, genrePlaylist, playback }) : /* @__PURE__ */ (0, import_jsx_runtime30.jsx)(GenreActionBand, { ...band }),
1721
1746
  /* @__PURE__ */ (0, import_react14.createElement)(
1722
1747
  GenreDetailExtras,
1723
1748
  {
@@ -1737,7 +1762,7 @@ function GenreInfoPanelBody({
1737
1762
  }
1738
1763
 
1739
1764
  // src/genre-tree/GenreDetailPanel.tsx
1740
- var import_jsx_runtime30 = require("react/jsx-runtime");
1765
+ var import_jsx_runtime31 = require("react/jsx-runtime");
1741
1766
  var RELATIVE_ITEM_COUNT = 1;
1742
1767
  function toNode(genre, parentId) {
1743
1768
  return {
@@ -1762,7 +1787,8 @@ function useGenreDetailContent({
1762
1787
  expanded,
1763
1788
  treeStyles
1764
1789
  }) {
1765
- const { data: detail, isPending, isError } = (0, import_genre_tree20.useFetchGenreDetail)(genreUuid, scope, getBackendBaseUrl);
1790
+ const result = (0, import_genre_tree20.useFetchGenreDetail)(genreUuid, scope, getBackendBaseUrl);
1791
+ const detail = result.status === "success" ? result.genre : void 0;
1766
1792
  const defaultPlayback = useGenrePlaylistPlayback({ scope, genrePlaylists: [], artist: extras?.artist });
1767
1793
  const playback = hostPlayback ?? defaultPlayback;
1768
1794
  const view = (0, import_react15.useMemo)(() => {
@@ -1791,22 +1817,27 @@ function useGenreDetailContent({
1791
1817
  )
1792
1818
  };
1793
1819
  }, [detail, scope, treeStyles]);
1794
- if (isError) return {
1820
+ if (result.status === "not-found") return {
1821
+ name: null,
1822
+ title: "Genre not found",
1823
+ content: /* @__PURE__ */ (0, import_jsx_runtime31.jsx)("p", { className: "text-[var(--color-text-muted)]", children: "This genre does not exist." })
1824
+ };
1825
+ if (result.status === "error") return {
1795
1826
  name: null,
1796
1827
  title: "Genre unavailable",
1797
- content: /* @__PURE__ */ (0, import_jsx_runtime30.jsx)("p", { role: "alert", className: "text-body text-[var(--color-text)]", children: "This genre could not be loaded." })
1828
+ content: /* @__PURE__ */ (0, import_jsx_runtime31.jsx)("p", { role: "alert", className: "text-body text-[var(--color-text)]", children: "This genre could not be loaded." })
1798
1829
  };
1799
- if (isPending || !detail || !view) return {
1830
+ if (!detail || !view) return {
1800
1831
  name: null,
1801
1832
  title: "Loading genre\u2026",
1802
- content: /* @__PURE__ */ (0, import_jsx_runtime30.jsxs)("div", { className: "flex flex-col gap-3", "aria-busy": "true", "data-testid": "genre-detail-panel-skeleton", children: [
1803
- /* @__PURE__ */ (0, import_jsx_runtime30.jsx)(import_ui10.Skeleton, { className: "h-6 w-2/3" }),
1804
- /* @__PURE__ */ (0, import_jsx_runtime30.jsx)(import_ui10.Skeleton, { className: "h-4 w-full" }),
1805
- /* @__PURE__ */ (0, import_jsx_runtime30.jsx)(import_ui10.Skeleton, { className: "h-4 w-3/4" })
1833
+ content: /* @__PURE__ */ (0, import_jsx_runtime31.jsxs)("div", { className: "flex flex-col gap-3", "aria-busy": "true", "data-testid": "genre-detail-panel-skeleton", children: [
1834
+ /* @__PURE__ */ (0, import_jsx_runtime31.jsx)(import_ui10.Skeleton, { className: "h-6 w-2/3" }),
1835
+ /* @__PURE__ */ (0, import_jsx_runtime31.jsx)(import_ui10.Skeleton, { className: "h-4 w-full" }),
1836
+ /* @__PURE__ */ (0, import_jsx_runtime31.jsx)(import_ui10.Skeleton, { className: "h-4 w-3/4" })
1806
1837
  ] })
1807
1838
  };
1808
1839
  const selectGenre = (nodeId) => onSelectGenre(import_genre_tree20.GenreUuidSchema.parse(nodeId));
1809
- const family = /* @__PURE__ */ (0, import_jsx_runtime30.jsx)(
1840
+ const family = /* @__PURE__ */ (0, import_jsx_runtime31.jsx)(
1810
1841
  import_genre_tree_view4.GenreNodeFamily,
1811
1842
  {
1812
1843
  node: view.node,
@@ -1817,17 +1848,17 @@ function useGenreDetailContent({
1817
1848
  onSelectNode: selectGenre
1818
1849
  }
1819
1850
  );
1820
- const ancestors = /* @__PURE__ */ (0, import_jsx_runtime30.jsx)(import_genre_tree_view4.GenreNodeAncestors, { ancestorNodes: view.ancestorNodes, onSelectNode: selectGenre });
1851
+ const ancestors = /* @__PURE__ */ (0, import_jsx_runtime31.jsx)(import_genre_tree_view4.GenreNodeAncestors, { ancestorNodes: view.ancestorNodes, onSelectNode: selectGenre });
1821
1852
  return {
1822
1853
  name: detail.name,
1823
1854
  title: detail.name,
1824
1855
  headerAccent: { background: view.fill, color: view.textColor },
1825
1856
  aside: expanded ? ancestors : void 0,
1826
- content: /* @__PURE__ */ (0, import_jsx_runtime30.jsxs)(import_jsx_runtime30.Fragment, { children: [
1827
- /* @__PURE__ */ (0, import_jsx_runtime30.jsx)(
1857
+ content: /* @__PURE__ */ (0, import_jsx_runtime31.jsxs)(import_jsx_runtime31.Fragment, { children: [
1858
+ /* @__PURE__ */ (0, import_jsx_runtime31.jsx)(
1828
1859
  GenreInfoPanelBody,
1829
1860
  {
1830
- family: /* @__PURE__ */ (0, import_jsx_runtime30.jsxs)(import_jsx_runtime30.Fragment, { children: [
1861
+ family: /* @__PURE__ */ (0, import_jsx_runtime31.jsxs)(import_jsx_runtime31.Fragment, { children: [
1831
1862
  family,
1832
1863
  !expanded && ancestors
1833
1864
  ] }),
@@ -1871,7 +1902,7 @@ function GenreDetailPanel({
1871
1902
  expanded,
1872
1903
  band: { onZoomIn, shareUrl, shareTarget: shareUrl ? shareTarget : void 0 }
1873
1904
  });
1874
- return /* @__PURE__ */ (0, import_jsx_runtime30.jsx)(
1905
+ return /* @__PURE__ */ (0, import_jsx_runtime31.jsx)(
1875
1906
  import_ui10.DetailPanel,
1876
1907
  {
1877
1908
  side: "left",
@@ -1884,7 +1915,7 @@ function GenreDetailPanel({
1884
1915
  edgeTab,
1885
1916
  aside: panel.aside,
1886
1917
  titleInSearch,
1887
- headerActions: onExpandedChange ? /* @__PURE__ */ (0, import_jsx_runtime30.jsx)(import_ui10.IconButton, { "aria-label": expanded ? "Shrink panel" : "Extend panel", onClick: () => onExpandedChange(!expanded), children: expanded ? /* @__PURE__ */ (0, import_jsx_runtime30.jsx)(import_lucide_react9.Minimize2, { "aria-hidden": "true", className: "h-4 w-4" }) : /* @__PURE__ */ (0, import_jsx_runtime30.jsx)(import_lucide_react9.Maximize2, { "aria-hidden": "true", className: "h-4 w-4" }) }) : void 0,
1918
+ headerActions: onExpandedChange ? /* @__PURE__ */ (0, import_jsx_runtime31.jsx)(import_ui10.IconButton, { "aria-label": expanded ? "Shrink panel" : "Extend panel", onClick: () => onExpandedChange(!expanded), children: expanded ? /* @__PURE__ */ (0, import_jsx_runtime31.jsx)(import_lucide_react9.Minimize2, { "aria-hidden": "true", className: "h-4 w-4" }) : /* @__PURE__ */ (0, import_jsx_runtime31.jsx)(import_lucide_react9.Maximize2, { "aria-hidden": "true", className: "h-4 w-4" }) }) : void 0,
1888
1919
  children: panel.content
1889
1920
  }
1890
1921
  );
@@ -1893,7 +1924,7 @@ function GenreDetailPanel({
1893
1924
  // src/genre-tree/ChangeParentPopup.tsx
1894
1925
  var import_react16 = require("react");
1895
1926
  var import_genre_tree21 = require("@behindthemusictree/app-kit/genre-tree");
1896
- var import_jsx_runtime31 = require("react/jsx-runtime");
1927
+ var import_jsx_runtime32 = require("react/jsx-runtime");
1897
1928
  function ChangeParentPopup({
1898
1929
  genrePlaylist,
1899
1930
  genrePlaylists,
@@ -1928,12 +1959,12 @@ function ChangeParentPopup({
1928
1959
  window.addEventListener("keydown", handleKeyDown);
1929
1960
  return () => window.removeEventListener("keydown", handleKeyDown);
1930
1961
  }, [onClose]);
1931
- return /* @__PURE__ */ (0, import_jsx_runtime31.jsxs)(BasePopup_default, { title: `Change parent of ${genrePlaylist.name}`, onClose, showCancelButton: true, fitContent: true, children: [
1932
- /* @__PURE__ */ (0, import_jsx_runtime31.jsxs)("p", { className: "mb-3 text-body text-[var(--color-text-muted)]", children: [
1962
+ return /* @__PURE__ */ (0, import_jsx_runtime32.jsxs)(BasePopup_default, { title: `Change parent of ${genrePlaylist.name}`, onClose, showCancelButton: true, fitContent: true, children: [
1963
+ /* @__PURE__ */ (0, import_jsx_runtime32.jsxs)("p", { className: "mb-3 text-body text-[var(--color-text-muted)]", children: [
1933
1964
  "Current parent: ",
1934
1965
  genrePlaylist.parent?.name ?? "none"
1935
1966
  ] }),
1936
- /* @__PURE__ */ (0, import_jsx_runtime31.jsx)("div", { "aria-busy": isPending, className: isPending ? "pointer-events-none opacity-60" : void 0, children: /* @__PURE__ */ (0, import_jsx_runtime31.jsx)(
1967
+ /* @__PURE__ */ (0, import_jsx_runtime32.jsx)("div", { "aria-busy": isPending, className: isPending ? "pointer-events-none opacity-60" : void 0, children: /* @__PURE__ */ (0, import_jsx_runtime32.jsx)(
1937
1968
  GenreSearch,
1938
1969
  {
1939
1970
  genrePlaylists: candidates,
@@ -1941,7 +1972,7 @@ function ChangeParentPopup({
1941
1972
  placeholder: "Search a new parent\u2026"
1942
1973
  }
1943
1974
  ) }),
1944
- formErrors.length > 0 && /* @__PURE__ */ (0, import_jsx_runtime31.jsx)("p", { role: "alert", className: "mt-3 text-body text-[var(--color-red-500)]", children: formErrors.map(({ message }) => message).join(" ") })
1975
+ formErrors.length > 0 && /* @__PURE__ */ (0, import_jsx_runtime32.jsx)("p", { role: "alert", className: "mt-3 text-body text-[var(--color-red-500)]", children: formErrors.map(({ message }) => message).join(" ") })
1945
1976
  ] });
1946
1977
  }
1947
1978
 
@@ -1954,7 +1985,7 @@ var import_lucide_react10 = require("lucide-react");
1954
1985
  var import_ui11 = require("@behindthemusictree/ui");
1955
1986
  var import_genre_tree22 = require("@behindthemusictree/app-kit/genre-tree");
1956
1987
  var import_player7 = require("@behindthemusictree/app-kit/player");
1957
- var import_jsx_runtime32 = require("react/jsx-runtime");
1988
+ var import_jsx_runtime33 = require("react/jsx-runtime");
1958
1989
  function ArtistActionBand({ artist, share }) {
1959
1990
  const { trackList, playNewTrackListFromArtist } = (0, import_genre_tree22.useTrackList)();
1960
1991
  const { isPlaying, setIsPlaying } = (0, import_player7.usePlayer)();
@@ -1970,11 +2001,11 @@ function ArtistActionBand({ artist, share }) {
1970
2001
  console.error("Failed to load artist tracks:", error);
1971
2002
  });
1972
2003
  };
1973
- return /* @__PURE__ */ (0, import_jsx_runtime32.jsx)(import_ui11.PanelBleed, { children: /* @__PURE__ */ (0, import_jsx_runtime32.jsxs)(import_ui11.ActionBand, { children: [
1974
- /* @__PURE__ */ (0, import_jsx_runtime32.jsx)(
2004
+ return /* @__PURE__ */ (0, import_jsx_runtime33.jsx)(import_ui11.PanelBleed, { children: /* @__PURE__ */ (0, import_jsx_runtime33.jsxs)(import_ui11.ActionBand, { children: [
2005
+ /* @__PURE__ */ (0, import_jsx_runtime33.jsx)(
1975
2006
  import_ui11.ActionButton,
1976
2007
  {
1977
- icon: playing ? /* @__PURE__ */ (0, import_jsx_runtime32.jsx)(import_lucide_react10.Pause, { "aria-hidden": "true", className: "w-5 h-5" }) : /* @__PURE__ */ (0, import_jsx_runtime32.jsx)(import_lucide_react10.Play, { "aria-hidden": "true", className: "w-5 h-5" }),
2008
+ icon: playing ? /* @__PURE__ */ (0, import_jsx_runtime33.jsx)(import_lucide_react10.Pause, { "aria-hidden": "true", className: "w-5 h-5" }) : /* @__PURE__ */ (0, import_jsx_runtime33.jsx)(import_lucide_react10.Play, { "aria-hidden": "true", className: "w-5 h-5" }),
1978
2009
  label: playing ? "Pause" : "Play",
1979
2010
  caption: String(artist.playableTracksCount),
1980
2011
  disabled: artist.playableTracksCount === 0,
@@ -1993,7 +2024,7 @@ var import_ui13 = require("@behindthemusictree/ui");
1993
2024
  var import_lucide_react11 = require("lucide-react");
1994
2025
  var import_si3 = require("react-icons/si");
1995
2026
  var import_ui12 = require("@behindthemusictree/ui");
1996
- var import_jsx_runtime33 = require("react/jsx-runtime");
2027
+ var import_jsx_runtime34 = require("react/jsx-runtime");
1997
2028
  var LINK_KINDS = {
1998
2029
  official: { label: "Official website", Icon: import_lucide_react11.Globe },
1999
2030
  wikipedia: { label: "Wikipedia", Icon: import_si3.SiWikipedia },
@@ -2030,7 +2061,7 @@ function artistLinks(artist) {
2030
2061
  return links;
2031
2062
  }
2032
2063
  function ArtistLinks({ links }) {
2033
- return /* @__PURE__ */ (0, import_jsx_runtime33.jsx)("ul", { className: "flex flex-wrap gap-1.5", children: links.map(({ key, url, Icon, label }) => /* @__PURE__ */ (0, import_jsx_runtime33.jsx)("li", { children: /* @__PURE__ */ (0, import_jsx_runtime33.jsx)(import_ui12.Chip, { href: url, icon: /* @__PURE__ */ (0, import_jsx_runtime33.jsx)(Icon, { className: "h-3.5 w-3.5 shrink-0", "aria-hidden": "true" }), children: label }) }, key)) });
2064
+ return /* @__PURE__ */ (0, import_jsx_runtime34.jsx)("ul", { className: "flex flex-wrap gap-1.5", children: links.map(({ key, url, Icon, label }) => /* @__PURE__ */ (0, import_jsx_runtime34.jsx)("li", { children: /* @__PURE__ */ (0, import_jsx_runtime34.jsx)(import_ui12.Chip, { href: url, icon: /* @__PURE__ */ (0, import_jsx_runtime34.jsx)(Icon, { className: "h-3.5 w-3.5 shrink-0", "aria-hidden": "true" }), children: label }) }, key)) });
2034
2065
  }
2035
2066
 
2036
2067
  // src/genre-tree/artist/formatYears.ts
@@ -2041,45 +2072,59 @@ function formatYears(beginYear, endYear, ended) {
2041
2072
  }
2042
2073
 
2043
2074
  // src/genre-tree/artist/ArtistHeader.tsx
2044
- var import_jsx_runtime34 = require("react/jsx-runtime");
2075
+ var import_jsx_runtime35 = require("react/jsx-runtime");
2045
2076
  var countryNames = new Intl.DisplayNames(["en"], { type: "region" });
2046
- function ArtistHeader({ artist }) {
2077
+ function originLabel(artist, origin) {
2078
+ if (origin.source === "area") return "From";
2079
+ if (artist.type === "person") return "Born in";
2080
+ if (artist.type === "group") return "Formed in";
2081
+ return "From";
2082
+ }
2083
+ function OriginValue({
2084
+ origin,
2085
+ country,
2086
+ renderPlaceLink
2087
+ }) {
2088
+ const place = renderPlaceLink && origin.regionId !== null ? renderPlaceLink({ ...origin, regionId: origin.regionId }) : origin.name;
2089
+ return /* @__PURE__ */ (0, import_jsx_runtime35.jsxs)(import_jsx_runtime35.Fragment, { children: [
2090
+ place,
2091
+ country && origin.kind !== "country" && `, ${country}`
2092
+ ] });
2093
+ }
2094
+ function ArtistHeader({
2095
+ artist,
2096
+ renderPlaceLink
2097
+ }) {
2047
2098
  const fields = [];
2048
2099
  if (artist.type) fields.push({ label: "Type", value: artist.type.charAt(0).toUpperCase() + artist.type.slice(1) });
2049
- const country = artist.country && countryNames.of(artist.country);
2050
- if (country) fields.push({ label: "Country", value: country });
2100
+ const country = artist.country ? countryNames.of(artist.country) : void 0;
2101
+ if (artist.origin) {
2102
+ fields.push({
2103
+ label: originLabel(artist, artist.origin),
2104
+ value: /* @__PURE__ */ (0, import_jsx_runtime35.jsx)(OriginValue, { origin: artist.origin, country, renderPlaceLink })
2105
+ });
2106
+ }
2107
+ if (country && !artist.origin) fields.push({ label: "Country", value: country });
2051
2108
  const years = formatYears(artist.beginYear, artist.endYear, artist.ended);
2052
2109
  if (years) fields.push({ label: "Active", value: years });
2053
2110
  const links = artistLinks(artist);
2054
- if (links.length > 0) fields.push({ label: "Links", value: /* @__PURE__ */ (0, import_jsx_runtime34.jsx)(ArtistLinks, { links }) });
2111
+ if (links.length > 0) fields.push({ label: "Links", value: /* @__PURE__ */ (0, import_jsx_runtime35.jsx)(ArtistLinks, { links }) });
2055
2112
  if (fields.length === 0) return null;
2056
- return /* @__PURE__ */ (0, import_jsx_runtime34.jsx)(import_ui13.FieldList, { items: fields });
2113
+ return /* @__PURE__ */ (0, import_jsx_runtime35.jsx)(import_ui13.FieldList, { items: fields });
2057
2114
  }
2058
2115
 
2059
2116
  // src/genre-tree/artist/ArtistPhoto.tsx
2060
- var import_jsx_runtime35 = require("react/jsx-runtime");
2061
- function ExternalLink({ href, children }) {
2062
- return /* @__PURE__ */ (0, import_jsx_runtime35.jsx)("a", { href, target: "_blank", rel: "noopener noreferrer", className: "underline-offset-2 hover:underline", children });
2063
- }
2117
+ var import_jsx_runtime36 = require("react/jsx-runtime");
2064
2118
  function ArtistPhoto({ image, name }) {
2065
- return /* @__PURE__ */ (0, import_jsx_runtime35.jsxs)("figure", { className: "flex flex-col gap-1.5", children: [
2066
- /* @__PURE__ */ (0, import_jsx_runtime35.jsx)("img", { src: image.url, alt: name, className: "h-64 w-full object-contain bg-[var(--color-surface-muted)] rounded" }),
2067
- /* @__PURE__ */ (0, import_jsx_runtime35.jsxs)("figcaption", { className: "text-caption text-[var(--color-text-muted)]", children: [
2068
- "Photo: ",
2069
- /* @__PURE__ */ (0, import_jsx_runtime35.jsx)(ExternalLink, { href: image.pageUrl, children: image.author ?? "Wikimedia Commons" }),
2070
- ",",
2071
- " ",
2072
- image.licenseUrl ? /* @__PURE__ */ (0, import_jsx_runtime35.jsx)(ExternalLink, { href: image.licenseUrl, children: image.license }) : image.license
2073
- ] })
2074
- ] });
2119
+ return /* @__PURE__ */ (0, import_jsx_runtime36.jsx)(CommonsPhoto, { image, alt: name });
2075
2120
  }
2076
2121
 
2077
2122
  // src/genre-tree/artist/ArtistMembers.tsx
2078
2123
  var import_ui14 = require("@behindthemusictree/ui");
2079
- var import_jsx_runtime36 = require("react/jsx-runtime");
2124
+ var import_jsx_runtime37 = require("react/jsx-runtime");
2080
2125
  function MembershipList({ title, memberships, renderArtistLink }) {
2081
2126
  if (memberships.length === 0) return null;
2082
- return /* @__PURE__ */ (0, import_jsx_runtime36.jsx)(import_ui14.PanelGroup, { title, children: /* @__PURE__ */ (0, import_jsx_runtime36.jsx)("ul", { children: memberships.map((membership, index) => {
2127
+ return /* @__PURE__ */ (0, import_jsx_runtime37.jsx)(import_ui14.PanelGroup, { title, children: /* @__PURE__ */ (0, import_jsx_runtime37.jsx)("ul", { children: memberships.map((membership, index) => {
2083
2128
  const details = [
2084
2129
  formatYears(membership.beginYear, membership.endYear, membership.ended),
2085
2130
  ...membership.roles
@@ -2087,9 +2132,9 @@ function MembershipList({ title, memberships, renderArtistLink }) {
2087
2132
  const detailsText = details.filter(Boolean).join(" \xB7 ");
2088
2133
  return (
2089
2134
  // A member can have several periods in the same group.
2090
- /* @__PURE__ */ (0, import_jsx_runtime36.jsxs)("li", { className: "truncate py-0.5", title: detailsText || void 0, children: [
2091
- /* @__PURE__ */ (0, import_jsx_runtime36.jsx)("span", { className: "font-medium text-[var(--color-text)]", children: renderArtistLink(membership.artist) }),
2092
- detailsText && /* @__PURE__ */ (0, import_jsx_runtime36.jsxs)("span", { className: "text-[var(--color-text-muted)]", children: [
2135
+ /* @__PURE__ */ (0, import_jsx_runtime37.jsxs)("li", { className: "truncate py-0.5", title: detailsText || void 0, children: [
2136
+ /* @__PURE__ */ (0, import_jsx_runtime37.jsx)("span", { className: "font-medium text-[var(--color-text)]", children: renderArtistLink(membership.artist) }),
2137
+ detailsText && /* @__PURE__ */ (0, import_jsx_runtime37.jsxs)("span", { className: "text-[var(--color-text-muted)]", children: [
2093
2138
  " \u2014 ",
2094
2139
  detailsText
2095
2140
  ] })
@@ -2100,9 +2145,9 @@ function MembershipList({ title, memberships, renderArtistLink }) {
2100
2145
  function ArtistMembers({ title, memberships, renderArtistLink }) {
2101
2146
  if (memberships.length === 0) return null;
2102
2147
  const isCurrent = (membership) => !membership.ended && membership.endYear === null;
2103
- return /* @__PURE__ */ (0, import_jsx_runtime36.jsxs)(import_ui14.PanelSection, { title, children: [
2104
- /* @__PURE__ */ (0, import_jsx_runtime36.jsx)(MembershipList, { title: "Current", memberships: memberships.filter(isCurrent), renderArtistLink }),
2105
- /* @__PURE__ */ (0, import_jsx_runtime36.jsx)(
2148
+ return /* @__PURE__ */ (0, import_jsx_runtime37.jsxs)(import_ui14.PanelSection, { title, children: [
2149
+ /* @__PURE__ */ (0, import_jsx_runtime37.jsx)(MembershipList, { title: "Current", memberships: memberships.filter(isCurrent), renderArtistLink }),
2150
+ /* @__PURE__ */ (0, import_jsx_runtime37.jsx)(
2106
2151
  MembershipList,
2107
2152
  {
2108
2153
  title: "Past",
@@ -2115,7 +2160,7 @@ function ArtistMembers({ title, memberships, renderArtistLink }) {
2115
2160
 
2116
2161
  // src/genre-tree/artist/ArtistDiscography.tsx
2117
2162
  var import_ui15 = require("@behindthemusictree/ui");
2118
- var import_jsx_runtime37 = require("react/jsx-runtime");
2163
+ var import_jsx_runtime38 = require("react/jsx-runtime");
2119
2164
  var GROUPS = ["Albums", "EPs", "Singles", "Live", "Compilations", "Other"];
2120
2165
  var PRIMARY_GROUPS = { album: "Albums", ep: "EPs", single: "Singles" };
2121
2166
  function discographyGroup({ primaryType, secondaryTypes }) {
@@ -2135,12 +2180,12 @@ function byYear(a, b) {
2135
2180
  function ArtistDiscography({ albums }) {
2136
2181
  if (albums.length === 0) return null;
2137
2182
  const sorted = [...albums].sort(byYear);
2138
- return /* @__PURE__ */ (0, import_jsx_runtime37.jsx)(import_ui15.PanelSection, { title: "Discography", children: GROUPS.map((group) => {
2183
+ return /* @__PURE__ */ (0, import_jsx_runtime38.jsx)(import_ui15.PanelSection, { title: "Discography", children: GROUPS.map((group) => {
2139
2184
  const groupAlbums = sorted.filter((album) => discographyGroup(album) === group);
2140
2185
  if (groupAlbums.length === 0) return null;
2141
- return /* @__PURE__ */ (0, import_jsx_runtime37.jsx)(import_ui15.PanelGroup, { title: group, children: /* @__PURE__ */ (0, import_jsx_runtime37.jsx)("ul", { children: groupAlbums.map((album) => /* @__PURE__ */ (0, import_jsx_runtime37.jsxs)("li", { className: "grid grid-cols-[2.5rem_minmax(0,1fr)] gap-2 py-0.5", children: [
2142
- /* @__PURE__ */ (0, import_jsx_runtime37.jsx)("span", { className: "tabular-nums text-[var(--color-text-muted)]", children: album.year ?? "\u2014" }),
2143
- /* @__PURE__ */ (0, import_jsx_runtime37.jsx)("span", { className: "truncate text-[var(--color-text)]", title: album.name, children: album.name })
2186
+ return /* @__PURE__ */ (0, import_jsx_runtime38.jsx)(import_ui15.PanelGroup, { title: group, children: /* @__PURE__ */ (0, import_jsx_runtime38.jsx)("ul", { children: groupAlbums.map((album) => /* @__PURE__ */ (0, import_jsx_runtime38.jsxs)("li", { className: "grid grid-cols-[2.5rem_minmax(0,1fr)] gap-2 py-0.5", children: [
2187
+ /* @__PURE__ */ (0, import_jsx_runtime38.jsx)("span", { className: "tabular-nums text-[var(--color-text-muted)]", children: album.year ?? "\u2014" }),
2188
+ /* @__PURE__ */ (0, import_jsx_runtime38.jsx)("span", { className: "truncate text-[var(--color-text)]", title: album.name, children: album.name })
2144
2189
  ] }, album.uuid)) }) }, group);
2145
2190
  }) });
2146
2191
  }
@@ -2161,7 +2206,7 @@ var import_genre_tree23 = require("@behindthemusictree/app-kit/genre-tree");
2161
2206
  var import_fa3 = require("react-icons/fa");
2162
2207
  var import_player8 = require("@behindthemusictree/app-kit/player");
2163
2208
  var import_player9 = require("@behindthemusictree/app-kit/player");
2164
- var import_jsx_runtime38 = require("react/jsx-runtime");
2209
+ var import_jsx_runtime39 = require("react/jsx-runtime");
2165
2210
  function TrackPositionPlayPause({
2166
2211
  position,
2167
2212
  uuid,
@@ -2175,14 +2220,14 @@ function TrackPositionPlayPause({
2175
2220
  const isPlaying = isThisTrack && isConfirmedPlayable && playState === import_player9.PlayStates.PLAYING;
2176
2221
  const isPaused = isThisTrack && isConfirmedPlayable && playState === import_player9.PlayStates.PAUSED;
2177
2222
  const isCurrentTrackActive = isPlaying || isPaused;
2178
- return /* @__PURE__ */ (0, import_jsx_runtime38.jsxs)(
2223
+ return /* @__PURE__ */ (0, import_jsx_runtime39.jsxs)(
2179
2224
  "div",
2180
2225
  {
2181
2226
  className: "track-position-play-pause relative flex items-center justify-center gap-2 text-lg w-16 cursor-pointer",
2182
2227
  style: { minWidth: "4rem", maxWidth: "4rem" },
2183
2228
  onClick: handlePlayPauseClick,
2184
2229
  children: [
2185
- /* @__PURE__ */ (0, import_jsx_runtime38.jsx)("div", { className: "track-position-play-pause__status group-hover:hidden flex items-center justify-center", children: isAwaitingPlayback ? /* @__PURE__ */ (0, import_jsx_runtime38.jsx)("span", { className: "track-position-play-pause__spinner", role: "status", "aria-label": "Loading track" }) : isCurrentTrackActive ? /* @__PURE__ */ (0, import_jsx_runtime38.jsxs)(
2230
+ /* @__PURE__ */ (0, import_jsx_runtime39.jsx)("div", { className: "track-position-play-pause__status group-hover:hidden flex items-center justify-center", children: isAwaitingPlayback ? /* @__PURE__ */ (0, import_jsx_runtime39.jsx)("span", { className: "track-position-play-pause__spinner", role: "status", "aria-label": "Loading track" }) : isCurrentTrackActive ? /* @__PURE__ */ (0, import_jsx_runtime39.jsxs)(
2186
2231
  "span",
2187
2232
  {
2188
2233
  className: "track-position-play-pause__bars",
@@ -2190,18 +2235,18 @@ function TrackPositionPlayPause({
2190
2235
  role: "img",
2191
2236
  "aria-label": isPlaying ? "Playing" : "Paused",
2192
2237
  children: [
2193
- /* @__PURE__ */ (0, import_jsx_runtime38.jsx)("span", { className: "track-position-play-pause__bar" }),
2194
- /* @__PURE__ */ (0, import_jsx_runtime38.jsx)("span", { className: "track-position-play-pause__bar" }),
2195
- /* @__PURE__ */ (0, import_jsx_runtime38.jsx)("span", { className: "track-position-play-pause__bar" })
2238
+ /* @__PURE__ */ (0, import_jsx_runtime39.jsx)("span", { className: "track-position-play-pause__bar" }),
2239
+ /* @__PURE__ */ (0, import_jsx_runtime39.jsx)("span", { className: "track-position-play-pause__bar" }),
2240
+ /* @__PURE__ */ (0, import_jsx_runtime39.jsx)("span", { className: "track-position-play-pause__bar" })
2196
2241
  ]
2197
2242
  }
2198
2243
  ) : position }),
2199
- /* @__PURE__ */ (0, import_jsx_runtime38.jsx)(
2244
+ /* @__PURE__ */ (0, import_jsx_runtime39.jsx)(
2200
2245
  "div",
2201
2246
  {
2202
2247
  className: `hidden group-hover:flex items-center justify-center ${isCurrentTrackActive ? "" : "absolute inset-0"}`,
2203
2248
  "aria-hidden": "true",
2204
- children: isAwaitingPlayback ? /* @__PURE__ */ (0, import_jsx_runtime38.jsx)("span", { className: "track-position-play-pause__spinner" }) : isPlaying ? /* @__PURE__ */ (0, import_jsx_runtime38.jsx)(import_fa3.FaPause, {}) : /* @__PURE__ */ (0, import_jsx_runtime38.jsx)(import_fa3.FaPlay, {})
2249
+ children: isAwaitingPlayback ? /* @__PURE__ */ (0, import_jsx_runtime39.jsx)("span", { className: "track-position-play-pause__spinner" }) : isPlaying ? /* @__PURE__ */ (0, import_jsx_runtime39.jsx)(import_fa3.FaPause, {}) : /* @__PURE__ */ (0, import_jsx_runtime39.jsx)(import_fa3.FaPlay, {})
2205
2250
  }
2206
2251
  )
2207
2252
  ]
@@ -2212,7 +2257,7 @@ function TrackPositionPlayPause({
2212
2257
  // src/genre-tree/track-list-sidebar/TrackItem.tsx
2213
2258
  var import_genre_tree24 = require("@behindthemusictree/app-kit/genre-tree");
2214
2259
  var import_genre_tree25 = require("@behindthemusictree/app-kit/genre-tree");
2215
- var import_jsx_runtime39 = require("react/jsx-runtime");
2260
+ var import_jsx_runtime40 = require("react/jsx-runtime");
2216
2261
  function TrackItem({
2217
2262
  track,
2218
2263
  position,
@@ -2236,12 +2281,12 @@ function TrackItem({
2236
2281
  loadTrackForPlayer(track.uuid);
2237
2282
  }
2238
2283
  };
2239
- return /* @__PURE__ */ (0, import_jsx_runtime39.jsxs)(
2284
+ return /* @__PURE__ */ (0, import_jsx_runtime40.jsxs)(
2240
2285
  "div",
2241
2286
  {
2242
2287
  className: `track-item flex h-14 text-[var(--color-text)] ${isUnplayable ? "opacity-50" : "hover:bg-[var(--color-surface-muted)] group"}`,
2243
2288
  children: [
2244
- isUnplayable ? /* @__PURE__ */ (0, import_jsx_runtime39.jsx)(
2289
+ isUnplayable ? /* @__PURE__ */ (0, import_jsx_runtime40.jsx)(
2245
2290
  "div",
2246
2291
  {
2247
2292
  className: "flex items-center justify-center text-lg w-16",
@@ -2249,7 +2294,7 @@ function TrackItem({
2249
2294
  "aria-disabled": "true",
2250
2295
  children: position
2251
2296
  }
2252
- ) : /* @__PURE__ */ (0, import_jsx_runtime39.jsx)(
2297
+ ) : /* @__PURE__ */ (0, import_jsx_runtime40.jsx)(
2253
2298
  TrackPositionPlayPause,
2254
2299
  {
2255
2300
  position,
@@ -2258,12 +2303,12 @@ function TrackItem({
2258
2303
  isSelected: selectedTrack?.uuid === track.uuid
2259
2304
  }
2260
2305
  ),
2261
- /* @__PURE__ */ (0, import_jsx_runtime39.jsxs)("div", { className: "title-artist-container flex flex-col items-start justify-center min-w-0 flex-1", children: [
2262
- /* @__PURE__ */ (0, import_jsx_runtime39.jsx)("div", { className: "flex max-w-full items-center gap-2", children: /* @__PURE__ */ (0, import_jsx_runtime39.jsx)("div", { className: "title text-lg font-bold text-[var(--color-text)] max-w-full truncate", children: renderSongLink ? renderSongLink(track) : (0, import_genre_tree24.isSong)(track) ? /* @__PURE__ */ (0, import_jsx_runtime39.jsx)(MusicBrainzRecordingLink, { song: track, className: "hover:underline focus-visible:underline", children: track.title }) : track.title }) }),
2263
- track.artists && track.artists.length > 0 ? /* @__PURE__ */ (0, import_jsx_runtime39.jsx)("div", { className: "artist text-base max-w-full truncate text-[var(--color-text-muted)]", children: /* @__PURE__ */ (0, import_jsx_runtime39.jsx)(import_genre_tree25.ArtistNames, { artists: track.artists, renderArtistLink }) }) : ""
2306
+ /* @__PURE__ */ (0, import_jsx_runtime40.jsxs)("div", { className: "title-artist-container flex flex-col items-start justify-center min-w-0 flex-1", children: [
2307
+ /* @__PURE__ */ (0, import_jsx_runtime40.jsx)("div", { className: "flex max-w-full items-center gap-2", children: /* @__PURE__ */ (0, import_jsx_runtime40.jsx)("div", { className: "title text-lg font-bold text-[var(--color-text)] max-w-full truncate", children: renderSongLink ? renderSongLink(track) : (0, import_genre_tree24.isSong)(track) ? /* @__PURE__ */ (0, import_jsx_runtime40.jsx)(MusicBrainzRecordingLink, { song: track, className: "hover:underline focus-visible:underline", children: track.title }) : track.title }) }),
2308
+ track.artists && track.artists.length > 0 ? /* @__PURE__ */ (0, import_jsx_runtime40.jsx)("div", { className: "artist text-base max-w-full truncate text-[var(--color-text-muted)]", children: /* @__PURE__ */ (0, import_jsx_runtime40.jsx)(import_genre_tree25.ArtistNames, { artists: track.artists, renderArtistLink }) }) : ""
2264
2309
  ] }),
2265
- /* @__PURE__ */ (0, import_jsx_runtime39.jsx)("div", { className: "genre-name-container flex items-center justify-start w-[21.6%] min-w-0 shrink-0", children: track.genre ? /* @__PURE__ */ (0, import_jsx_runtime39.jsx)("div", { className: "genre-name max-w-full whitespace-normal break-words p-1 text-caption font-bold", children: track.genre.name }) : "" }),
2266
- renderDuration && /* @__PURE__ */ (0, import_jsx_runtime39.jsx)(
2310
+ /* @__PURE__ */ (0, import_jsx_runtime40.jsx)("div", { className: "genre-name-container flex items-center justify-start w-[21.6%] min-w-0 shrink-0", children: track.genre ? /* @__PURE__ */ (0, import_jsx_runtime40.jsx)("div", { className: "genre-name max-w-full whitespace-normal break-words p-1 text-caption font-bold", children: track.genre.name }) : "" }),
2311
+ renderDuration && /* @__PURE__ */ (0, import_jsx_runtime40.jsx)(
2267
2312
  "div",
2268
2313
  {
2269
2314
  className: "duration flex text-base w-16 items-center justify-center",
@@ -2271,14 +2316,14 @@ function TrackItem({
2271
2316
  children: renderDuration(track)
2272
2317
  }
2273
2318
  ),
2274
- renderActions && /* @__PURE__ */ (0, import_jsx_runtime39.jsx)("div", { className: "edit flex text-base w-6 items-center justify-center mr-2 cursor-pointer", children: renderActions(track) })
2319
+ renderActions && /* @__PURE__ */ (0, import_jsx_runtime40.jsx)("div", { className: "edit flex text-base w-6 items-center justify-center mr-2 cursor-pointer", children: renderActions(track) })
2275
2320
  ]
2276
2321
  }
2277
2322
  );
2278
2323
  }
2279
2324
 
2280
2325
  // src/genre-tree/artist/ArtistSongs.tsx
2281
- var import_jsx_runtime40 = require("react/jsx-runtime");
2326
+ var import_jsx_runtime41 = require("react/jsx-runtime");
2282
2327
  var SongPageSchema2 = (0, import_transport6.PaginatedResponseSchema)(import_genre_tree26.SongDetailedSchema);
2283
2328
  var SORTS = [
2284
2329
  { ordering: "title", label: "Title" },
@@ -2311,8 +2356,8 @@ function ArtistSongs({ artist, getBackendBaseUrl, renderArtistLink, renderSongLi
2311
2356
  event.stopPropagation();
2312
2357
  void play(song.uuid);
2313
2358
  };
2314
- return /* @__PURE__ */ (0, import_jsx_runtime40.jsx)(import_ui16.PanelSection, { title: "Songs", children: isPending ? /* @__PURE__ */ (0, import_jsx_runtime40.jsx)(import_ui16.Skeleton, { className: "h-14", "aria-busy": "true" }) : isError ? /* @__PURE__ */ (0, import_jsx_runtime40.jsx)("p", { className: "text-red-600", children: "Could not load songs." }) : data.results.length === 0 ? /* @__PURE__ */ (0, import_jsx_runtime40.jsx)("p", { className: "text-[var(--color-text-muted)]", children: "No songs yet." }) : /* @__PURE__ */ (0, import_jsx_runtime40.jsxs)(import_jsx_runtime40.Fragment, { children: [
2315
- /* @__PURE__ */ (0, import_jsx_runtime40.jsx)("div", { className: "flex flex-wrap items-center justify-end gap-2 mb-2", children: /* @__PURE__ */ (0, import_jsx_runtime40.jsx)("div", { role: "group", "aria-label": "Sort songs by", className: "flex gap-1 text-caption", children: SORTS.map((sort) => /* @__PURE__ */ (0, import_jsx_runtime40.jsx)(
2359
+ return /* @__PURE__ */ (0, import_jsx_runtime41.jsx)(import_ui16.PanelSection, { title: "Songs", children: isPending ? /* @__PURE__ */ (0, import_jsx_runtime41.jsx)(import_ui16.Skeleton, { className: "h-14", "aria-busy": "true" }) : isError ? /* @__PURE__ */ (0, import_jsx_runtime41.jsx)("p", { className: "text-red-600", children: "Could not load songs." }) : data.results.length === 0 ? /* @__PURE__ */ (0, import_jsx_runtime41.jsx)("p", { className: "text-[var(--color-text-muted)]", children: "No songs yet." }) : /* @__PURE__ */ (0, import_jsx_runtime41.jsxs)(import_jsx_runtime41.Fragment, { children: [
2360
+ /* @__PURE__ */ (0, import_jsx_runtime41.jsx)("div", { className: "flex flex-wrap items-center justify-end gap-2 mb-2", children: /* @__PURE__ */ (0, import_jsx_runtime41.jsx)("div", { role: "group", "aria-label": "Sort songs by", className: "flex gap-1 text-caption", children: SORTS.map((sort) => /* @__PURE__ */ (0, import_jsx_runtime41.jsx)(
2316
2361
  "button",
2317
2362
  {
2318
2363
  type: "button",
@@ -2326,7 +2371,7 @@ function ArtistSongs({ artist, getBackendBaseUrl, renderArtistLink, renderSongLi
2326
2371
  },
2327
2372
  sort.ordering
2328
2373
  )) }) }),
2329
- /* @__PURE__ */ (0, import_jsx_runtime40.jsx)("ul", { className: "overflow-hidden mb-2 bg-zinc-950 rounded-lg", children: data.results.map((song, index) => /* @__PURE__ */ (0, import_jsx_runtime40.jsx)("li", { onClickCapture: (event) => handleRowClickCapture(event, song), children: /* @__PURE__ */ (0, import_jsx_runtime40.jsx)(
2374
+ /* @__PURE__ */ (0, import_jsx_runtime41.jsx)("ul", { className: "overflow-hidden mb-2 bg-zinc-950 rounded-lg", children: data.results.map((song, index) => /* @__PURE__ */ (0, import_jsx_runtime41.jsx)("li", { onClickCapture: (event) => handleRowClickCapture(event, song), children: /* @__PURE__ */ (0, import_jsx_runtime41.jsx)(
2330
2375
  TrackItem,
2331
2376
  {
2332
2377
  track: song,
@@ -2335,12 +2380,12 @@ function ArtistSongs({ artist, getBackendBaseUrl, renderArtistLink, renderSongLi
2335
2380
  renderSongLink
2336
2381
  }
2337
2382
  ) }, song.uuid)) }),
2338
- /* @__PURE__ */ (0, import_jsx_runtime40.jsx)(import_ui16.Pagination, { currentPage: page, totalPages: data.totalPages, onPageChange: setPage })
2383
+ /* @__PURE__ */ (0, import_jsx_runtime41.jsx)(import_ui16.Pagination, { currentPage: page, totalPages: data.totalPages, onPageChange: setPage })
2339
2384
  ] }) });
2340
2385
  }
2341
2386
 
2342
2387
  // src/genre-tree/artist/useArtistPanel.tsx
2343
- var import_jsx_runtime41 = require("react/jsx-runtime");
2388
+ var import_jsx_runtime42 = require("react/jsx-runtime");
2344
2389
  function useArtistPanel({
2345
2390
  uuid,
2346
2391
  scope,
@@ -2348,6 +2393,7 @@ function useArtistPanel({
2348
2393
  getShareUrl,
2349
2394
  renderArtistLink,
2350
2395
  renderSongLink,
2396
+ renderPlaceLink,
2351
2397
  expanded,
2352
2398
  onExpandedChange,
2353
2399
  onClose
@@ -2361,26 +2407,26 @@ function useArtistPanel({
2361
2407
  return {
2362
2408
  ...panel,
2363
2409
  title: "Artist",
2364
- content: /* @__PURE__ */ (0, import_jsx_runtime41.jsxs)("div", { "aria-busy": "true", className: "flex flex-col gap-2.5", children: [
2365
- /* @__PURE__ */ (0, import_jsx_runtime41.jsx)(import_ui17.Skeleton, { className: "h-3 w-16" }),
2366
- /* @__PURE__ */ (0, import_jsx_runtime41.jsx)(import_ui17.Skeleton, { className: "h-4 w-32" }),
2367
- /* @__PURE__ */ (0, import_jsx_runtime41.jsx)(import_ui17.Skeleton, { className: "h-3 w-16" }),
2368
- /* @__PURE__ */ (0, import_jsx_runtime41.jsx)(import_ui17.Skeleton, { className: "h-4 w-24" }),
2369
- /* @__PURE__ */ (0, import_jsx_runtime41.jsx)(import_ui17.Skeleton, { className: "h-16 mt-2.5" }),
2370
- /* @__PURE__ */ (0, import_jsx_runtime41.jsx)(import_ui17.Skeleton, { className: "h-16" })
2410
+ content: /* @__PURE__ */ (0, import_jsx_runtime42.jsxs)("div", { "aria-busy": "true", className: "flex flex-col gap-2.5", children: [
2411
+ /* @__PURE__ */ (0, import_jsx_runtime42.jsx)(import_ui17.Skeleton, { className: "h-3 w-16" }),
2412
+ /* @__PURE__ */ (0, import_jsx_runtime42.jsx)(import_ui17.Skeleton, { className: "h-4 w-32" }),
2413
+ /* @__PURE__ */ (0, import_jsx_runtime42.jsx)(import_ui17.Skeleton, { className: "h-3 w-16" }),
2414
+ /* @__PURE__ */ (0, import_jsx_runtime42.jsx)(import_ui17.Skeleton, { className: "h-4 w-24" }),
2415
+ /* @__PURE__ */ (0, import_jsx_runtime42.jsx)(import_ui17.Skeleton, { className: "h-16 mt-2.5" }),
2416
+ /* @__PURE__ */ (0, import_jsx_runtime42.jsx)(import_ui17.Skeleton, { className: "h-16" })
2371
2417
  ] })
2372
2418
  };
2373
2419
  case "not-found":
2374
2420
  return {
2375
2421
  ...panel,
2376
2422
  title: "Artist not found",
2377
- content: /* @__PURE__ */ (0, import_jsx_runtime41.jsx)("p", { className: "text-[var(--color-text-muted)]", children: "This artist does not exist." })
2423
+ content: /* @__PURE__ */ (0, import_jsx_runtime42.jsx)("p", { className: "text-[var(--color-text-muted)]", children: "This artist does not exist." })
2378
2424
  };
2379
2425
  case "error":
2380
2426
  return {
2381
2427
  ...panel,
2382
2428
  title: "Artist",
2383
- content: /* @__PURE__ */ (0, import_jsx_runtime41.jsx)("p", { className: "text-red-600", children: "Could not load this artist." })
2429
+ content: /* @__PURE__ */ (0, import_jsx_runtime42.jsx)("p", { className: "text-red-600", children: "Could not load this artist." })
2384
2430
  };
2385
2431
  case "success": {
2386
2432
  const { artist } = result;
@@ -2389,19 +2435,19 @@ function useArtistPanel({
2389
2435
  return {
2390
2436
  ...panel,
2391
2437
  title: artist.name,
2392
- content: /* @__PURE__ */ (0, import_jsx_runtime41.jsxs)(import_jsx_runtime41.Fragment, { children: [
2393
- /* @__PURE__ */ (0, import_jsx_runtime41.jsx)(
2438
+ content: /* @__PURE__ */ (0, import_jsx_runtime42.jsxs)(import_jsx_runtime42.Fragment, { children: [
2439
+ /* @__PURE__ */ (0, import_jsx_runtime42.jsx)(
2394
2440
  ArtistActionBand,
2395
2441
  {
2396
2442
  artist,
2397
- share: shareTarget && shareUrl ? /* @__PURE__ */ (0, import_jsx_runtime41.jsx)(ShareActionButton, { target: shareTarget, url: shareUrl }) : void 0
2443
+ share: shareTarget && shareUrl ? /* @__PURE__ */ (0, import_jsx_runtime42.jsx)(ShareActionButton, { target: shareTarget, url: shareUrl }) : void 0
2398
2444
  }
2399
2445
  ),
2400
- artist.image && /* @__PURE__ */ (0, import_jsx_runtime41.jsx)(ArtistPhoto, { image: artist.image, name: artist.name }),
2401
- /* @__PURE__ */ (0, import_jsx_runtime41.jsx)(ArtistHeader, { artist }),
2402
- /* @__PURE__ */ (0, import_jsx_runtime41.jsx)(ArtistMembers, { title: "Members", memberships: artist.members, renderArtistLink }),
2403
- /* @__PURE__ */ (0, import_jsx_runtime41.jsx)(ArtistMembers, { title: "Member of", memberships: artist.memberOf, renderArtistLink }),
2404
- artist.tracksCount > 0 && /* @__PURE__ */ (0, import_jsx_runtime41.jsx)(
2446
+ artist.image && /* @__PURE__ */ (0, import_jsx_runtime42.jsx)(ArtistPhoto, { image: artist.image, name: artist.name }),
2447
+ /* @__PURE__ */ (0, import_jsx_runtime42.jsx)(ArtistHeader, { artist, renderPlaceLink }),
2448
+ /* @__PURE__ */ (0, import_jsx_runtime42.jsx)(ArtistMembers, { title: "Members", memberships: artist.members, renderArtistLink }),
2449
+ /* @__PURE__ */ (0, import_jsx_runtime42.jsx)(ArtistMembers, { title: "Member of", memberships: artist.memberOf, renderArtistLink }),
2450
+ artist.tracksCount > 0 && /* @__PURE__ */ (0, import_jsx_runtime42.jsx)(
2405
2451
  ArtistSongs,
2406
2452
  {
2407
2453
  artist: { uuid: artist.uuid, name: artist.name },
@@ -2411,7 +2457,7 @@ function useArtistPanel({
2411
2457
  }
2412
2458
  )
2413
2459
  ] }),
2414
- extraContent: artist.albums.length > 0 ? /* @__PURE__ */ (0, import_jsx_runtime41.jsx)(ArtistDiscography, { albums: artist.albums }) : void 0
2460
+ extraContent: artist.albums.length > 0 ? /* @__PURE__ */ (0, import_jsx_runtime42.jsx)(ArtistDiscography, { albums: artist.albums }) : void 0
2415
2461
  };
2416
2462
  }
2417
2463
  }
@@ -2427,7 +2473,7 @@ var import_lucide_react12 = require("lucide-react");
2427
2473
  var import_ui18 = require("@behindthemusictree/ui");
2428
2474
  var import_player12 = require("@behindthemusictree/app-kit/player");
2429
2475
  var import_genre_tree28 = require("@behindthemusictree/app-kit/genre-tree");
2430
- var import_jsx_runtime42 = require("react/jsx-runtime");
2476
+ var import_jsx_runtime43 = require("react/jsx-runtime");
2431
2477
  var numberFormat = new Intl.NumberFormat("en");
2432
2478
  var dateFormat = new Intl.DateTimeFormat("en", { dateStyle: "medium" });
2433
2479
  var externalLink = "text-[var(--color-primary)] hover:underline focus-visible:underline";
@@ -2435,27 +2481,27 @@ function SongHistory({ uuid, getBackendBaseUrl }) {
2435
2481
  const [open, setOpen] = (0, import_react18.useState)(false);
2436
2482
  const history = (0, import_genre_tree28.useSongHistory)(uuid, getBackendBaseUrl, open);
2437
2483
  let body = null;
2438
- if (history.status === "loading") body = /* @__PURE__ */ (0, import_jsx_runtime42.jsx)(import_ui18.Skeleton, { className: "h-10", "aria-busy": "true" });
2439
- else if (history.status === "error") body = /* @__PURE__ */ (0, import_jsx_runtime42.jsx)("p", { className: "text-red-600", children: "Could not load the history." });
2484
+ if (history.status === "loading") body = /* @__PURE__ */ (0, import_jsx_runtime43.jsx)(import_ui18.Skeleton, { className: "h-10", "aria-busy": "true" });
2485
+ else if (history.status === "error") body = /* @__PURE__ */ (0, import_jsx_runtime43.jsx)("p", { className: "text-red-600", children: "Could not load the history." });
2440
2486
  else if (history.status === "success")
2441
- body = history.entries.length === 0 ? /* @__PURE__ */ (0, import_jsx_runtime42.jsx)("p", { className: "text-[var(--color-text-muted)]", children: "No changes yet." }) : /* @__PURE__ */ (0, import_jsx_runtime42.jsx)("ul", { className: "divide-y divide-[var(--color-border)]", children: history.entries.map((entry) => /* @__PURE__ */ (0, import_jsx_runtime42.jsxs)("li", { className: "py-1.5 text-body", children: [
2442
- /* @__PURE__ */ (0, import_jsx_runtime42.jsx)("span", { className: "font-medium", children: entry.action }),
2443
- entry.oldValue !== null || entry.newValue !== null ? /* @__PURE__ */ (0, import_jsx_runtime42.jsxs)("span", { className: "text-[var(--color-text-muted)]", children: [
2487
+ body = history.entries.length === 0 ? /* @__PURE__ */ (0, import_jsx_runtime43.jsx)("p", { className: "text-[var(--color-text-muted)]", children: "No changes yet." }) : /* @__PURE__ */ (0, import_jsx_runtime43.jsx)("ul", { className: "divide-y divide-[var(--color-border)]", children: history.entries.map((entry) => /* @__PURE__ */ (0, import_jsx_runtime43.jsxs)("li", { className: "py-1.5 text-body", children: [
2488
+ /* @__PURE__ */ (0, import_jsx_runtime43.jsx)("span", { className: "font-medium", children: entry.action }),
2489
+ entry.oldValue !== null || entry.newValue !== null ? /* @__PURE__ */ (0, import_jsx_runtime43.jsxs)("span", { className: "text-[var(--color-text-muted)]", children: [
2444
2490
  " ",
2445
2491
  "\u2014 ",
2446
2492
  entry.oldValue ?? "none",
2447
2493
  " \u2192 ",
2448
2494
  entry.newValue ?? "none"
2449
2495
  ] }) : null,
2450
- /* @__PURE__ */ (0, import_jsx_runtime42.jsxs)("span", { className: "block text-caption text-[var(--color-text-muted)]", children: [
2496
+ /* @__PURE__ */ (0, import_jsx_runtime43.jsxs)("span", { className: "block text-caption text-[var(--color-text-muted)]", children: [
2451
2497
  entry.actorPseudo ?? "Anonymous",
2452
2498
  " \xB7 ",
2453
2499
  dateFormat.format(new Date(entry.createdOn))
2454
2500
  ] })
2455
2501
  ] }, entry.uuid)) });
2456
- return /* @__PURE__ */ (0, import_jsx_runtime42.jsx)(import_ui18.PanelBleed, { padded: true, className: "border-t border-[var(--color-border)]", children: /* @__PURE__ */ (0, import_jsx_runtime42.jsxs)("details", { onToggle: (event) => setOpen(event.currentTarget.open), className: "pb-2.5", children: [
2457
- /* @__PURE__ */ (0, import_jsx_runtime42.jsx)("summary", { className: `mt-2.5 cursor-pointer ${import_ui18.PANEL_LABEL_CLASS}`, children: "History" }),
2458
- /* @__PURE__ */ (0, import_jsx_runtime42.jsx)("div", { className: "mt-1.5", children: body })
2502
+ return /* @__PURE__ */ (0, import_jsx_runtime43.jsx)(import_ui18.PanelBleed, { padded: true, className: "border-t border-[var(--color-border)]", children: /* @__PURE__ */ (0, import_jsx_runtime43.jsxs)("details", { onToggle: (event) => setOpen(event.currentTarget.open), className: "pb-2.5", children: [
2503
+ /* @__PURE__ */ (0, import_jsx_runtime43.jsx)("summary", { className: `mt-2.5 cursor-pointer ${import_ui18.PANEL_LABEL_CLASS}`, children: "History" }),
2504
+ /* @__PURE__ */ (0, import_jsx_runtime43.jsx)("div", { className: "mt-1.5", children: body })
2459
2505
  ] }) });
2460
2506
  }
2461
2507
  function SongPanelContent(props) {
@@ -2474,13 +2520,13 @@ function SongPanelContent(props) {
2474
2520
  const genre = song.genre;
2475
2521
  fields.push({
2476
2522
  label: "Genre",
2477
- value: onSelectGenre ? /* @__PURE__ */ (0, import_jsx_runtime42.jsx)("button", { type: "button", onClick: () => onSelectGenre(genre), className: "hover:underline", children: genre.name }) : path ? /* @__PURE__ */ (0, import_jsx_runtime42.jsx)("span", { className: "flex flex-wrap items-center gap-x-1", children: path.map((genrePlaylist, index) => /* @__PURE__ */ (0, import_jsx_runtime42.jsxs)("span", { children: [
2478
- index > 0 && /* @__PURE__ */ (0, import_jsx_runtime42.jsx)("span", { "aria-hidden": "true", children: "\u203A " }),
2479
- /* @__PURE__ */ (0, import_jsx_runtime42.jsx)("button", { type: "button", onClick: () => props.onSelectGenrePlaylist?.(genrePlaylist), className: "hover:underline", children: genrePlaylist.name })
2523
+ value: onSelectGenre ? /* @__PURE__ */ (0, import_jsx_runtime43.jsx)("button", { type: "button", onClick: () => onSelectGenre(genre), className: "hover:underline", children: genre.name }) : path ? /* @__PURE__ */ (0, import_jsx_runtime43.jsx)("span", { className: "flex flex-wrap items-center gap-x-1", children: path.map((genrePlaylist, index) => /* @__PURE__ */ (0, import_jsx_runtime43.jsxs)("span", { children: [
2524
+ index > 0 && /* @__PURE__ */ (0, import_jsx_runtime43.jsx)("span", { "aria-hidden": "true", children: "\u203A " }),
2525
+ /* @__PURE__ */ (0, import_jsx_runtime43.jsx)("button", { type: "button", onClick: () => props.onSelectGenrePlaylist?.(genrePlaylist), className: "hover:underline", children: genrePlaylist.name })
2480
2526
  ] }, genrePlaylist.uuid)) }) : song.genre.name
2481
2527
  });
2482
2528
  } else {
2483
- fields.push({ label: "Genre", value: /* @__PURE__ */ (0, import_jsx_runtime42.jsx)("span", { className: "text-[var(--color-text-muted)]", children: "No genre" }) });
2529
+ fields.push({ label: "Genre", value: /* @__PURE__ */ (0, import_jsx_runtime43.jsx)("span", { className: "text-[var(--color-text-muted)]", children: "No genre" }) });
2484
2530
  }
2485
2531
  if (song.firstReleaseYear) fields.push({ label: "First released", value: song.firstReleaseYear });
2486
2532
  const viewCount = song.youtubeVideo?.viewCount;
@@ -2490,11 +2536,11 @@ function SongPanelContent(props) {
2490
2536
  const links = [];
2491
2537
  if (musicBrainzUrl)
2492
2538
  links.push(
2493
- /* @__PURE__ */ (0, import_jsx_runtime42.jsx)("a", { href: musicBrainzUrl, target: "_blank", rel: "noopener noreferrer", className: externalLink, children: "MusicBrainz" }, "mb")
2539
+ /* @__PURE__ */ (0, import_jsx_runtime43.jsx)("a", { href: musicBrainzUrl, target: "_blank", rel: "noopener noreferrer", className: externalLink, children: "MusicBrainz" }, "mb")
2494
2540
  );
2495
2541
  if (song.youtubeVideo)
2496
2542
  links.push(
2497
- /* @__PURE__ */ (0, import_jsx_runtime42.jsx)(
2543
+ /* @__PURE__ */ (0, import_jsx_runtime43.jsx)(
2498
2544
  "a",
2499
2545
  {
2500
2546
  href: `https://www.youtube.com/watch?v=${encodeURIComponent(song.youtubeVideo.id)}`,
@@ -2509,31 +2555,31 @@ function SongPanelContent(props) {
2509
2555
  if (links.length > 0)
2510
2556
  fields.push({
2511
2557
  label: "Links",
2512
- value: /* @__PURE__ */ (0, import_jsx_runtime42.jsx)("span", { className: "flex gap-3", children: links })
2558
+ value: /* @__PURE__ */ (0, import_jsx_runtime43.jsx)("span", { className: "flex gap-3", children: links })
2513
2559
  });
2514
- return /* @__PURE__ */ (0, import_jsx_runtime42.jsxs)(import_jsx_runtime42.Fragment, { children: [
2515
- /* @__PURE__ */ (0, import_jsx_runtime42.jsx)(import_ui18.PanelBleed, { children: /* @__PURE__ */ (0, import_jsx_runtime42.jsxs)(import_ui18.ActionBand, { children: [
2516
- /* @__PURE__ */ (0, import_jsx_runtime42.jsx)(
2560
+ return /* @__PURE__ */ (0, import_jsx_runtime43.jsxs)(import_jsx_runtime43.Fragment, { children: [
2561
+ /* @__PURE__ */ (0, import_jsx_runtime43.jsx)(import_ui18.PanelBleed, { children: /* @__PURE__ */ (0, import_jsx_runtime43.jsxs)(import_ui18.ActionBand, { children: [
2562
+ /* @__PURE__ */ (0, import_jsx_runtime43.jsx)(
2517
2563
  import_ui18.ActionButton,
2518
2564
  {
2519
- icon: isPlaying ? /* @__PURE__ */ (0, import_jsx_runtime42.jsx)(import_lucide_react12.Pause, { "aria-hidden": "true", className: "w-5 h-5" }) : /* @__PURE__ */ (0, import_jsx_runtime42.jsx)(import_lucide_react12.Play, { "aria-hidden": "true", className: "w-5 h-5" }),
2565
+ icon: isPlaying ? /* @__PURE__ */ (0, import_jsx_runtime43.jsx)(import_lucide_react12.Pause, { "aria-hidden": "true", className: "w-5 h-5" }) : /* @__PURE__ */ (0, import_jsx_runtime43.jsx)(import_lucide_react12.Play, { "aria-hidden": "true", className: "w-5 h-5" }),
2520
2566
  label: isPlaying ? "Pause" : "Play",
2521
2567
  disabled: !!unplayableReason,
2522
2568
  title: unplayableReason ?? void 0,
2523
2569
  onClick: () => isCurrent ? handlePlayPauseAction() : playNewTrackListFromTrackUuid(song, scope)
2524
2570
  }
2525
2571
  ),
2526
- shareUrl && /* @__PURE__ */ (0, import_jsx_runtime42.jsx)(ShareActionButton, { target: shareTarget, url: shareUrl })
2572
+ shareUrl && /* @__PURE__ */ (0, import_jsx_runtime43.jsx)(ShareActionButton, { target: shareTarget, url: shareUrl })
2527
2573
  ] }) }),
2528
- song.artists && song.artists.length > 0 && /* @__PURE__ */ (0, import_jsx_runtime42.jsx)("p", { className: "text-[var(--color-text-muted)]", children: /* @__PURE__ */ (0, import_jsx_runtime42.jsx)(import_genre_tree28.ArtistNames, { artists: song.artists, renderArtistLink }) }),
2529
- /* @__PURE__ */ (0, import_jsx_runtime42.jsx)(import_ui18.FieldList, { items: fields }),
2530
- song.essentialGenres && song.essentialGenres.length > 0 && /* @__PURE__ */ (0, import_jsx_runtime42.jsx)(import_ui18.PanelSection, { title: "Essential in", children: /* @__PURE__ */ (0, import_jsx_runtime42.jsx)("ul", { children: song.essentialGenres.map((genre) => /* @__PURE__ */ (0, import_jsx_runtime42.jsx)("li", { className: "py-0.5", children: genre.name }, genre.uuid)) }) }),
2531
- /* @__PURE__ */ (0, import_jsx_runtime42.jsx)(SongHistory, { uuid: song.uuid, getBackendBaseUrl })
2574
+ song.artists && song.artists.length > 0 && /* @__PURE__ */ (0, import_jsx_runtime43.jsx)("p", { className: "text-[var(--color-text-muted)]", children: /* @__PURE__ */ (0, import_jsx_runtime43.jsx)(import_genre_tree28.ArtistNames, { artists: song.artists, renderArtistLink }) }),
2575
+ /* @__PURE__ */ (0, import_jsx_runtime43.jsx)(import_ui18.FieldList, { items: fields }),
2576
+ song.essentialGenres && song.essentialGenres.length > 0 && /* @__PURE__ */ (0, import_jsx_runtime43.jsx)(import_ui18.PanelSection, { title: "Essential in", children: /* @__PURE__ */ (0, import_jsx_runtime43.jsx)("ul", { children: song.essentialGenres.map((genre) => /* @__PURE__ */ (0, import_jsx_runtime43.jsx)("li", { className: "py-0.5", children: genre.name }, genre.uuid)) }) }),
2577
+ /* @__PURE__ */ (0, import_jsx_runtime43.jsx)(SongHistory, { uuid: song.uuid, getBackendBaseUrl })
2532
2578
  ] });
2533
2579
  }
2534
2580
 
2535
2581
  // src/genre-tree/song/useSongPanel.tsx
2536
- var import_jsx_runtime43 = require("react/jsx-runtime");
2582
+ var import_jsx_runtime44 = require("react/jsx-runtime");
2537
2583
  function useSongPanel({
2538
2584
  uuid,
2539
2585
  scope,
@@ -2554,24 +2600,24 @@ function useSongPanel({
2554
2600
  return {
2555
2601
  ...panel,
2556
2602
  title: "Song",
2557
- content: /* @__PURE__ */ (0, import_jsx_runtime43.jsxs)("div", { "aria-busy": "true", className: "flex flex-col gap-2.5", children: [
2558
- /* @__PURE__ */ (0, import_jsx_runtime43.jsx)(import_ui19.Skeleton, { className: "h-3 w-16" }),
2559
- /* @__PURE__ */ (0, import_jsx_runtime43.jsx)(import_ui19.Skeleton, { className: "h-4 w-32" }),
2560
- /* @__PURE__ */ (0, import_jsx_runtime43.jsx)(import_ui19.Skeleton, { className: "h-3 w-16" }),
2561
- /* @__PURE__ */ (0, import_jsx_runtime43.jsx)(import_ui19.Skeleton, { className: "h-4 w-24" })
2603
+ content: /* @__PURE__ */ (0, import_jsx_runtime44.jsxs)("div", { "aria-busy": "true", className: "flex flex-col gap-2.5", children: [
2604
+ /* @__PURE__ */ (0, import_jsx_runtime44.jsx)(import_ui19.Skeleton, { className: "h-3 w-16" }),
2605
+ /* @__PURE__ */ (0, import_jsx_runtime44.jsx)(import_ui19.Skeleton, { className: "h-4 w-32" }),
2606
+ /* @__PURE__ */ (0, import_jsx_runtime44.jsx)(import_ui19.Skeleton, { className: "h-3 w-16" }),
2607
+ /* @__PURE__ */ (0, import_jsx_runtime44.jsx)(import_ui19.Skeleton, { className: "h-4 w-24" })
2562
2608
  ] })
2563
2609
  };
2564
2610
  case "not-found":
2565
2611
  return {
2566
2612
  ...panel,
2567
2613
  title: "Song not found",
2568
- content: /* @__PURE__ */ (0, import_jsx_runtime43.jsx)("p", { className: "text-[var(--color-text-muted)]", children: "This song does not exist." })
2614
+ content: /* @__PURE__ */ (0, import_jsx_runtime44.jsx)("p", { className: "text-[var(--color-text-muted)]", children: "This song does not exist." })
2569
2615
  };
2570
2616
  case "error":
2571
2617
  return {
2572
2618
  ...panel,
2573
2619
  title: "Song",
2574
- content: /* @__PURE__ */ (0, import_jsx_runtime43.jsx)("p", { className: "text-red-600", children: "Could not load this song." })
2620
+ content: /* @__PURE__ */ (0, import_jsx_runtime44.jsx)("p", { className: "text-red-600", children: "Could not load this song." })
2575
2621
  };
2576
2622
  case "success": {
2577
2623
  const { song } = result;
@@ -2579,7 +2625,7 @@ function useSongPanel({
2579
2625
  return {
2580
2626
  ...panel,
2581
2627
  title: song.title,
2582
- content: /* @__PURE__ */ (0, import_jsx_runtime43.jsx)(
2628
+ content: /* @__PURE__ */ (0, import_jsx_runtime44.jsx)(
2583
2629
  SongPanelContent,
2584
2630
  {
2585
2631
  song,
@@ -2606,6 +2652,7 @@ function useEntityPanel({
2606
2652
  getShareUrl,
2607
2653
  renderArtistLink,
2608
2654
  renderSongLink,
2655
+ renderPlaceLink,
2609
2656
  song,
2610
2657
  artist,
2611
2658
  ...navigation
@@ -2624,7 +2671,8 @@ function useEntityPanel({
2624
2671
  getBackendBaseUrl: getArtistBackendBaseUrl ?? getBackendBaseUrl,
2625
2672
  getShareUrl,
2626
2673
  renderArtistLink,
2627
- renderSongLink
2674
+ renderSongLink,
2675
+ renderPlaceLink
2628
2676
  });
2629
2677
  return songPanel ?? artistPanel;
2630
2678
  }
@@ -2632,12 +2680,12 @@ function useEntityPanel({
2632
2680
  // src/genre-tree/SizeMetricToggle.tsx
2633
2681
  var import_md = require("react-icons/md");
2634
2682
  var import_ui20 = require("@behindthemusictree/ui");
2635
- var import_jsx_runtime44 = require("react/jsx-runtime");
2683
+ var import_jsx_runtime45 = require("react/jsx-runtime");
2636
2684
  function SizeMetricToggle({
2637
2685
  value,
2638
2686
  onChange
2639
2687
  }) {
2640
- return /* @__PURE__ */ (0, import_jsx_runtime44.jsx)(
2688
+ return /* @__PURE__ */ (0, import_jsx_runtime45.jsx)(
2641
2689
  import_ui20.SegmentedControl,
2642
2690
  {
2643
2691
  variant: "floating",
@@ -2645,15 +2693,15 @@ function SizeMetricToggle({
2645
2693
  value,
2646
2694
  onChange,
2647
2695
  options: [
2648
- { value: "songs", label: /* @__PURE__ */ (0, import_jsx_runtime44.jsx)(import_md.MdMusicNote, { size: 18, "aria-hidden": true }), "aria-label": "Size by songs", title: "Node size by song count" },
2649
- { value: "views", label: /* @__PURE__ */ (0, import_jsx_runtime44.jsx)(import_md.MdVisibility, { size: 18, "aria-hidden": true }), "aria-label": "Size by views", title: "Node size by song views" }
2696
+ { value: "songs", label: /* @__PURE__ */ (0, import_jsx_runtime45.jsx)(import_md.MdMusicNote, { size: 18, "aria-hidden": true }), "aria-label": "Size by songs", title: "Node size by song count" },
2697
+ { value: "views", label: /* @__PURE__ */ (0, import_jsx_runtime45.jsx)(import_md.MdVisibility, { size: 18, "aria-hidden": true }), "aria-label": "Size by views", title: "Node size by song views" }
2650
2698
  ]
2651
2699
  }
2652
2700
  );
2653
2701
  }
2654
2702
 
2655
2703
  // src/genre-tree/GenreTreeView.tsx
2656
- var import_jsx_runtime45 = require("react/jsx-runtime");
2704
+ var import_jsx_runtime46 = require("react/jsx-runtime");
2657
2705
  var NO_POP_ROOT_TITLE = "This genre tree has no 'Mainstream Pop' root yet";
2658
2706
  var REGIONS_CLASS = "[--gtv-center-left:var(--layout-center-left)] [--gtv-center-right:var(--layout-center-right)] [--gtv-center-top:var(--layout-band-top)]";
2659
2707
  var noop = () => {
@@ -2701,6 +2749,7 @@ function GenreTreeView({
2701
2749
  songSearch = true,
2702
2750
  onSongSelect,
2703
2751
  renderSongLink,
2752
+ renderPlaceLink,
2704
2753
  renderSummary,
2705
2754
  renderEssentialTracks,
2706
2755
  renderSongLeading,
@@ -2761,7 +2810,7 @@ function GenreTreeView({
2761
2810
  allowWheelRotation: internalAllowWheelRotation,
2762
2811
  toggleWheelRotation,
2763
2812
  onNodeHover: handleNodeHover
2764
- } = (0, import_genre_tree30.useGenreTreeViewState)({
2813
+ } = (0, import_genre_tree31.useGenreTreeViewState)({
2765
2814
  scope,
2766
2815
  getBackendBaseUrl,
2767
2816
  viewMode: controlledViewMode,
@@ -2780,12 +2829,12 @@ function GenreTreeView({
2780
2829
  restoreRootUuid = selectedGenrePlaylist.root.uuid;
2781
2830
  setStackedRestoreOwner({ revision: restoreRevision, rootUuid: restoreRootUuid });
2782
2831
  }
2783
- const isGenreUpdating = (0, import_react_query3.useIsMutating)({ mutationKey: import_genre_tree30.genreUpdateMutationKey }) > 0;
2784
- const { mutate: updateGenre } = (0, import_genre_tree30.useUpdateGenre)(scope, getBackendBaseUrl);
2832
+ const isGenreUpdating = (0, import_react_query3.useIsMutating)({ mutationKey: import_genre_tree31.genreUpdateMutationKey }) > 0;
2833
+ const { mutate: updateGenre } = (0, import_genre_tree31.useUpdateGenre)(scope, getBackendBaseUrl);
2785
2834
  const busy = deferredSizeMetric !== sizeMetric || isTreeFetching || isGenreUpdating;
2786
2835
  const isArtistFiltered = artistUuid !== null;
2787
2836
  const canSizeByViews = !isArtistFiltered && hasViewsCount(visibleGenrePlaylists);
2788
- const artistResult = (0, import_genre_tree30.useArtist)(artistUuid, artistSearchBaseUrl ?? getBackendBaseUrl);
2837
+ const artistResult = (0, import_genre_tree31.useArtist)(artistUuid, artistSearchBaseUrl ?? getBackendBaseUrl);
2789
2838
  const filterArtistName = artistResult.status === "success" ? artistResult.artist.name : null;
2790
2839
  const filterArtist = (0, import_react19.useMemo)(
2791
2840
  () => artistUuid !== null && filterArtistName !== null ? { uuid: artistUuid, name: filterArtistName } : null,
@@ -2805,14 +2854,14 @@ function GenreTreeView({
2805
2854
  const openSongRef = (0, import_react19.useRef)(openSong);
2806
2855
  openSongRef.current = openSong;
2807
2856
  const resolvedRenderSongLink = (0, import_react19.useCallback)(
2808
- (song) => renderSongLink ? renderSongLink(song) : /* @__PURE__ */ (0, import_jsx_runtime45.jsx)("button", { type: "button", onClick: () => openSongRef.current(song.uuid), className: "hover:underline", children: song.title }),
2857
+ (song) => renderSongLink ? renderSongLink(song) : /* @__PURE__ */ (0, import_jsx_runtime46.jsx)("button", { type: "button", onClick: () => openSongRef.current(song.uuid), className: "hover:underline", children: song.title }),
2809
2858
  [renderSongLink]
2810
2859
  );
2811
2860
  const [expandedSongUuid, setExpandedSongUuid] = (0, import_react19.useState)(null);
2812
2861
  const [expandedArtistUuid, setExpandedArtistUuid] = (0, import_react19.useState)(null);
2813
2862
  const artistBaseUrl = artistSearchBaseUrl ?? getBackendBaseUrl;
2814
2863
  const songBaseUrl = getBackendBaseUrl;
2815
- const resolvedRenderArtistLink = renderArtistLink ?? ((artist) => artist.isSpecialPurpose ? artist.name : /* @__PURE__ */ (0, import_jsx_runtime45.jsx)("button", { type: "button", onClick: () => openArtist(artist.uuid), className: "hover:underline", children: artist.name }));
2864
+ const resolvedRenderArtistLink = renderArtistLink ?? ((artist) => artist.isSpecialPurpose ? artist.name : /* @__PURE__ */ (0, import_jsx_runtime46.jsx)("button", { type: "button", onClick: () => openArtist(artist.uuid), className: "hover:underline", children: artist.name }));
2816
2865
  const getGenrePath = (genre) => {
2817
2866
  const path = [];
2818
2867
  let current = visibleGenrePlaylists.find((playlist) => playlist.criteria?.uuid === genre.uuid);
@@ -2830,6 +2879,7 @@ function GenreTreeView({
2830
2879
  getShareUrl: getShareUrlRef.current,
2831
2880
  renderArtistLink: resolvedRenderArtistLink,
2832
2881
  renderSongLink: resolvedRenderSongLink,
2882
+ renderPlaceLink,
2833
2883
  song: {
2834
2884
  uuid: songUuid,
2835
2885
  expanded: controlledSongExpanded ?? (songUuid !== null && expandedSongUuid === songUuid),
@@ -2889,9 +2939,10 @@ function GenreTreeView({
2889
2939
  resolveStyle
2890
2940
  );
2891
2941
  const treeStyles = (0, import_react19.useMemo)(() => toGenreNodeStyles(relations, visibleGenrePlaylists), [relations, visibleGenrePlaylists]);
2892
- const [selectionHistory, setSelectionHistory] = (0, import_react19.useState)({ current: selectedGenreUuid, previous: null });
2893
- if (selectionHistory.current !== selectedGenreUuid) {
2894
- setSelectionHistory({ current: selectedGenreUuid, previous: selectionHistory.current ?? selectionHistory.previous });
2942
+ const validSelectedGenreUuid = (0, import_genre_tree30.toGenreUuid)(selectedGenreUuid);
2943
+ const [selectionHistory, setSelectionHistory] = (0, import_react19.useState)({ current: validSelectedGenreUuid, previous: null });
2944
+ if (selectionHistory.current !== validSelectedGenreUuid) {
2945
+ setSelectionHistory({ current: validSelectedGenreUuid, previous: selectionHistory.current ?? selectionHistory.previous });
2895
2946
  }
2896
2947
  const handlePrevious = selectionHistory.previous ? () => selectGenre(selectionHistory.previous) : void 0;
2897
2948
  const { showPopup, hidePopup } = (0, import_popup4.usePopup)();
@@ -2923,7 +2974,7 @@ function GenreTreeView({
2923
2974
  const canToggleSide = !readOnly && criteria !== void 0 && criteria !== null && parentPlaylist !== void 0 && parentPlaylist.parent === null;
2924
2975
  const side = criteria?.side ?? "core";
2925
2976
  const popSibling = canToggleSide && side === "core" ? visibleGenrePlaylists.find((sibling) => sibling.parent?.uuid === parentPlaylist.uuid && sibling.uuid !== genrePlaylist?.uuid && sibling.criteria?.side === "pop") : void 0;
2926
- const shareTarget = selectedGenreUuid ? { kind: "genre", genreUuid: selectedGenreUuid, name: criteria?.name ?? "Genre", scope, artistUuid } : void 0;
2977
+ const shareTarget = validSelectedGenreUuid ? { kind: "genre", genreUuid: validSelectedGenreUuid, name: criteria?.name ?? "Genre", scope, artistUuid } : void 0;
2927
2978
  const shareUrl = shareTarget ? getShareUrlRef.current?.(shareTarget) ?? void 0 : void 0;
2928
2979
  const genreContent = useGenreDetailContent({
2929
2980
  genreUuid: selectedGenreUuid,
@@ -2945,7 +2996,7 @@ function GenreTreeView({
2945
2996
  onAddChild: readOnly || !criteria ? void 0 : () => handleGenreCreationAction(criteria),
2946
2997
  onRename: readOnly || !criteria ? void 0 : () => handleGenreRenameAction(criteria),
2947
2998
  onChangeParent: readOnly || !genrePlaylist || !criteria ? void 0 : () => showPopup(
2948
- /* @__PURE__ */ (0, import_jsx_runtime45.jsx)(
2999
+ /* @__PURE__ */ (0, import_jsx_runtime46.jsx)(
2949
3000
  ChangeParentPopup,
2950
3001
  {
2951
3002
  genrePlaylist: { ...genrePlaylist, criteria },
@@ -2989,7 +3040,7 @@ function GenreTreeView({
2989
3040
  onClose: otherPanel.onClose,
2990
3041
  searchName: otherPanel.title
2991
3042
  };
2992
- } else if (selectedGenreUuid) {
3043
+ } else if (selectedGenreUuid !== null) {
2993
3044
  activePanel = {
2994
3045
  title: genreContent.title,
2995
3046
  content: genreContent.content,
@@ -3006,19 +3057,19 @@ function GenreTreeView({
3006
3057
  edge: activePanel.expanded ? { label: "Shrink panel", onClick: () => activePanel.onExpandedChange?.(false) } : isCollapsed ? null : { label: "Hide panel", onClick: () => changeCollapsed(true) },
3007
3058
  center: activePanel.expanded ? null : isCollapsed ? { label: "Show panel", onClick: () => changeCollapsed(false) } : activePanel.onExpandedChange ? { label: "Extend panel", onClick: () => activePanel.onExpandedChange?.(true) } : null
3008
3059
  } : void 0;
3009
- const expandButton = placement === "default" && activePanel?.onExpandedChange ? /* @__PURE__ */ (0, import_jsx_runtime45.jsx)(
3060
+ const expandButton = placement === "default" && activePanel?.onExpandedChange ? /* @__PURE__ */ (0, import_jsx_runtime46.jsx)(
3010
3061
  import_ui21.IconButton,
3011
3062
  {
3012
3063
  "aria-label": activePanel.expanded ? "Shrink panel" : "Extend panel",
3013
3064
  style: activePanel.headerAccent && { color: "inherit" },
3014
3065
  onClick: () => activePanel.onExpandedChange?.(!activePanel.expanded),
3015
- children: activePanel.expanded ? /* @__PURE__ */ (0, import_jsx_runtime45.jsx)(import_lucide_react14.Minimize2, { "aria-hidden": "true", className: "h-4 w-4" }) : /* @__PURE__ */ (0, import_jsx_runtime45.jsx)(import_lucide_react14.Maximize2, { "aria-hidden": "true", className: "h-4 w-4" })
3066
+ children: activePanel.expanded ? /* @__PURE__ */ (0, import_jsx_runtime46.jsx)(import_lucide_react14.Minimize2, { "aria-hidden": "true", className: "h-4 w-4" }) : /* @__PURE__ */ (0, import_jsx_runtime46.jsx)(import_lucide_react14.Maximize2, { "aria-hidden": "true", className: "h-4 w-4" })
3016
3067
  }
3017
3068
  ) : null;
3018
3069
  const renderArtistFilter = () => {
3019
3070
  if (isLoading || !filterArtist) return null;
3020
- return /* @__PURE__ */ (0, import_jsx_runtime45.jsxs)("span", { className: "inline-flex items-center overflow-hidden rounded-full bg-[var(--color-neutral-100)] text-body", children: [
3021
- /* @__PURE__ */ (0, import_jsx_runtime45.jsx)(
3071
+ return /* @__PURE__ */ (0, import_jsx_runtime46.jsxs)("span", { className: "inline-flex items-center overflow-hidden rounded-full bg-[var(--color-neutral-100)] text-body", children: [
3072
+ /* @__PURE__ */ (0, import_jsx_runtime46.jsx)(
3022
3073
  "button",
3023
3074
  {
3024
3075
  type: "button",
@@ -3030,7 +3081,7 @@ function GenreTreeView({
3030
3081
  children: filterArtist.name
3031
3082
  }
3032
3083
  ),
3033
- /* @__PURE__ */ (0, import_jsx_runtime45.jsx)(
3084
+ /* @__PURE__ */ (0, import_jsx_runtime46.jsx)(
3034
3085
  "button",
3035
3086
  {
3036
3087
  type: "button",
@@ -3042,8 +3093,8 @@ function GenreTreeView({
3042
3093
  )
3043
3094
  ] });
3044
3095
  };
3045
- const actions = /* @__PURE__ */ (0, import_jsx_runtime45.jsxs)(import_jsx_runtime45.Fragment, { children: [
3046
- !isLoading && !isError && /* @__PURE__ */ (0, import_jsx_runtime45.jsx)(
3096
+ const actions = /* @__PURE__ */ (0, import_jsx_runtime46.jsxs)(import_jsx_runtime46.Fragment, { children: [
3097
+ !isLoading && !isError && /* @__PURE__ */ (0, import_jsx_runtime46.jsx)(
3047
3098
  GenreTreeSearch,
3048
3099
  {
3049
3100
  artistBaseUrl,
@@ -3076,9 +3127,9 @@ function GenreTreeView({
3076
3127
  extraGroups: extraSearchGroups
3077
3128
  }
3078
3129
  ),
3079
- !isLoading && !isError && filterArtist && /* @__PURE__ */ (0, import_jsx_runtime45.jsx)("div", { className: "hidden w-full justify-center max-md:flex", children: renderArtistFilter() }),
3080
- /* @__PURE__ */ (0, import_jsx_runtime45.jsxs)("div", { className: "contents max-md:flex max-md:w-full max-md:items-center max-md:gap-2 max-md:overflow-x-auto max-md:[scrollbar-width:none] max-md:[&>*]:shrink-0", children: [
3081
- !isLoading && !isError && !isControlled && /* @__PURE__ */ (0, import_jsx_runtime45.jsx)(
3130
+ !isLoading && !isError && filterArtist && /* @__PURE__ */ (0, import_jsx_runtime46.jsx)("div", { className: "hidden w-full justify-center max-md:flex", children: renderArtistFilter() }),
3131
+ /* @__PURE__ */ (0, import_jsx_runtime46.jsxs)("div", { className: "contents max-md:flex max-md:w-full max-md:items-center max-md:gap-2 max-md:overflow-x-auto max-md:[scrollbar-width:none] max-md:[&>*]:shrink-0", children: [
3132
+ !isLoading && !isError && !isControlled && /* @__PURE__ */ (0, import_jsx_runtime46.jsx)(
3082
3133
  import_ui21.SegmentedControl,
3083
3134
  {
3084
3135
  "aria-label": "Tree view mode",
@@ -3092,13 +3143,13 @@ function GenreTreeView({
3092
3143
  ]
3093
3144
  }
3094
3145
  ),
3095
- !isLoading && !isError && showDisplayOptions && /* @__PURE__ */ (0, import_jsx_runtime45.jsx)(
3146
+ !isLoading && !isError && showDisplayOptions && /* @__PURE__ */ (0, import_jsx_runtime46.jsx)(
3096
3147
  "div",
3097
3148
  {
3098
3149
  className: "flex items-center gap-1",
3099
3150
  role: "group",
3100
3151
  "aria-label": "Tree display options",
3101
- children: !isRotationControlled && viewMode !== "stacked" && viewMode !== "outline" && /* @__PURE__ */ (0, import_jsx_runtime45.jsx)(
3152
+ children: !isRotationControlled && viewMode !== "stacked" && viewMode !== "outline" && /* @__PURE__ */ (0, import_jsx_runtime46.jsx)(
3102
3153
  import_ui21.Button,
3103
3154
  {
3104
3155
  variant: allowWheelRotation ? "default" : "outline",
@@ -3109,7 +3160,7 @@ function GenreTreeView({
3109
3160
  )
3110
3161
  }
3111
3162
  ),
3112
- !isLoading && !isError && !readOnly && showAddRoot && /* @__PURE__ */ (0, import_jsx_runtime45.jsx)(
3163
+ !isLoading && !isError && !readOnly && showAddRoot && /* @__PURE__ */ (0, import_jsx_runtime46.jsx)(
3113
3164
  import_ui21.IconTextButton,
3114
3165
  {
3115
3166
  icon: import_lucide_react13.Plus,
@@ -3119,38 +3170,38 @@ function GenreTreeView({
3119
3170
  )
3120
3171
  ] })
3121
3172
  ] });
3122
- return /* @__PURE__ */ (0, import_jsx_runtime45.jsxs)(
3173
+ return /* @__PURE__ */ (0, import_jsx_runtime46.jsxs)(
3123
3174
  "div",
3124
3175
  {
3125
3176
  "data-left-panel": isPanelShown && !isCollapsed ? void 0 : "hidden",
3126
3177
  className: ["genre-tree-view relative flex flex-col h-full", placement === "regions" && REGIONS_CLASS, className2].filter(Boolean).join(" "),
3127
3178
  children: [
3128
- /* @__PURE__ */ (0, import_jsx_runtime45.jsx)(
3179
+ /* @__PURE__ */ (0, import_jsx_runtime46.jsx)(
3129
3180
  "div",
3130
3181
  {
3131
3182
  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"}`,
3132
3183
  children: actions
3133
3184
  }
3134
3185
  ),
3135
- !isLoading && !isError && filterArtist && /* @__PURE__ */ (0, import_jsx_runtime45.jsx)(
3186
+ !isLoading && !isError && filterArtist && /* @__PURE__ */ (0, import_jsx_runtime46.jsx)(
3136
3187
  "div",
3137
3188
  {
3138
3189
  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"}`,
3139
3190
  children: renderArtistFilter()
3140
3191
  }
3141
3192
  ),
3142
- /* @__PURE__ */ (0, import_jsx_runtime45.jsx)("div", { className: "content-container flex-1 min-h-0 flex flex-row gap-4", children: /* @__PURE__ */ (0, import_jsx_runtime45.jsxs)("div", { className: "tree-view-container relative flex-1 min-w-0 flex flex-col h-full", children: [
3143
- !isLoading && !isError && viewMode === "outline" && floatingControlsLeading && /* @__PURE__ */ (0, import_jsx_runtime45.jsx)("div", { className: "absolute bottom-3 right-3 z-10", children: floatingControlsLeading }),
3144
- isLoading ? /* @__PURE__ */ (0, import_jsx_runtime45.jsx)(import_genre_tree_view5.GenreTreeViewSkeleton, { viewMode }) : isError ? /* @__PURE__ */ (0, import_jsx_runtime45.jsxs)("p", { role: "alert", className: "m-auto text-center", children: [
3193
+ /* @__PURE__ */ (0, import_jsx_runtime46.jsx)("div", { className: "content-container flex-1 min-h-0 flex flex-row gap-4", children: /* @__PURE__ */ (0, import_jsx_runtime46.jsxs)("div", { className: "tree-view-container relative flex-1 min-w-0 flex flex-col h-full", children: [
3194
+ !isLoading && !isError && viewMode === "outline" && floatingControlsLeading && /* @__PURE__ */ (0, import_jsx_runtime46.jsx)("div", { className: "absolute bottom-3 right-3 z-10", children: floatingControlsLeading }),
3195
+ isLoading ? /* @__PURE__ */ (0, import_jsx_runtime46.jsx)(import_genre_tree_view5.GenreTreeViewSkeleton, { viewMode }) : isError ? /* @__PURE__ */ (0, import_jsx_runtime46.jsxs)("p", { role: "alert", className: "m-auto text-center", children: [
3145
3196
  "Couldn't load the genre tree.",
3146
3197
  " ",
3147
- /* @__PURE__ */ (0, import_jsx_runtime45.jsx)("button", { type: "button", className: "underline", onClick: refetch, children: "Retry" })
3198
+ /* @__PURE__ */ (0, import_jsx_runtime46.jsx)("button", { type: "button", className: "underline", onClick: refetch, children: "Retry" })
3148
3199
  ] }) : viewMode === "wheel" ? (
3149
3200
  // The info panel's top edge aligns with the tree's; the search bar floats `--layout-panel-inset` below it,
3150
3201
  // and the header inset (the bar's height plus that inset) extends the header's fill behind the bar,
3151
3202
  // Google Maps style; the 8px bottom padding mirrors the inset above the bar, and the hidden title's height is zeroed.
3152
3203
  // The panel's own Zoom in button is hidden: the search bar carries it.
3153
- /* @__PURE__ */ (0, import_jsx_runtime45.jsx)("div", { className: "tree-container search-overlay relative flex-1 w-full min-h-0 ", children: /* @__PURE__ */ (0, import_jsx_runtime45.jsx)(GenreTreeWheelHandoff, { skeleton: /* @__PURE__ */ (0, import_jsx_runtime45.jsx)(import_genre_tree_view5.GenreTreeWheelSkeleton, {}), onRevealedChange: setIsWheelRevealed, children: /* @__PURE__ */ (0, import_jsx_runtime45.jsx)(
3204
+ /* @__PURE__ */ (0, import_jsx_runtime46.jsx)("div", { className: "tree-container search-overlay relative flex-1 w-full min-h-0 ", children: /* @__PURE__ */ (0, import_jsx_runtime46.jsx)(GenreTreeWheelHandoff, { skeleton: /* @__PURE__ */ (0, import_jsx_runtime46.jsx)(import_genre_tree_view5.GenreTreeWheelSkeleton, {}), onRevealedChange: setIsWheelRevealed, children: /* @__PURE__ */ (0, import_jsx_runtime46.jsx)(
3154
3205
  GenrePlaylistTreeWheel,
3155
3206
  {
3156
3207
  scope,
@@ -3168,9 +3219,9 @@ function GenreTreeView({
3168
3219
  allowWheelRotation,
3169
3220
  sizeMetric: isArtistFiltered || !canSizeByViews ? "songs" : deferredSizeMetric,
3170
3221
  busy,
3171
- floatingControlsLeading: floatingControlsLeading || canSizeByViews ? /* @__PURE__ */ (0, import_jsx_runtime45.jsxs)("div", { className: "flex items-center gap-2", children: [
3222
+ floatingControlsLeading: floatingControlsLeading || canSizeByViews ? /* @__PURE__ */ (0, import_jsx_runtime46.jsxs)("div", { className: "flex items-center gap-2", children: [
3172
3223
  floatingControlsLeading,
3173
- canSizeByViews && /* @__PURE__ */ (0, import_jsx_runtime45.jsx)(SizeMetricToggle, { value: sizeMetric, onChange: changeSizeMetric })
3224
+ canSizeByViews && /* @__PURE__ */ (0, import_jsx_runtime46.jsx)(SizeMetricToggle, { value: sizeMetric, onChange: changeSizeMetric })
3174
3225
  ] }) : void 0,
3175
3226
  defaultViewport,
3176
3227
  viewportRestore,
@@ -3179,7 +3230,7 @@ function GenreTreeView({
3179
3230
  ) }) })
3180
3231
  ) : viewMode === "pop-core" ? (
3181
3232
  // Same as the wheel: the header inset extends its fill behind the floating search bar, Google Maps style.
3182
- /* @__PURE__ */ (0, import_jsx_runtime45.jsx)("div", { className: "tree-container search-overlay relative flex-1 w-full min-h-0 ", children: /* @__PURE__ */ (0, import_jsx_runtime45.jsx)(GenreTreeWheelHandoff, { skeleton: /* @__PURE__ */ (0, import_jsx_runtime45.jsx)(import_genre_tree_view5.GenreTreeWheelSkeleton, {}), onRevealedChange: setIsWheelRevealed, children: /* @__PURE__ */ (0, import_jsx_runtime45.jsx)(
3233
+ /* @__PURE__ */ (0, import_jsx_runtime46.jsx)("div", { className: "tree-container search-overlay relative flex-1 w-full min-h-0 ", children: /* @__PURE__ */ (0, import_jsx_runtime46.jsx)(GenreTreeWheelHandoff, { skeleton: /* @__PURE__ */ (0, import_jsx_runtime46.jsx)(import_genre_tree_view5.GenreTreeWheelSkeleton, {}), onRevealedChange: setIsWheelRevealed, children: /* @__PURE__ */ (0, import_jsx_runtime46.jsx)(
3183
3234
  GenrePlaylistTreeWheelRadialPopCore,
3184
3235
  {
3185
3236
  scope,
@@ -3197,9 +3248,9 @@ function GenreTreeView({
3197
3248
  allowWheelRotation,
3198
3249
  sizeMetric: isArtistFiltered || !canSizeByViews ? "songs" : deferredSizeMetric,
3199
3250
  busy,
3200
- floatingControlsLeading: floatingControlsLeading || canSizeByViews ? /* @__PURE__ */ (0, import_jsx_runtime45.jsxs)("div", { className: "flex items-center gap-2", children: [
3251
+ floatingControlsLeading: floatingControlsLeading || canSizeByViews ? /* @__PURE__ */ (0, import_jsx_runtime46.jsxs)("div", { className: "flex items-center gap-2", children: [
3201
3252
  floatingControlsLeading,
3202
- canSizeByViews && /* @__PURE__ */ (0, import_jsx_runtime45.jsx)(SizeMetricToggle, { value: sizeMetric, onChange: changeSizeMetric })
3253
+ canSizeByViews && /* @__PURE__ */ (0, import_jsx_runtime46.jsx)(SizeMetricToggle, { value: sizeMetric, onChange: changeSizeMetric })
3203
3254
  ] }) : void 0,
3204
3255
  defaultViewport,
3205
3256
  viewportRestore,
@@ -3207,7 +3258,7 @@ function GenreTreeView({
3207
3258
  ...focus
3208
3259
  }
3209
3260
  ) }) })
3210
- ) : viewMode === "outline" ? /* @__PURE__ */ (0, import_jsx_runtime45.jsx)("div", { className: "tree-container flex-1 min-h-0 w-full overflow-y-auto relative", children: /* @__PURE__ */ (0, import_jsx_runtime45.jsx)(
3261
+ ) : viewMode === "outline" ? /* @__PURE__ */ (0, import_jsx_runtime46.jsx)("div", { className: "tree-container flex-1 min-h-0 w-full overflow-y-auto relative", children: /* @__PURE__ */ (0, import_jsx_runtime46.jsx)(
3211
3262
  GenrePlaylistTreeWheelRadialPopCore,
3212
3263
  {
3213
3264
  outline: true,
@@ -3224,13 +3275,13 @@ function GenreTreeView({
3224
3275
  selectedNodeId,
3225
3276
  readOnly
3226
3277
  }
3227
- ) }) : /* @__PURE__ */ (0, import_jsx_runtime45.jsx)("div", { className: "tree-container relative flex flex-col gap-4 w-full pt-14 overflow-x-auto overflow-y-auto text-[var(--color-text)]", children: Object.entries(groupedGenrePlaylistsByRoot).map(
3278
+ ) }) : /* @__PURE__ */ (0, import_jsx_runtime46.jsx)("div", { className: "tree-container relative flex flex-col gap-4 w-full pt-14 overflow-x-auto overflow-y-auto text-[var(--color-text)]", children: Object.entries(groupedGenrePlaylistsByRoot).map(
3228
3279
  ([uuid, genrePlaylistTreePerRoot]) => {
3229
- return /* @__PURE__ */ (0, import_jsx_runtime45.jsx)(
3280
+ return /* @__PURE__ */ (0, import_jsx_runtime46.jsx)(
3230
3281
  "div",
3231
3282
  {
3232
3283
  className: "tree-per-root-container relative shrink-0 h-[28rem] mt-2 mr-16 p-2 bg-[var(--color-neutral-100)] rounded-lg",
3233
- children: /* @__PURE__ */ (0, import_jsx_runtime45.jsx)("div", { className: "graph-container relative z-10 h-full", children: /* @__PURE__ */ (0, import_jsx_runtime45.jsx)(
3284
+ children: /* @__PURE__ */ (0, import_jsx_runtime46.jsx)("div", { className: "graph-container relative z-10 h-full", children: /* @__PURE__ */ (0, import_jsx_runtime46.jsx)(
3234
3285
  GenrePlaylistTreePerRoot,
3235
3286
  {
3236
3287
  scope,
@@ -3257,7 +3308,7 @@ function GenreTreeView({
3257
3308
  );
3258
3309
  }
3259
3310
  ) }),
3260
- /* @__PURE__ */ (0, import_jsx_runtime45.jsx)(
3311
+ /* @__PURE__ */ (0, import_jsx_runtime46.jsx)(
3261
3312
  import_ui21.DetailPanel,
3262
3313
  {
3263
3314
  ref: panelRef,
@@ -3272,7 +3323,7 @@ function GenreTreeView({
3272
3323
  onExpandedChange: activePanel?.onExpandedChange,
3273
3324
  aside: activePanel?.aside,
3274
3325
  edgeTab: detailTab,
3275
- children: activePanel?.aside && !activePanel.expanded ? /* @__PURE__ */ (0, import_jsx_runtime45.jsxs)(import_jsx_runtime45.Fragment, { children: [
3326
+ children: activePanel?.aside && !activePanel.expanded ? /* @__PURE__ */ (0, import_jsx_runtime46.jsxs)(import_jsx_runtime46.Fragment, { children: [
3276
3327
  activePanel.content,
3277
3328
  activePanel.aside
3278
3329
  ] }) : activePanel?.content
@@ -3290,10 +3341,10 @@ var import_genre_tree_view6 = require("@behindthemusictree/genre-tree-view");
3290
3341
  // src/genre-tree/track-list-sidebar/TrackListSidebar.tsx
3291
3342
  var import_react20 = require("react");
3292
3343
  var import_lucide_react15 = require("lucide-react");
3293
- var import_genre_tree31 = require("@behindthemusictree/app-kit/genre-tree");
3294
3344
  var import_genre_tree32 = require("@behindthemusictree/app-kit/genre-tree");
3345
+ var import_genre_tree33 = require("@behindthemusictree/app-kit/genre-tree");
3295
3346
  var import_track_noun3 = require("@behindthemusictree/app-kit/track-noun");
3296
- var import_jsx_runtime46 = require("react/jsx-runtime");
3347
+ var import_jsx_runtime47 = require("react/jsx-runtime");
3297
3348
  var useBrowserLayoutEffect = typeof window === "undefined" ? import_react20.useEffect : import_react20.useLayoutEffect;
3298
3349
  function TrackListSidebar({
3299
3350
  className: className2 = "",
@@ -3306,7 +3357,7 @@ function TrackListSidebar({
3306
3357
  renderArtistLink,
3307
3358
  renderSongLink
3308
3359
  }) {
3309
- const { trackList, loadMore, autoAdvanceScrollRequest } = (0, import_genre_tree31.useTrackList)();
3360
+ const { trackList, loadMore, autoAdvanceScrollRequest } = (0, import_genre_tree32.useTrackList)();
3310
3361
  const trackNoun = (0, import_track_noun3.useTrackNoun)();
3311
3362
  const trackListRef = (0, import_react20.useRef)(null);
3312
3363
  const previousOriginRef = (0, import_react20.useRef)(trackList?.origin ?? null);
@@ -3395,25 +3446,25 @@ function TrackListSidebar({
3395
3446
  }
3396
3447
  };
3397
3448
  const positionClasses = layout === "inline" ? "relative w-full flex h-full flex-col" : "fixed right-0 w-[50rem] flex flex-col";
3398
- return trackList ? /* @__PURE__ */ (0, import_jsx_runtime46.jsxs)(
3449
+ return trackList ? /* @__PURE__ */ (0, import_jsx_runtime47.jsxs)(
3399
3450
  "div",
3400
3451
  {
3401
3452
  "data-theme": "dark",
3402
3453
  className: `track-list-sidebar ${positionClasses} rounded-2xl bg-[var(--color-surface)] pb-1 ${className2}`,
3403
3454
  style: layout === "fixed" ? { top: "63.5px", maxHeight: "calc(100vh - 63.5px - 79px)" } : void 0,
3404
3455
  children: [
3405
- /* @__PURE__ */ (0, import_jsx_runtime46.jsxs)("div", { className: "header flex h-16 items-center px-4 py-2 text-[var(--color-text)]", children: [
3406
- /* @__PURE__ */ (0, import_jsx_runtime46.jsxs)("div", { className: "min-w-0 flex flex-1 flex-col justify-center", children: [
3407
- /* @__PURE__ */ (0, import_jsx_runtime46.jsxs)("div", { className: "origin flex text-xl", children: [
3408
- /* @__PURE__ */ (0, import_jsx_runtime46.jsx)("div", { className: "from h-auto flex flex-col justify-center items-center mr-2", children: "From" }),
3409
- /* @__PURE__ */ (0, import_jsx_runtime46.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_runtime46.jsx)("div", { className: "name max-w-full truncate", children: trackList.origin.label }) })
3456
+ /* @__PURE__ */ (0, import_jsx_runtime47.jsxs)("div", { className: "header flex h-16 items-center px-4 py-2 text-[var(--color-text)]", children: [
3457
+ /* @__PURE__ */ (0, import_jsx_runtime47.jsxs)("div", { className: "min-w-0 flex flex-1 flex-col justify-center", children: [
3458
+ /* @__PURE__ */ (0, import_jsx_runtime47.jsxs)("div", { className: "origin flex text-xl", children: [
3459
+ /* @__PURE__ */ (0, import_jsx_runtime47.jsx)("div", { className: "from h-auto flex flex-col justify-center items-center mr-2", children: "From" }),
3460
+ /* @__PURE__ */ (0, import_jsx_runtime47.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_runtime47.jsx)("div", { className: "name max-w-full truncate", children: trackList.origin.label }) })
3410
3461
  ] }),
3411
- /* @__PURE__ */ (0, import_jsx_runtime46.jsxs)("div", { className: "info flex items-center text-body text-[var(--color-text-muted)]", children: [
3412
- 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 `,
3462
+ /* @__PURE__ */ (0, import_jsx_runtime47.jsxs)("div", { className: "info flex items-center text-body text-[var(--color-text-muted)]", children: [
3463
+ 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 `,
3413
3464
  `${trackList.total} ${trackList.total > 1 ? trackNoun.plural : trackNoun.singular}`
3414
3465
  ] })
3415
3466
  ] }),
3416
- (isCollapsed && onExpand || !isCollapsed && onMinimize) && /* @__PURE__ */ (0, import_jsx_runtime46.jsx)(
3467
+ (isCollapsed && onExpand || !isCollapsed && onMinimize) && /* @__PURE__ */ (0, import_jsx_runtime47.jsx)(
3417
3468
  "button",
3418
3469
  {
3419
3470
  type: "button",
@@ -3422,11 +3473,11 @@ function TrackListSidebar({
3422
3473
  "aria-expanded": !isCollapsed,
3423
3474
  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"}`,
3424
3475
  onClick: isCollapsed ? onExpand : onMinimize,
3425
- children: /* @__PURE__ */ (0, import_jsx_runtime46.jsx)(import_lucide_react15.ListMusic, { "aria-hidden": "true", className: "h-5 w-5" })
3476
+ children: /* @__PURE__ */ (0, import_jsx_runtime47.jsx)(import_lucide_react15.ListMusic, { "aria-hidden": "true", className: "h-5 w-5" })
3426
3477
  }
3427
3478
  )
3428
3479
  ] }),
3429
- !isCollapsed && /* @__PURE__ */ (0, import_jsx_runtime46.jsxs)(
3480
+ !isCollapsed && /* @__PURE__ */ (0, import_jsx_runtime47.jsxs)(
3430
3481
  "ul",
3431
3482
  {
3432
3483
  ref: trackListRef,
@@ -3436,7 +3487,7 @@ function TrackListSidebar({
3436
3487
  onPointerDown: markUserScrollIntent,
3437
3488
  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",
3438
3489
  children: [
3439
- trackList.tracks.map((track, index) => /* @__PURE__ */ (0, import_jsx_runtime46.jsx)("li", { "data-track-uuid": track.uuid, children: /* @__PURE__ */ (0, import_jsx_runtime46.jsx)(
3490
+ trackList.tracks.map((track, index) => /* @__PURE__ */ (0, import_jsx_runtime47.jsx)("li", { "data-track-uuid": track.uuid, children: /* @__PURE__ */ (0, import_jsx_runtime47.jsx)(
3440
3491
  TrackItem,
3441
3492
  {
3442
3493
  track,
@@ -3447,7 +3498,7 @@ function TrackListSidebar({
3447
3498
  renderSongLink
3448
3499
  }
3449
3500
  ) }, track.uuid)),
3450
- hasMore && /* @__PURE__ */ (0, import_jsx_runtime46.jsx)("li", { ref: loadMoreSentinelRef, "aria-hidden": "true", className: "h-px" })
3501
+ hasMore && /* @__PURE__ */ (0, import_jsx_runtime47.jsx)("li", { ref: loadMoreSentinelRef, "aria-hidden": "true", className: "h-px" })
3451
3502
  ]
3452
3503
  }
3453
3504
  )
@@ -3460,14 +3511,14 @@ function TrackListSidebar({
3460
3511
  var import_react21 = require("react");
3461
3512
  var import_tailwind_merge = require("tailwind-merge");
3462
3513
  var import_player14 = require("@behindthemusictree/app-kit/player");
3463
- var import_genre_tree34 = require("@behindthemusictree/app-kit/genre-tree");
3514
+ var import_genre_tree35 = require("@behindthemusictree/app-kit/genre-tree");
3464
3515
  var import_ui22 = require("@behindthemusictree/ui");
3465
3516
 
3466
3517
  // src/genre-tree/PlayerControls.tsx
3467
3518
  var import_lucide_react16 = require("lucide-react");
3468
- var import_genre_tree33 = require("@behindthemusictree/app-kit/genre-tree");
3519
+ var import_genre_tree34 = require("@behindthemusictree/app-kit/genre-tree");
3469
3520
  var import_player13 = require("@behindthemusictree/app-kit/player");
3470
- var import_jsx_runtime47 = require("react/jsx-runtime");
3521
+ var import_jsx_runtime48 = require("react/jsx-runtime");
3471
3522
  function PlayerControls() {
3472
3523
  const {
3473
3524
  duration,
@@ -3479,22 +3530,22 @@ function PlayerControls() {
3479
3530
  setVolume,
3480
3531
  volume
3481
3532
  } = (0, import_player13.usePlayer)();
3482
- const { hasNextTrack, hasPreviousTrack, playNextTrack, playPreviousTrack } = (0, import_genre_tree33.useTrackList)();
3533
+ const { hasNextTrack, hasPreviousTrack, playNextTrack, playPreviousTrack } = (0, import_genre_tree34.useTrackList)();
3483
3534
  const currentTime = (0, import_player13.useCurrentTime)();
3484
3535
  const isBusy = isLoading || playState === import_player13.PlayStates.LOADING;
3485
3536
  const isReady = !!playerTrackObject?.isReady && !isBusy && !playerTrackObject.loadError;
3486
3537
  const safeDuration = Number.isFinite(duration) && duration > 0 ? duration : 0;
3487
3538
  const safeCurrentTime = Number.isFinite(currentTime) ? Math.max(0, Math.min(currentTime, safeDuration)) : 0;
3488
3539
  const isPlaying = playState === import_player13.PlayStates.PLAYING;
3489
- return /* @__PURE__ */ (0, import_jsx_runtime47.jsxs)(
3540
+ return /* @__PURE__ */ (0, import_jsx_runtime48.jsxs)(
3490
3541
  "section",
3491
3542
  {
3492
3543
  "aria-label": "Player controls",
3493
3544
  className: "player-controls flex shrink-0 flex-col gap-2 border-b border-zinc-800 px-3 py-2 text-zinc-200",
3494
3545
  children: [
3495
- /* @__PURE__ */ (0, import_jsx_runtime47.jsxs)("div", { className: "flex min-w-0 items-center gap-2 text-xs tabular-nums text-zinc-400", children: [
3496
- /* @__PURE__ */ (0, import_jsx_runtime47.jsx)("span", { "aria-label": "Elapsed time", children: (0, import_genre_tree33.formatTime)(safeCurrentTime) }),
3497
- /* @__PURE__ */ (0, import_jsx_runtime47.jsx)(
3546
+ /* @__PURE__ */ (0, import_jsx_runtime48.jsxs)("div", { className: "flex min-w-0 items-center gap-2 text-xs tabular-nums text-zinc-400", children: [
3547
+ /* @__PURE__ */ (0, import_jsx_runtime48.jsx)("span", { "aria-label": "Elapsed time", children: (0, import_genre_tree34.formatTime)(safeCurrentTime) }),
3548
+ /* @__PURE__ */ (0, import_jsx_runtime48.jsx)(
3498
3549
  "input",
3499
3550
  {
3500
3551
  "aria-label": "Seek",
@@ -3508,11 +3559,11 @@ function PlayerControls() {
3508
3559
  onChange: (event) => seekTo(Number(event.currentTarget.value))
3509
3560
  }
3510
3561
  ),
3511
- /* @__PURE__ */ (0, import_jsx_runtime47.jsx)("span", { "aria-label": "Duration", children: (0, import_genre_tree33.formatTime)(safeDuration) })
3562
+ /* @__PURE__ */ (0, import_jsx_runtime48.jsx)("span", { "aria-label": "Duration", children: (0, import_genre_tree34.formatTime)(safeDuration) })
3512
3563
  ] }),
3513
- /* @__PURE__ */ (0, import_jsx_runtime47.jsxs)("div", { className: "grid min-h-9 grid-cols-[1fr_auto_1fr] items-center gap-3", children: [
3514
- /* @__PURE__ */ (0, import_jsx_runtime47.jsxs)("div", { className: "col-start-2 flex items-center justify-center gap-2", children: [
3515
- /* @__PURE__ */ (0, import_jsx_runtime47.jsx)(
3564
+ /* @__PURE__ */ (0, import_jsx_runtime48.jsxs)("div", { className: "grid min-h-9 grid-cols-[1fr_auto_1fr] items-center gap-3", children: [
3565
+ /* @__PURE__ */ (0, import_jsx_runtime48.jsxs)("div", { className: "col-start-2 flex items-center justify-center gap-2", children: [
3566
+ /* @__PURE__ */ (0, import_jsx_runtime48.jsx)(
3516
3567
  "button",
3517
3568
  {
3518
3569
  type: "button",
@@ -3521,10 +3572,10 @@ function PlayerControls() {
3521
3572
  disabled: !isReady || !hasPreviousTrack,
3522
3573
  onClick: playPreviousTrack,
3523
3574
  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",
3524
- children: /* @__PURE__ */ (0, import_jsx_runtime47.jsx)(import_lucide_react16.SkipBack, { "aria-hidden": "true", className: "h-4 w-4" })
3575
+ children: /* @__PURE__ */ (0, import_jsx_runtime48.jsx)(import_lucide_react16.SkipBack, { "aria-hidden": "true", className: "h-4 w-4" })
3525
3576
  }
3526
3577
  ),
3527
- /* @__PURE__ */ (0, import_jsx_runtime47.jsx)(
3578
+ /* @__PURE__ */ (0, import_jsx_runtime48.jsx)(
3528
3579
  "button",
3529
3580
  {
3530
3581
  type: "button",
@@ -3533,10 +3584,10 @@ function PlayerControls() {
3533
3584
  disabled: !isReady,
3534
3585
  onClick: handlePlayPauseAction,
3535
3586
  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",
3536
- children: isPlaying ? /* @__PURE__ */ (0, import_jsx_runtime47.jsx)(import_lucide_react16.Pause, { "aria-hidden": "true", className: "h-4 w-4", fill: "currentColor" }) : /* @__PURE__ */ (0, import_jsx_runtime47.jsx)(import_lucide_react16.Play, { "aria-hidden": "true", className: "h-4 w-4", fill: "currentColor" })
3587
+ children: isPlaying ? /* @__PURE__ */ (0, import_jsx_runtime48.jsx)(import_lucide_react16.Pause, { "aria-hidden": "true", className: "h-4 w-4", fill: "currentColor" }) : /* @__PURE__ */ (0, import_jsx_runtime48.jsx)(import_lucide_react16.Play, { "aria-hidden": "true", className: "h-4 w-4", fill: "currentColor" })
3537
3588
  }
3538
3589
  ),
3539
- /* @__PURE__ */ (0, import_jsx_runtime47.jsx)(
3590
+ /* @__PURE__ */ (0, import_jsx_runtime48.jsx)(
3540
3591
  "button",
3541
3592
  {
3542
3593
  type: "button",
@@ -3545,14 +3596,14 @@ function PlayerControls() {
3545
3596
  disabled: !isReady || !hasNextTrack,
3546
3597
  onClick: playNextTrack,
3547
3598
  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",
3548
- children: /* @__PURE__ */ (0, import_jsx_runtime47.jsx)(import_lucide_react16.SkipForward, { "aria-hidden": "true", className: "h-4 w-4" })
3599
+ children: /* @__PURE__ */ (0, import_jsx_runtime48.jsx)(import_lucide_react16.SkipForward, { "aria-hidden": "true", className: "h-4 w-4" })
3549
3600
  }
3550
3601
  )
3551
3602
  ] }),
3552
- /* @__PURE__ */ (0, import_jsx_runtime47.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: [
3553
- volume === 0 ? /* @__PURE__ */ (0, import_jsx_runtime47.jsx)(import_lucide_react16.VolumeX, { "aria-hidden": "true", className: "h-[0.7rem] w-[0.7rem] shrink-0" }) : /* @__PURE__ */ (0, import_jsx_runtime47.jsx)(import_lucide_react16.Volume2, { "aria-hidden": "true", className: "h-[0.7rem] w-[0.7rem] shrink-0" }),
3554
- /* @__PURE__ */ (0, import_jsx_runtime47.jsx)("span", { className: "sr-only", children: "Volume" }),
3555
- /* @__PURE__ */ (0, import_jsx_runtime47.jsx)(
3603
+ /* @__PURE__ */ (0, import_jsx_runtime48.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: [
3604
+ volume === 0 ? /* @__PURE__ */ (0, import_jsx_runtime48.jsx)(import_lucide_react16.VolumeX, { "aria-hidden": "true", className: "h-[0.7rem] w-[0.7rem] shrink-0" }) : /* @__PURE__ */ (0, import_jsx_runtime48.jsx)(import_lucide_react16.Volume2, { "aria-hidden": "true", className: "h-[0.7rem] w-[0.7rem] shrink-0" }),
3605
+ /* @__PURE__ */ (0, import_jsx_runtime48.jsx)("span", { className: "sr-only", children: "Volume" }),
3606
+ /* @__PURE__ */ (0, import_jsx_runtime48.jsx)(
3556
3607
  "input",
3557
3608
  {
3558
3609
  "aria-label": "Volume",
@@ -3574,7 +3625,7 @@ function PlayerControls() {
3574
3625
  }
3575
3626
 
3576
3627
  // src/genre-tree/PlayerPanel.tsx
3577
- var import_jsx_runtime48 = require("react/jsx-runtime");
3628
+ var import_jsx_runtime49 = require("react/jsx-runtime");
3578
3629
  var PANEL_ID = "player-panel";
3579
3630
  var DEFAULT_EXTENDED_WIDTH = "calc(100vw - var(--layout-center-left) - var(--layout-gutter))";
3580
3631
  function PlayerPanel({
@@ -3587,7 +3638,7 @@ function PlayerPanel({
3587
3638
  renderSongLink
3588
3639
  }) {
3589
3640
  const { playerTrackObject, retryPlayback } = (0, import_player14.usePlayer)();
3590
- const { toggleTrackListSidebar, isTrackListSidebarVisible } = (0, import_genre_tree34.useTrackListSidebarVisibility)();
3641
+ const { toggleTrackListSidebar, isTrackListSidebarVisible } = (0, import_genre_tree35.useTrackListSidebarVisibility)();
3591
3642
  const [lastLoadedTrackTitle, setLastLoadedTrackTitle] = (0, import_react21.useState)("");
3592
3643
  const loadedTrackTitle = playerTrackObject?.track.title;
3593
3644
  (0, import_react21.useEffect)(() => {
@@ -3614,7 +3665,7 @@ function PlayerPanel({
3614
3665
  onClick: () => setIsExtendRequested(true),
3615
3666
  expanded: false
3616
3667
  } : null;
3617
- return /* @__PURE__ */ (0, import_jsx_runtime48.jsxs)(
3668
+ return /* @__PURE__ */ (0, import_jsx_runtime49.jsxs)(
3618
3669
  "div",
3619
3670
  {
3620
3671
  id: PANEL_ID,
@@ -3627,7 +3678,7 @@ function PlayerPanel({
3627
3678
  ),
3628
3679
  style: isExtended ? { width: extendedWidth } : void 0,
3629
3680
  children: [
3630
- /* @__PURE__ */ (0, import_jsx_runtime48.jsx)(
3681
+ /* @__PURE__ */ (0, import_jsx_runtime49.jsx)(
3631
3682
  import_ui22.PanelEdgeTab,
3632
3683
  {
3633
3684
  side: "right",
@@ -3637,7 +3688,7 @@ function PlayerPanel({
3637
3688
  className: "absolute right-full top-1/2 -translate-y-1/2"
3638
3689
  }
3639
3690
  ),
3640
- /* @__PURE__ */ (0, import_jsx_runtime48.jsxs)(
3691
+ /* @__PURE__ */ (0, import_jsx_runtime49.jsxs)(
3641
3692
  "div",
3642
3693
  {
3643
3694
  className: (0, import_tailwind_merge.twMerge)(
@@ -3645,20 +3696,20 @@ function PlayerPanel({
3645
3696
  isExtended ? "flex-row" : "flex-col"
3646
3697
  ),
3647
3698
  children: [
3648
- /* @__PURE__ */ (0, import_jsx_runtime48.jsxs)("div", { className: "flex w-[var(--layout-right-w)] shrink-0 flex-col", children: [
3649
- /* @__PURE__ */ (0, import_jsx_runtime48.jsx)("div", { className: "flex shrink-0 items-center justify-between gap-2 border-b border-zinc-800 p-2", children: /* @__PURE__ */ (0, import_jsx_runtime48.jsx)("span", { className: "min-w-0 truncate text-sm font-semibold", children: loadedTrackTitle || lastLoadedTrackTitle }) }),
3650
- /* @__PURE__ */ (0, import_jsx_runtime48.jsxs)("div", { className: "relative w-full shrink-0", children: [
3651
- /* @__PURE__ */ (0, import_jsx_runtime48.jsx)(import_player14.PlayerVideoSurface, { className: "aspect-video w-full bg-black" }),
3652
- playerTrackObject?.loadError && /* @__PURE__ */ (0, import_jsx_runtime48.jsxs)("span", { className: "absolute bottom-2 left-2 right-2 text-xs text-red-400", children: [
3699
+ /* @__PURE__ */ (0, import_jsx_runtime49.jsxs)("div", { className: "flex w-[var(--layout-right-w)] shrink-0 flex-col", children: [
3700
+ /* @__PURE__ */ (0, import_jsx_runtime49.jsx)("div", { className: "flex shrink-0 items-center justify-between gap-2 border-b border-zinc-800 p-2", children: /* @__PURE__ */ (0, import_jsx_runtime49.jsx)("span", { className: "min-w-0 truncate text-sm font-semibold", children: loadedTrackTitle || lastLoadedTrackTitle }) }),
3701
+ /* @__PURE__ */ (0, import_jsx_runtime49.jsxs)("div", { className: "relative w-full shrink-0", children: [
3702
+ /* @__PURE__ */ (0, import_jsx_runtime49.jsx)(import_player14.PlayerVideoSurface, { className: "aspect-video w-full bg-black" }),
3703
+ playerTrackObject?.loadError && /* @__PURE__ */ (0, import_jsx_runtime49.jsxs)("span", { className: "absolute bottom-2 left-2 right-2 text-xs text-red-400", children: [
3653
3704
  playerTrackObject.loadError,
3654
- /* @__PURE__ */ (0, import_jsx_runtime48.jsx)("button", { type: "button", className: "ml-2 underline", onClick: () => {
3705
+ /* @__PURE__ */ (0, import_jsx_runtime49.jsx)("button", { type: "button", className: "ml-2 underline", onClick: () => {
3655
3706
  void retryPlayback();
3656
3707
  }, children: "Retry" })
3657
3708
  ] })
3658
3709
  ] }),
3659
- /* @__PURE__ */ (0, import_jsx_runtime48.jsx)(PlayerControls, {})
3710
+ /* @__PURE__ */ (0, import_jsx_runtime49.jsx)(PlayerControls, {})
3660
3711
  ] }),
3661
- /* @__PURE__ */ (0, import_jsx_runtime48.jsx)(
3712
+ /* @__PURE__ */ (0, import_jsx_runtime49.jsx)(
3662
3713
  TrackListSidebar,
3663
3714
  {
3664
3715
  layout: "inline",
@@ -3684,33 +3735,33 @@ function PlayerPanel({
3684
3735
  // src/genre-tree/Rating.tsx
3685
3736
  var import_uuid = require("uuid");
3686
3737
  var import_ai = require("react-icons/ai");
3687
- var import_genre_tree35 = require("@behindthemusictree/app-kit/genre-tree");
3688
- var import_jsx_runtime49 = require("react/jsx-runtime");
3738
+ var import_genre_tree36 = require("@behindthemusictree/app-kit/genre-tree");
3739
+ var import_jsx_runtime50 = require("react/jsx-runtime");
3689
3740
  function Rating({ rating, handleChange }) {
3690
3741
  const instanceId = (0, import_uuid.v4)();
3691
- return /* @__PURE__ */ (0, import_jsx_runtime49.jsx)("div", { className: "flex items-center", children: [...Array(6)].map((_, i) => /* @__PURE__ */ (0, import_jsx_runtime49.jsxs)("div", { className: `relative ${i === 0 ? "mr-4" : "mr-1"}`, children: [
3692
- /* @__PURE__ */ (0, import_jsx_runtime49.jsx)(
3742
+ return /* @__PURE__ */ (0, import_jsx_runtime50.jsx)("div", { className: "flex items-center", children: [...Array(6)].map((_, i) => /* @__PURE__ */ (0, import_jsx_runtime50.jsxs)("div", { className: `relative ${i === 0 ? "mr-4" : "mr-1"}`, children: [
3743
+ /* @__PURE__ */ (0, import_jsx_runtime50.jsx)(
3693
3744
  "input",
3694
3745
  {
3695
3746
  id: `star-${instanceId}-${i}`,
3696
3747
  className: "w-6 h-6 opacity-0",
3697
3748
  type: "radio",
3698
3749
  name: "rating",
3699
- value: i === 0 ? import_genre_tree35.FORM_RATING_NULL_VALUE : i,
3750
+ value: i === 0 ? import_genre_tree36.FORM_RATING_NULL_VALUE : i,
3700
3751
  checked: rating === (i === 0 ? null : i),
3701
3752
  onChange: handleChange
3702
3753
  }
3703
3754
  ),
3704
- /* @__PURE__ */ (0, import_jsx_runtime49.jsx)("label", { htmlFor: `star-${instanceId}-${i}`, className: "cursor-pointer", children: i === 0 ? /* @__PURE__ */ (0, import_jsx_runtime49.jsxs)("div", { className: "flex", children: [
3705
- /* @__PURE__ */ (0, import_jsx_runtime49.jsx)(
3755
+ /* @__PURE__ */ (0, import_jsx_runtime50.jsx)("label", { htmlFor: `star-${instanceId}-${i}`, className: "cursor-pointer", children: i === 0 ? /* @__PURE__ */ (0, import_jsx_runtime50.jsxs)("div", { className: "flex", children: [
3756
+ /* @__PURE__ */ (0, import_jsx_runtime50.jsx)(
3706
3757
  import_ai.AiOutlineStar,
3707
3758
  {
3708
- 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)]"}`
3759
+ 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)]"}`
3709
3760
  }
3710
3761
  ),
3711
- /* @__PURE__ */ (0, import_jsx_runtime49.jsx)("span", { className: "flex-grow w-8" }),
3762
+ /* @__PURE__ */ (0, import_jsx_runtime50.jsx)("span", { className: "flex-grow w-8" }),
3712
3763
  " "
3713
- ] }) : /* @__PURE__ */ (0, import_jsx_runtime49.jsx)(
3764
+ ] }) : /* @__PURE__ */ (0, import_jsx_runtime50.jsx)(
3714
3765
  import_ai.AiFillStar,
3715
3766
  {
3716
3767
  className: `w-7 h-7 absolute top-0 left-0 ${(rating ?? -1) >= i ? "text-[var(--color-accent-rating)]" : "text-[var(--color-text-muted)]"}`
@@ -3722,7 +3773,7 @@ function Rating({ rating, handleChange }) {
3722
3773
  // src/genre-tree/EntityDetailPanel.tsx
3723
3774
  var import_react22 = require("react");
3724
3775
  var import_ui23 = require("@behindthemusictree/ui");
3725
- var import_jsx_runtime50 = require("react/jsx-runtime");
3776
+ var import_jsx_runtime51 = require("react/jsx-runtime");
3726
3777
  function EntityDetailPanel({
3727
3778
  scope,
3728
3779
  getBackendBaseUrl,
@@ -3734,6 +3785,7 @@ function EntityDetailPanel({
3734
3785
  onArtistUuidChange,
3735
3786
  renderArtistLink,
3736
3787
  renderSongLink,
3788
+ renderPlaceLink,
3737
3789
  collapsed,
3738
3790
  edgeTab,
3739
3791
  ...navigation
@@ -3745,8 +3797,9 @@ function EntityDetailPanel({
3745
3797
  getBackendBaseUrl,
3746
3798
  getArtistBackendBaseUrl,
3747
3799
  getShareUrl,
3748
- renderArtistLink: renderArtistLink ?? ((artist) => artist.isSpecialPurpose ? artist.name : /* @__PURE__ */ (0, import_jsx_runtime50.jsx)("button", { type: "button", onClick: () => onArtistUuidChange(artist.uuid), className: "hover:underline", children: artist.name })),
3749
- renderSongLink: renderSongLink ?? ((song) => /* @__PURE__ */ (0, import_jsx_runtime50.jsx)("button", { type: "button", onClick: () => onSongUuidChange(song.uuid), className: "hover:underline", children: song.title })),
3800
+ renderPlaceLink,
3801
+ renderArtistLink: renderArtistLink ?? ((artist) => artist.isSpecialPurpose ? artist.name : /* @__PURE__ */ (0, import_jsx_runtime51.jsx)("button", { type: "button", onClick: () => onArtistUuidChange(artist.uuid), className: "hover:underline", children: artist.name })),
3802
+ renderSongLink: renderSongLink ?? ((song) => /* @__PURE__ */ (0, import_jsx_runtime51.jsx)("button", { type: "button", onClick: () => onSongUuidChange(song.uuid), className: "hover:underline", children: song.title })),
3750
3803
  song: {
3751
3804
  uuid: songUuid,
3752
3805
  expanded: songUuid !== null && expandedSongUuid === songUuid,
@@ -3762,7 +3815,7 @@ function EntityDetailPanel({
3762
3815
  ...navigation
3763
3816
  });
3764
3817
  if (!panel) return null;
3765
- return /* @__PURE__ */ (0, import_jsx_runtime50.jsx)(
3818
+ return /* @__PURE__ */ (0, import_jsx_runtime51.jsx)(
3766
3819
  import_ui23.DetailPanel,
3767
3820
  {
3768
3821
  side: "left",
@@ -3774,7 +3827,7 @@ function EntityDetailPanel({
3774
3827
  onExpandedChange: panel.onExpandedChange,
3775
3828
  aside: panel.extraContent,
3776
3829
  edgeTab,
3777
- children: panel.extraContent && !panel.expanded ? /* @__PURE__ */ (0, import_jsx_runtime50.jsxs)(import_jsx_runtime50.Fragment, { children: [
3830
+ children: panel.extraContent && !panel.expanded ? /* @__PURE__ */ (0, import_jsx_runtime51.jsxs)(import_jsx_runtime51.Fragment, { children: [
3778
3831
  panel.content,
3779
3832
  panel.extraContent
3780
3833
  ] }) : panel.content