@behindthemusictree/app-kit-ui 40.0.0 → 41.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -50,11 +50,12 @@ module.exports = __toCommonJS(genre_tree_exports);
50
50
  // src/genre-tree/GenreTreeView.tsx
51
51
  var import_react19 = require("react");
52
52
  var import_react_query3 = require("@tanstack/react-query");
53
+ var import_lucide_react13 = require("lucide-react");
53
54
  var import_lucide_react14 = require("lucide-react");
54
- var import_lucide_react15 = 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
@@ -85,6 +86,21 @@ function toTreeNodes(genrePlaylists, sizeMetric, focusUuids) {
85
86
  }));
86
87
  }
87
88
 
89
+ // src/genre-tree/playlist-tree/toGenreNodeStyles.ts
90
+ function toGenreNodeStyles(relations, genrePlaylists) {
91
+ const styles = /* @__PURE__ */ new Map();
92
+ if (!relations) return styles;
93
+ const genreUuidByPlaylistUuid = new Map(
94
+ genrePlaylists.flatMap((genrePlaylist) => genrePlaylist.criteria ? [[genrePlaylist.uuid, genrePlaylist.criteria.uuid]] : [])
95
+ );
96
+ const drawn = [relations, relations.parentNode, ...relations.childNodes, ...relations.ancestorNodes];
97
+ for (const entry of drawn) {
98
+ const genreUuid = entry && genreUuidByPlaylistUuid.get(entry.node.id);
99
+ if (entry && genreUuid) styles.set(genreUuid, { fill: entry.fill, textColor: entry.textColor });
100
+ }
101
+ return styles;
102
+ }
103
+
88
104
  // src/genre-tree/playlist-tree/TreePerRoot.tsx
89
105
  var import_genre_tree3 = require("@behindthemusictree/app-kit/genre-tree");
90
106
  var import_react3 = require("react");
@@ -1387,6 +1403,29 @@ function ClampedText({ children, className: className2 = "" }) {
1387
1403
  ] });
1388
1404
  }
1389
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
+
1390
1429
  // src/genre-tree/GenrePlaylistTracks.tsx
1391
1430
  var import_react13 = require("react");
1392
1431
  var import_react_query = require("@tanstack/react-query");
@@ -1409,15 +1448,15 @@ var import_genre_tree16 = require("@behindthemusictree/app-kit/genre-tree");
1409
1448
 
1410
1449
  // src/genre-tree/MusicBrainzRecordingLink.tsx
1411
1450
  var import_genre_tree11 = require("@behindthemusictree/app-kit/genre-tree");
1412
- var import_jsx_runtime26 = require("react/jsx-runtime");
1451
+ var import_jsx_runtime27 = require("react/jsx-runtime");
1413
1452
  function MusicBrainzRecordingLink({
1414
1453
  song,
1415
1454
  children,
1416
1455
  className: className2 = ""
1417
1456
  }) {
1418
1457
  const url = (0, import_genre_tree11.musicBrainzRecordingUrl)(song);
1419
- if (!url) return /* @__PURE__ */ (0, import_jsx_runtime26.jsx)(import_jsx_runtime26.Fragment, { children });
1420
- 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)(
1421
1460
  "a",
1422
1461
  {
1423
1462
  href: url,
@@ -1434,7 +1473,7 @@ function MusicBrainzRecordingLink({
1434
1473
  // src/genre-tree/GenrePlaylistTracks.tsx
1435
1474
  var import_genre_tree17 = require("@behindthemusictree/app-kit/genre-tree");
1436
1475
  var import_track_noun = require("@behindthemusictree/app-kit/track-noun");
1437
- var import_jsx_runtime27 = require("react/jsx-runtime");
1476
+ var import_jsx_runtime28 = require("react/jsx-runtime");
1438
1477
  var CATALOGUE_PAGE_SIZE = 100;
1439
1478
  var SongPageSchema = (0, import_transport3.PaginatedResponseSchema)(import_genre_tree15.SongDetailedSchema).extend({
1440
1479
  overallTotal: import_zod.z.number().nullable(),
@@ -1521,35 +1560,35 @@ function GenrePlaylistTracks({
1521
1560
  started.catch((error) => {
1522
1561
  console.error("Failed to play genre playlist:", error);
1523
1562
  if (error instanceof import_genre_tree16.TracksFailureAlreadyReportedError) return;
1524
- 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 }));
1525
1564
  });
1526
1565
  };
1527
- 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: [
1528
1567
  renderTracksToolbar?.(),
1529
- 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: [
1530
1569
  "Couldn't load ",
1531
1570
  trackNoun.plural,
1532
1571
  ".",
1533
1572
  " ",
1534
- /* @__PURE__ */ (0, import_jsx_runtime27.jsx)("button", { type: "button", className: "underline", onClick: () => void refetch(), children: "Retry" })
1535
- ] }) : 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: [
1536
1575
  tracks.map((track, index) => {
1537
1576
  const label = track.artists?.length ? `${track.title} \u2014 ${track.artists.map((artist2) => artist2.name).join(", ")}` : track.title;
1538
1577
  const isCurrent = playerTrackObject?.track.id === track.uuid;
1539
1578
  const isPlaying = isCurrent && playState === import_player6.PlayStates.PLAYING;
1540
- const text = /* @__PURE__ */ (0, import_jsx_runtime27.jsxs)("span", { className: TEXT_CELL, children: [
1541
- /* @__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 }),
1542
- track.artists?.length ? /* @__PURE__ */ (0, import_jsx_runtime27.jsx)("span", { className: SEPARATOR, children: " \u2014 " }) : null,
1543
- /* @__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 }),
1544
- 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 })
1545
1584
  ] });
1546
1585
  const leading = renderSongLeading?.(track, index);
1547
1586
  const actions = renderSongActions?.(track);
1548
1587
  const isUnplayable = (0, import_genre_tree15.isSong)(track) && (0, import_genre_tree15.songUnplayableReason)(track) !== null;
1549
1588
  if (isUnplayable) {
1550
- 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: [
1551
1590
  leading,
1552
- /* @__PURE__ */ (0, import_jsx_runtime27.jsx)(
1591
+ /* @__PURE__ */ (0, import_jsx_runtime28.jsx)(
1553
1592
  "button",
1554
1593
  {
1555
1594
  type: "button",
@@ -1557,27 +1596,27 @@ function GenrePlaylistTracks({
1557
1596
  "aria-disabled": "true",
1558
1597
  "aria-label": label,
1559
1598
  className: "flex h-6 w-6 shrink-0 items-center justify-center rounded-full opacity-0 cursor-not-allowed",
1560
- 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 })
1561
1600
  }
1562
1601
  ),
1563
1602
  text,
1564
1603
  actions
1565
1604
  ] }, track.uuid);
1566
1605
  }
1567
- return /* @__PURE__ */ (0, import_jsx_runtime27.jsxs)(
1606
+ return /* @__PURE__ */ (0, import_jsx_runtime28.jsxs)(
1568
1607
  "li",
1569
1608
  {
1570
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)]" : ""}`,
1571
1610
  children: [
1572
1611
  leading,
1573
- /* @__PURE__ */ (0, import_jsx_runtime27.jsx)(
1612
+ /* @__PURE__ */ (0, import_jsx_runtime28.jsx)(
1574
1613
  "button",
1575
1614
  {
1576
1615
  type: "button",
1577
1616
  "aria-label": `${isPlaying ? "Pause" : "Play"} ${label}`,
1578
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"}`,
1579
1618
  onClick: () => isCurrent ? handlePlayPauseAction() : play(track.uuid),
1580
- 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 })
1581
1620
  }
1582
1621
  ),
1583
1622
  text,
@@ -1587,7 +1626,7 @@ function GenrePlaylistTracks({
1587
1626
  track.uuid
1588
1627
  );
1589
1628
  }),
1590
- 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" })
1591
1630
  ] })
1592
1631
  ] }) });
1593
1632
  }
@@ -1595,7 +1634,7 @@ function GenrePlaylistTracks({
1595
1634
  // src/genre-tree/GenreDetailExtras.tsx
1596
1635
  var import_track_noun2 = require("@behindthemusictree/app-kit/track-noun");
1597
1636
  var import_si2 = require("react-icons/si");
1598
- var import_jsx_runtime28 = require("react/jsx-runtime");
1637
+ var import_jsx_runtime29 = require("react/jsx-runtime");
1599
1638
  function GenreDetailExtras({
1600
1639
  genreUuid,
1601
1640
  genrePlaylist,
@@ -1621,17 +1660,18 @@ function GenreDetailExtras({
1621
1660
  criteriaOverviewSchema
1622
1661
  );
1623
1662
  if (isPending) {
1624
- return /* @__PURE__ */ (0, import_jsx_runtime28.jsxs)(import_jsx_runtime28.Fragment, { children: [
1625
- /* @__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" }) }) }),
1626
1665
  family
1627
1666
  ] });
1628
1667
  }
1629
- if (!overview) return /* @__PURE__ */ (0, import_jsx_runtime28.jsx)(import_jsx_runtime28.Fragment, { children: family });
1630
- 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;
1631
1670
  const wikidataUrl = wikidataId ? `https://www.wikidata.org/wiki/${encodeURIComponent(wikidataId)}` : null;
1632
- return /* @__PURE__ */ (0, import_jsx_runtime28.jsxs)(import_jsx_runtime28.Fragment, { children: [
1633
- (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: [
1634
- 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)(
1635
1675
  "a",
1636
1676
  {
1637
1677
  href: summarySourceUrl,
@@ -1639,12 +1679,12 @@ function GenreDetailExtras({
1639
1679
  rel: "noopener noreferrer",
1640
1680
  className: "inline-flex items-center gap-1.5 text-[var(--color-text-muted)] hover:text-[var(--color-text)]",
1641
1681
  children: [
1642
- /* @__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" }),
1643
1683
  "Wikipedia"
1644
1684
  ]
1645
1685
  }
1646
1686
  ),
1647
- wikidataUrl && /* @__PURE__ */ (0, import_jsx_runtime28.jsxs)(
1687
+ wikidataUrl && /* @__PURE__ */ (0, import_jsx_runtime29.jsxs)(
1648
1688
  "a",
1649
1689
  {
1650
1690
  href: wikidataUrl,
@@ -1652,17 +1692,17 @@ function GenreDetailExtras({
1652
1692
  rel: "noopener noreferrer",
1653
1693
  className: "inline-flex items-center gap-1.5 text-[var(--color-text-muted)] hover:text-[var(--color-text)]",
1654
1694
  children: [
1655
- /* @__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" }),
1656
1696
  "Wikidata"
1657
1697
  ]
1658
1698
  }
1659
1699
  )
1660
1700
  ] }) }),
1661
- /* @__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) }),
1662
1702
  family,
1663
1703
  renderGenreDetailExtras?.(overview),
1664
- (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" }) }),
1665
- /* @__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)(
1666
1706
  GenrePlaylistTracks,
1667
1707
  {
1668
1708
  genreUuid,
@@ -1680,10 +1720,10 @@ function GenreDetailExtras({
1680
1720
  }
1681
1721
 
1682
1722
  // src/genre-tree/GenreInfoPanelBody.tsx
1683
- var import_jsx_runtime29 = require("react/jsx-runtime");
1723
+ var import_jsx_runtime30 = require("react/jsx-runtime");
1684
1724
  var import_react14 = require("react");
1685
1725
  function GenreNodeActionBand({ genrePlaylist, playback, ...bandProps }) {
1686
- 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 }) });
1687
1727
  }
1688
1728
  function GenreInfoPanelBody({
1689
1729
  toolbar,
@@ -1700,9 +1740,9 @@ function GenreInfoPanelBody({
1700
1740
  }) {
1701
1741
  const summary = scope === "reference" ? import_genre_tree19.GenreCatalogueCountsSchema.parse(genrePlaylist ?? catalogue) : void 0;
1702
1742
  const selection = summary ? (0, import_genre_tree19.defaultGenreMusicSelection)(summary) : void 0;
1703
- 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: [
1704
1744
  toolbar,
1705
- 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 }),
1706
1746
  /* @__PURE__ */ (0, import_react14.createElement)(
1707
1747
  GenreDetailExtras,
1708
1748
  {
@@ -1722,7 +1762,7 @@ function GenreInfoPanelBody({
1722
1762
  }
1723
1763
 
1724
1764
  // src/genre-tree/GenreDetailPanel.tsx
1725
- var import_jsx_runtime30 = require("react/jsx-runtime");
1765
+ var import_jsx_runtime31 = require("react/jsx-runtime");
1726
1766
  var RELATIVE_ITEM_COUNT = 1;
1727
1767
  function toNode(genre, parentId) {
1728
1768
  return {
@@ -1745,27 +1785,21 @@ function useGenreDetailContent({
1745
1785
  trailing,
1746
1786
  genrePlaylist: hostPlaylist,
1747
1787
  expanded,
1748
- treeRelations
1788
+ treeStyles
1749
1789
  }) {
1750
- 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;
1751
1792
  const defaultPlayback = useGenrePlaylistPlayback({ scope, genrePlaylists: [], artist: extras?.artist });
1752
1793
  const playback = hostPlayback ?? defaultPlayback;
1753
1794
  const view = (0, import_react15.useMemo)(() => {
1754
1795
  if (!detail) return null;
1755
1796
  const rootColor = (0, import_genre_tree_view4.getGenreTreeColor)(detail.root.wikidataId);
1756
- const treeStyles = /* @__PURE__ */ new Map();
1757
- if (treeRelations) {
1758
- const drawn = [treeRelations, treeRelations.parentNode, ...treeRelations.childNodes, ...treeRelations.ancestorNodes];
1759
- for (const entry of drawn) {
1760
- if (entry) treeStyles.set(entry.node.id, { fill: entry.fill, textColor: entry.textColor });
1761
- }
1762
- }
1763
1797
  const relative = (genre, parentId) => ({
1764
1798
  node: toNode(genre, parentId),
1765
- ...treeStyles.get(genre.uuid) ?? { fill: (0, import_genre_tree_view4.tintSurface)(rootColor), textColor: import_genre_tree_view4.TEXT_COLOR }
1799
+ ...treeStyles?.get(genre.uuid) ?? { fill: (0, import_genre_tree_view4.tintSurface)(rootColor), textColor: import_genre_tree_view4.TEXT_COLOR }
1766
1800
  });
1767
1801
  const ancestors2 = [...detail.ascendants].sort((a, b) => b.degree - a.degree);
1768
- const own = treeStyles.get(detail.uuid) ?? { fill: rootColor, textColor: (0, import_genre_tree_view4.getTextColorOnFill)(rootColor) };
1802
+ const own = treeStyles?.get(detail.uuid) ?? { fill: rootColor, textColor: (0, import_genre_tree_view4.getTextColorOnFill)(rootColor) };
1769
1803
  return {
1770
1804
  node: {
1771
1805
  id: detail.uuid,
@@ -1782,23 +1816,28 @@ function useGenreDetailContent({
1782
1816
  ({ ascendant }, index) => relative(ascendant, index === 0 ? null : ancestors2[index - 1].ascendant.uuid)
1783
1817
  )
1784
1818
  };
1785
- }, [detail, scope, treeRelations]);
1786
- if (isError) return {
1819
+ }, [detail, scope, treeStyles]);
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 {
1787
1826
  name: null,
1788
1827
  title: "Genre unavailable",
1789
- 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." })
1790
1829
  };
1791
- if (isPending || !detail || !view) return {
1830
+ if (!detail || !view) return {
1792
1831
  name: null,
1793
1832
  title: "Loading genre\u2026",
1794
- content: /* @__PURE__ */ (0, import_jsx_runtime30.jsxs)("div", { className: "flex flex-col gap-3", "aria-busy": "true", "data-testid": "genre-detail-panel-skeleton", children: [
1795
- /* @__PURE__ */ (0, import_jsx_runtime30.jsx)(import_ui10.Skeleton, { className: "h-6 w-2/3" }),
1796
- /* @__PURE__ */ (0, import_jsx_runtime30.jsx)(import_ui10.Skeleton, { className: "h-4 w-full" }),
1797
- /* @__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" })
1798
1837
  ] })
1799
1838
  };
1800
1839
  const selectGenre = (nodeId) => onSelectGenre(import_genre_tree20.GenreUuidSchema.parse(nodeId));
1801
- const family = /* @__PURE__ */ (0, import_jsx_runtime30.jsx)(
1840
+ const family = /* @__PURE__ */ (0, import_jsx_runtime31.jsx)(
1802
1841
  import_genre_tree_view4.GenreNodeFamily,
1803
1842
  {
1804
1843
  node: view.node,
@@ -1809,17 +1848,17 @@ function useGenreDetailContent({
1809
1848
  onSelectNode: selectGenre
1810
1849
  }
1811
1850
  );
1812
- 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 });
1813
1852
  return {
1814
1853
  name: detail.name,
1815
1854
  title: detail.name,
1816
1855
  headerAccent: { background: view.fill, color: view.textColor },
1817
1856
  aside: expanded ? ancestors : void 0,
1818
- content: /* @__PURE__ */ (0, import_jsx_runtime30.jsxs)(import_jsx_runtime30.Fragment, { children: [
1819
- /* @__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)(
1820
1859
  GenreInfoPanelBody,
1821
1860
  {
1822
- 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: [
1823
1862
  family,
1824
1863
  !expanded && ancestors
1825
1864
  ] }),
@@ -1863,7 +1902,7 @@ function GenreDetailPanel({
1863
1902
  expanded,
1864
1903
  band: { onZoomIn, shareUrl, shareTarget: shareUrl ? shareTarget : void 0 }
1865
1904
  });
1866
- return /* @__PURE__ */ (0, import_jsx_runtime30.jsx)(
1905
+ return /* @__PURE__ */ (0, import_jsx_runtime31.jsx)(
1867
1906
  import_ui10.DetailPanel,
1868
1907
  {
1869
1908
  side: "left",
@@ -1876,7 +1915,7 @@ function GenreDetailPanel({
1876
1915
  edgeTab,
1877
1916
  aside: panel.aside,
1878
1917
  titleInSearch,
1879
- 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,
1880
1919
  children: panel.content
1881
1920
  }
1882
1921
  );
@@ -1885,7 +1924,7 @@ function GenreDetailPanel({
1885
1924
  // src/genre-tree/ChangeParentPopup.tsx
1886
1925
  var import_react16 = require("react");
1887
1926
  var import_genre_tree21 = require("@behindthemusictree/app-kit/genre-tree");
1888
- var import_jsx_runtime31 = require("react/jsx-runtime");
1927
+ var import_jsx_runtime32 = require("react/jsx-runtime");
1889
1928
  function ChangeParentPopup({
1890
1929
  genrePlaylist,
1891
1930
  genrePlaylists,
@@ -1920,12 +1959,12 @@ function ChangeParentPopup({
1920
1959
  window.addEventListener("keydown", handleKeyDown);
1921
1960
  return () => window.removeEventListener("keydown", handleKeyDown);
1922
1961
  }, [onClose]);
1923
- return /* @__PURE__ */ (0, import_jsx_runtime31.jsxs)(BasePopup_default, { title: `Change parent of ${genrePlaylist.name}`, onClose, showCancelButton: true, fitContent: true, children: [
1924
- /* @__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: [
1925
1964
  "Current parent: ",
1926
1965
  genrePlaylist.parent?.name ?? "none"
1927
1966
  ] }),
1928
- /* @__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)(
1929
1968
  GenreSearch,
1930
1969
  {
1931
1970
  genrePlaylists: candidates,
@@ -1933,7 +1972,7 @@ function ChangeParentPopup({
1933
1972
  placeholder: "Search a new parent\u2026"
1934
1973
  }
1935
1974
  ) }),
1936
- 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(" ") })
1937
1976
  ] });
1938
1977
  }
1939
1978
 
@@ -1946,7 +1985,7 @@ var import_lucide_react10 = require("lucide-react");
1946
1985
  var import_ui11 = require("@behindthemusictree/ui");
1947
1986
  var import_genre_tree22 = require("@behindthemusictree/app-kit/genre-tree");
1948
1987
  var import_player7 = require("@behindthemusictree/app-kit/player");
1949
- var import_jsx_runtime32 = require("react/jsx-runtime");
1988
+ var import_jsx_runtime33 = require("react/jsx-runtime");
1950
1989
  function ArtistActionBand({ artist, share }) {
1951
1990
  const { trackList, playNewTrackListFromArtist } = (0, import_genre_tree22.useTrackList)();
1952
1991
  const { isPlaying, setIsPlaying } = (0, import_player7.usePlayer)();
@@ -1962,11 +2001,11 @@ function ArtistActionBand({ artist, share }) {
1962
2001
  console.error("Failed to load artist tracks:", error);
1963
2002
  });
1964
2003
  };
1965
- return /* @__PURE__ */ (0, import_jsx_runtime32.jsx)(import_ui11.PanelBleed, { children: /* @__PURE__ */ (0, import_jsx_runtime32.jsxs)(import_ui11.ActionBand, { children: [
1966
- /* @__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)(
1967
2006
  import_ui11.ActionButton,
1968
2007
  {
1969
- 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" }),
1970
2009
  label: playing ? "Pause" : "Play",
1971
2010
  caption: String(artist.playableTracksCount),
1972
2011
  disabled: artist.playableTracksCount === 0,
@@ -1985,7 +2024,7 @@ var import_ui13 = require("@behindthemusictree/ui");
1985
2024
  var import_lucide_react11 = require("lucide-react");
1986
2025
  var import_si3 = require("react-icons/si");
1987
2026
  var import_ui12 = require("@behindthemusictree/ui");
1988
- var import_jsx_runtime33 = require("react/jsx-runtime");
2027
+ var import_jsx_runtime34 = require("react/jsx-runtime");
1989
2028
  var LINK_KINDS = {
1990
2029
  official: { label: "Official website", Icon: import_lucide_react11.Globe },
1991
2030
  wikipedia: { label: "Wikipedia", Icon: import_si3.SiWikipedia },
@@ -2022,7 +2061,7 @@ function artistLinks(artist) {
2022
2061
  return links;
2023
2062
  }
2024
2063
  function ArtistLinks({ links }) {
2025
- 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)) });
2026
2065
  }
2027
2066
 
2028
2067
  // src/genre-tree/artist/formatYears.ts
@@ -2033,7 +2072,7 @@ function formatYears(beginYear, endYear, ended) {
2033
2072
  }
2034
2073
 
2035
2074
  // src/genre-tree/artist/ArtistHeader.tsx
2036
- var import_jsx_runtime34 = require("react/jsx-runtime");
2075
+ var import_jsx_runtime35 = require("react/jsx-runtime");
2037
2076
  var countryNames = new Intl.DisplayNames(["en"], { type: "region" });
2038
2077
  function ArtistHeader({ artist }) {
2039
2078
  const fields = [];
@@ -2043,35 +2082,23 @@ function ArtistHeader({ artist }) {
2043
2082
  const years = formatYears(artist.beginYear, artist.endYear, artist.ended);
2044
2083
  if (years) fields.push({ label: "Active", value: years });
2045
2084
  const links = artistLinks(artist);
2046
- if (links.length > 0) fields.push({ label: "Links", value: /* @__PURE__ */ (0, import_jsx_runtime34.jsx)(ArtistLinks, { links }) });
2085
+ if (links.length > 0) fields.push({ label: "Links", value: /* @__PURE__ */ (0, import_jsx_runtime35.jsx)(ArtistLinks, { links }) });
2047
2086
  if (fields.length === 0) return null;
2048
- return /* @__PURE__ */ (0, import_jsx_runtime34.jsx)(import_ui13.FieldList, { items: fields });
2087
+ return /* @__PURE__ */ (0, import_jsx_runtime35.jsx)(import_ui13.FieldList, { items: fields });
2049
2088
  }
2050
2089
 
2051
2090
  // src/genre-tree/artist/ArtistPhoto.tsx
2052
- var import_jsx_runtime35 = require("react/jsx-runtime");
2053
- function ExternalLink({ href, children }) {
2054
- return /* @__PURE__ */ (0, import_jsx_runtime35.jsx)("a", { href, target: "_blank", rel: "noopener noreferrer", className: "underline-offset-2 hover:underline", children });
2055
- }
2091
+ var import_jsx_runtime36 = require("react/jsx-runtime");
2056
2092
  function ArtistPhoto({ image, name }) {
2057
- return /* @__PURE__ */ (0, import_jsx_runtime35.jsxs)("figure", { className: "flex flex-col gap-1.5", children: [
2058
- /* @__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" }),
2059
- /* @__PURE__ */ (0, import_jsx_runtime35.jsxs)("figcaption", { className: "text-caption text-[var(--color-text-muted)]", children: [
2060
- "Photo: ",
2061
- /* @__PURE__ */ (0, import_jsx_runtime35.jsx)(ExternalLink, { href: image.pageUrl, children: image.author ?? "Wikimedia Commons" }),
2062
- ",",
2063
- " ",
2064
- image.licenseUrl ? /* @__PURE__ */ (0, import_jsx_runtime35.jsx)(ExternalLink, { href: image.licenseUrl, children: image.license }) : image.license
2065
- ] })
2066
- ] });
2093
+ return /* @__PURE__ */ (0, import_jsx_runtime36.jsx)(CommonsPhoto, { image, alt: name });
2067
2094
  }
2068
2095
 
2069
2096
  // src/genre-tree/artist/ArtistMembers.tsx
2070
2097
  var import_ui14 = require("@behindthemusictree/ui");
2071
- var import_jsx_runtime36 = require("react/jsx-runtime");
2098
+ var import_jsx_runtime37 = require("react/jsx-runtime");
2072
2099
  function MembershipList({ title, memberships, renderArtistLink }) {
2073
2100
  if (memberships.length === 0) return null;
2074
- return /* @__PURE__ */ (0, import_jsx_runtime36.jsx)(import_ui14.PanelGroup, { title, children: /* @__PURE__ */ (0, import_jsx_runtime36.jsx)("ul", { children: memberships.map((membership, index) => {
2101
+ return /* @__PURE__ */ (0, import_jsx_runtime37.jsx)(import_ui14.PanelGroup, { title, children: /* @__PURE__ */ (0, import_jsx_runtime37.jsx)("ul", { children: memberships.map((membership, index) => {
2075
2102
  const details = [
2076
2103
  formatYears(membership.beginYear, membership.endYear, membership.ended),
2077
2104
  ...membership.roles
@@ -2079,9 +2106,9 @@ function MembershipList({ title, memberships, renderArtistLink }) {
2079
2106
  const detailsText = details.filter(Boolean).join(" \xB7 ");
2080
2107
  return (
2081
2108
  // A member can have several periods in the same group.
2082
- /* @__PURE__ */ (0, import_jsx_runtime36.jsxs)("li", { className: "truncate py-0.5", title: detailsText || void 0, children: [
2083
- /* @__PURE__ */ (0, import_jsx_runtime36.jsx)("span", { className: "font-medium text-[var(--color-text)]", children: renderArtistLink(membership.artist) }),
2084
- detailsText && /* @__PURE__ */ (0, import_jsx_runtime36.jsxs)("span", { className: "text-[var(--color-text-muted)]", children: [
2109
+ /* @__PURE__ */ (0, import_jsx_runtime37.jsxs)("li", { className: "truncate py-0.5", title: detailsText || void 0, children: [
2110
+ /* @__PURE__ */ (0, import_jsx_runtime37.jsx)("span", { className: "font-medium text-[var(--color-text)]", children: renderArtistLink(membership.artist) }),
2111
+ detailsText && /* @__PURE__ */ (0, import_jsx_runtime37.jsxs)("span", { className: "text-[var(--color-text-muted)]", children: [
2085
2112
  " \u2014 ",
2086
2113
  detailsText
2087
2114
  ] })
@@ -2092,9 +2119,9 @@ function MembershipList({ title, memberships, renderArtistLink }) {
2092
2119
  function ArtistMembers({ title, memberships, renderArtistLink }) {
2093
2120
  if (memberships.length === 0) return null;
2094
2121
  const isCurrent = (membership) => !membership.ended && membership.endYear === null;
2095
- return /* @__PURE__ */ (0, import_jsx_runtime36.jsxs)(import_ui14.PanelSection, { title, children: [
2096
- /* @__PURE__ */ (0, import_jsx_runtime36.jsx)(MembershipList, { title: "Current", memberships: memberships.filter(isCurrent), renderArtistLink }),
2097
- /* @__PURE__ */ (0, import_jsx_runtime36.jsx)(
2122
+ return /* @__PURE__ */ (0, import_jsx_runtime37.jsxs)(import_ui14.PanelSection, { title, children: [
2123
+ /* @__PURE__ */ (0, import_jsx_runtime37.jsx)(MembershipList, { title: "Current", memberships: memberships.filter(isCurrent), renderArtistLink }),
2124
+ /* @__PURE__ */ (0, import_jsx_runtime37.jsx)(
2098
2125
  MembershipList,
2099
2126
  {
2100
2127
  title: "Past",
@@ -2107,7 +2134,7 @@ function ArtistMembers({ title, memberships, renderArtistLink }) {
2107
2134
 
2108
2135
  // src/genre-tree/artist/ArtistDiscography.tsx
2109
2136
  var import_ui15 = require("@behindthemusictree/ui");
2110
- var import_jsx_runtime37 = require("react/jsx-runtime");
2137
+ var import_jsx_runtime38 = require("react/jsx-runtime");
2111
2138
  var GROUPS = ["Albums", "EPs", "Singles", "Live", "Compilations", "Other"];
2112
2139
  var PRIMARY_GROUPS = { album: "Albums", ep: "EPs", single: "Singles" };
2113
2140
  function discographyGroup({ primaryType, secondaryTypes }) {
@@ -2127,12 +2154,12 @@ function byYear(a, b) {
2127
2154
  function ArtistDiscography({ albums }) {
2128
2155
  if (albums.length === 0) return null;
2129
2156
  const sorted = [...albums].sort(byYear);
2130
- return /* @__PURE__ */ (0, import_jsx_runtime37.jsx)(import_ui15.PanelSection, { title: "Discography", children: GROUPS.map((group) => {
2157
+ return /* @__PURE__ */ (0, import_jsx_runtime38.jsx)(import_ui15.PanelSection, { title: "Discography", children: GROUPS.map((group) => {
2131
2158
  const groupAlbums = sorted.filter((album) => discographyGroup(album) === group);
2132
2159
  if (groupAlbums.length === 0) return null;
2133
- 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: [
2134
- /* @__PURE__ */ (0, import_jsx_runtime37.jsx)("span", { className: "tabular-nums text-[var(--color-text-muted)]", children: album.year ?? "\u2014" }),
2135
- /* @__PURE__ */ (0, import_jsx_runtime37.jsx)("span", { className: "truncate text-[var(--color-text)]", title: album.name, children: album.name })
2160
+ 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: [
2161
+ /* @__PURE__ */ (0, import_jsx_runtime38.jsx)("span", { className: "tabular-nums text-[var(--color-text-muted)]", children: album.year ?? "\u2014" }),
2162
+ /* @__PURE__ */ (0, import_jsx_runtime38.jsx)("span", { className: "truncate text-[var(--color-text)]", title: album.name, children: album.name })
2136
2163
  ] }, album.uuid)) }) }, group);
2137
2164
  }) });
2138
2165
  }
@@ -2140,7 +2167,6 @@ function ArtistDiscography({ albums }) {
2140
2167
  // src/genre-tree/artist/ArtistSongs.tsx
2141
2168
  var import_react17 = require("react");
2142
2169
  var import_react_query2 = require("@tanstack/react-query");
2143
- var import_lucide_react12 = require("lucide-react");
2144
2170
  var import_ui16 = require("@behindthemusictree/ui");
2145
2171
  var import_player11 = require("@behindthemusictree/app-kit/player");
2146
2172
  var import_genre_tree26 = require("@behindthemusictree/app-kit/genre-tree");
@@ -2154,7 +2180,7 @@ var import_genre_tree23 = require("@behindthemusictree/app-kit/genre-tree");
2154
2180
  var import_fa3 = require("react-icons/fa");
2155
2181
  var import_player8 = require("@behindthemusictree/app-kit/player");
2156
2182
  var import_player9 = require("@behindthemusictree/app-kit/player");
2157
- var import_jsx_runtime38 = require("react/jsx-runtime");
2183
+ var import_jsx_runtime39 = require("react/jsx-runtime");
2158
2184
  function TrackPositionPlayPause({
2159
2185
  position,
2160
2186
  uuid,
@@ -2168,14 +2194,14 @@ function TrackPositionPlayPause({
2168
2194
  const isPlaying = isThisTrack && isConfirmedPlayable && playState === import_player9.PlayStates.PLAYING;
2169
2195
  const isPaused = isThisTrack && isConfirmedPlayable && playState === import_player9.PlayStates.PAUSED;
2170
2196
  const isCurrentTrackActive = isPlaying || isPaused;
2171
- return /* @__PURE__ */ (0, import_jsx_runtime38.jsxs)(
2197
+ return /* @__PURE__ */ (0, import_jsx_runtime39.jsxs)(
2172
2198
  "div",
2173
2199
  {
2174
2200
  className: "track-position-play-pause relative flex items-center justify-center gap-2 text-lg w-16 cursor-pointer",
2175
2201
  style: { minWidth: "4rem", maxWidth: "4rem" },
2176
2202
  onClick: handlePlayPauseClick,
2177
2203
  children: [
2178
- /* @__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)(
2204
+ /* @__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)(
2179
2205
  "span",
2180
2206
  {
2181
2207
  className: "track-position-play-pause__bars",
@@ -2183,18 +2209,18 @@ function TrackPositionPlayPause({
2183
2209
  role: "img",
2184
2210
  "aria-label": isPlaying ? "Playing" : "Paused",
2185
2211
  children: [
2186
- /* @__PURE__ */ (0, import_jsx_runtime38.jsx)("span", { className: "track-position-play-pause__bar" }),
2187
- /* @__PURE__ */ (0, import_jsx_runtime38.jsx)("span", { className: "track-position-play-pause__bar" }),
2188
- /* @__PURE__ */ (0, import_jsx_runtime38.jsx)("span", { className: "track-position-play-pause__bar" })
2212
+ /* @__PURE__ */ (0, import_jsx_runtime39.jsx)("span", { className: "track-position-play-pause__bar" }),
2213
+ /* @__PURE__ */ (0, import_jsx_runtime39.jsx)("span", { className: "track-position-play-pause__bar" }),
2214
+ /* @__PURE__ */ (0, import_jsx_runtime39.jsx)("span", { className: "track-position-play-pause__bar" })
2189
2215
  ]
2190
2216
  }
2191
2217
  ) : position }),
2192
- /* @__PURE__ */ (0, import_jsx_runtime38.jsx)(
2218
+ /* @__PURE__ */ (0, import_jsx_runtime39.jsx)(
2193
2219
  "div",
2194
2220
  {
2195
2221
  className: `hidden group-hover:flex items-center justify-center ${isCurrentTrackActive ? "" : "absolute inset-0"}`,
2196
2222
  "aria-hidden": "true",
2197
- 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, {})
2223
+ 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, {})
2198
2224
  }
2199
2225
  )
2200
2226
  ]
@@ -2205,7 +2231,7 @@ function TrackPositionPlayPause({
2205
2231
  // src/genre-tree/track-list-sidebar/TrackItem.tsx
2206
2232
  var import_genre_tree24 = require("@behindthemusictree/app-kit/genre-tree");
2207
2233
  var import_genre_tree25 = require("@behindthemusictree/app-kit/genre-tree");
2208
- var import_jsx_runtime39 = require("react/jsx-runtime");
2234
+ var import_jsx_runtime40 = require("react/jsx-runtime");
2209
2235
  function TrackItem({
2210
2236
  track,
2211
2237
  position,
@@ -2229,12 +2255,12 @@ function TrackItem({
2229
2255
  loadTrackForPlayer(track.uuid);
2230
2256
  }
2231
2257
  };
2232
- return /* @__PURE__ */ (0, import_jsx_runtime39.jsxs)(
2258
+ return /* @__PURE__ */ (0, import_jsx_runtime40.jsxs)(
2233
2259
  "div",
2234
2260
  {
2235
2261
  className: `track-item flex h-14 text-[var(--color-text)] ${isUnplayable ? "opacity-50" : "hover:bg-[var(--color-surface-muted)] group"}`,
2236
2262
  children: [
2237
- isUnplayable ? /* @__PURE__ */ (0, import_jsx_runtime39.jsx)(
2263
+ isUnplayable ? /* @__PURE__ */ (0, import_jsx_runtime40.jsx)(
2238
2264
  "div",
2239
2265
  {
2240
2266
  className: "flex items-center justify-center text-lg w-16",
@@ -2242,7 +2268,7 @@ function TrackItem({
2242
2268
  "aria-disabled": "true",
2243
2269
  children: position
2244
2270
  }
2245
- ) : /* @__PURE__ */ (0, import_jsx_runtime39.jsx)(
2271
+ ) : /* @__PURE__ */ (0, import_jsx_runtime40.jsx)(
2246
2272
  TrackPositionPlayPause,
2247
2273
  {
2248
2274
  position,
@@ -2251,12 +2277,12 @@ function TrackItem({
2251
2277
  isSelected: selectedTrack?.uuid === track.uuid
2252
2278
  }
2253
2279
  ),
2254
- /* @__PURE__ */ (0, import_jsx_runtime39.jsxs)("div", { className: "title-artist-container flex flex-col items-start justify-center min-w-0 flex-1", children: [
2255
- /* @__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 }) }),
2256
- 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 }) }) : ""
2280
+ /* @__PURE__ */ (0, import_jsx_runtime40.jsxs)("div", { className: "title-artist-container flex flex-col items-start justify-center min-w-0 flex-1", children: [
2281
+ /* @__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 }) }),
2282
+ 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 }) }) : ""
2257
2283
  ] }),
2258
- /* @__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 }) : "" }),
2259
- renderDuration && /* @__PURE__ */ (0, import_jsx_runtime39.jsx)(
2284
+ /* @__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 }) : "" }),
2285
+ renderDuration && /* @__PURE__ */ (0, import_jsx_runtime40.jsx)(
2260
2286
  "div",
2261
2287
  {
2262
2288
  className: "duration flex text-base w-16 items-center justify-center",
@@ -2264,14 +2290,14 @@ function TrackItem({
2264
2290
  children: renderDuration(track)
2265
2291
  }
2266
2292
  ),
2267
- 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) })
2293
+ 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) })
2268
2294
  ]
2269
2295
  }
2270
2296
  );
2271
2297
  }
2272
2298
 
2273
2299
  // src/genre-tree/artist/ArtistSongs.tsx
2274
- var import_jsx_runtime40 = require("react/jsx-runtime");
2300
+ var import_jsx_runtime41 = require("react/jsx-runtime");
2275
2301
  var SongPageSchema2 = (0, import_transport6.PaginatedResponseSchema)(import_genre_tree26.SongDetailedSchema);
2276
2302
  var SORTS = [
2277
2303
  { ordering: "title", label: "Title" },
@@ -2304,36 +2330,22 @@ function ArtistSongs({ artist, getBackendBaseUrl, renderArtistLink, renderSongLi
2304
2330
  event.stopPropagation();
2305
2331
  void play(song.uuid);
2306
2332
  };
2307
- 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: [
2308
- /* @__PURE__ */ (0, import_jsx_runtime40.jsxs)("div", { className: "flex flex-wrap items-center justify-between gap-2 mb-2", children: [
2309
- /* @__PURE__ */ (0, import_jsx_runtime40.jsxs)(
2310
- "button",
2311
- {
2312
- type: "button",
2313
- onClick: () => void play(),
2314
- className: "flex w-fit items-center gap-1.5 px-3 py-1 text-caption font-medium bg-gray-900 text-white rounded-full hover:bg-gray-700",
2315
- children: [
2316
- /* @__PURE__ */ (0, import_jsx_runtime40.jsx)(import_lucide_react12.Play, { className: "h-3.5 w-3.5", "aria-hidden": "true" }),
2317
- "Play all"
2318
- ]
2319
- }
2320
- ),
2321
- /* @__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)(
2322
- "button",
2323
- {
2324
- type: "button",
2325
- "aria-pressed": ordering === sort.ordering,
2326
- onClick: () => {
2327
- setOrdering(sort.ordering);
2328
- setPage(1);
2329
- },
2330
- className: "px-2.5 py-0.5 text-[var(--color-text)] rounded-full hover:bg-[var(--color-surface-muted)] aria-pressed:bg-gray-900 aria-pressed:text-white",
2331
- children: sort.label
2333
+ 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: [
2334
+ /* @__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)(
2335
+ "button",
2336
+ {
2337
+ type: "button",
2338
+ "aria-pressed": ordering === sort.ordering,
2339
+ onClick: () => {
2340
+ setOrdering(sort.ordering);
2341
+ setPage(1);
2332
2342
  },
2333
- sort.ordering
2334
- )) })
2335
- ] }),
2336
- /* @__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)(
2343
+ className: "px-2.5 py-0.5 text-[var(--color-text)] rounded-full hover:bg-[var(--color-surface-muted)] aria-pressed:bg-gray-900 aria-pressed:text-white",
2344
+ children: sort.label
2345
+ },
2346
+ sort.ordering
2347
+ )) }) }),
2348
+ /* @__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)(
2337
2349
  TrackItem,
2338
2350
  {
2339
2351
  track: song,
@@ -2342,12 +2354,12 @@ function ArtistSongs({ artist, getBackendBaseUrl, renderArtistLink, renderSongLi
2342
2354
  renderSongLink
2343
2355
  }
2344
2356
  ) }, song.uuid)) }),
2345
- /* @__PURE__ */ (0, import_jsx_runtime40.jsx)(import_ui16.Pagination, { currentPage: page, totalPages: data.totalPages, onPageChange: setPage })
2357
+ /* @__PURE__ */ (0, import_jsx_runtime41.jsx)(import_ui16.Pagination, { currentPage: page, totalPages: data.totalPages, onPageChange: setPage })
2346
2358
  ] }) });
2347
2359
  }
2348
2360
 
2349
2361
  // src/genre-tree/artist/useArtistPanel.tsx
2350
- var import_jsx_runtime41 = require("react/jsx-runtime");
2362
+ var import_jsx_runtime42 = require("react/jsx-runtime");
2351
2363
  function useArtistPanel({
2352
2364
  uuid,
2353
2365
  scope,
@@ -2368,26 +2380,26 @@ function useArtistPanel({
2368
2380
  return {
2369
2381
  ...panel,
2370
2382
  title: "Artist",
2371
- content: /* @__PURE__ */ (0, import_jsx_runtime41.jsxs)("div", { "aria-busy": "true", className: "flex flex-col gap-2.5", children: [
2372
- /* @__PURE__ */ (0, import_jsx_runtime41.jsx)(import_ui17.Skeleton, { className: "h-3 w-16" }),
2373
- /* @__PURE__ */ (0, import_jsx_runtime41.jsx)(import_ui17.Skeleton, { className: "h-4 w-32" }),
2374
- /* @__PURE__ */ (0, import_jsx_runtime41.jsx)(import_ui17.Skeleton, { className: "h-3 w-16" }),
2375
- /* @__PURE__ */ (0, import_jsx_runtime41.jsx)(import_ui17.Skeleton, { className: "h-4 w-24" }),
2376
- /* @__PURE__ */ (0, import_jsx_runtime41.jsx)(import_ui17.Skeleton, { className: "h-16 mt-2.5" }),
2377
- /* @__PURE__ */ (0, import_jsx_runtime41.jsx)(import_ui17.Skeleton, { className: "h-16" })
2383
+ content: /* @__PURE__ */ (0, import_jsx_runtime42.jsxs)("div", { "aria-busy": "true", className: "flex flex-col gap-2.5", children: [
2384
+ /* @__PURE__ */ (0, import_jsx_runtime42.jsx)(import_ui17.Skeleton, { className: "h-3 w-16" }),
2385
+ /* @__PURE__ */ (0, import_jsx_runtime42.jsx)(import_ui17.Skeleton, { className: "h-4 w-32" }),
2386
+ /* @__PURE__ */ (0, import_jsx_runtime42.jsx)(import_ui17.Skeleton, { className: "h-3 w-16" }),
2387
+ /* @__PURE__ */ (0, import_jsx_runtime42.jsx)(import_ui17.Skeleton, { className: "h-4 w-24" }),
2388
+ /* @__PURE__ */ (0, import_jsx_runtime42.jsx)(import_ui17.Skeleton, { className: "h-16 mt-2.5" }),
2389
+ /* @__PURE__ */ (0, import_jsx_runtime42.jsx)(import_ui17.Skeleton, { className: "h-16" })
2378
2390
  ] })
2379
2391
  };
2380
2392
  case "not-found":
2381
2393
  return {
2382
2394
  ...panel,
2383
2395
  title: "Artist not found",
2384
- content: /* @__PURE__ */ (0, import_jsx_runtime41.jsx)("p", { className: "text-[var(--color-text-muted)]", children: "This artist does not exist." })
2396
+ content: /* @__PURE__ */ (0, import_jsx_runtime42.jsx)("p", { className: "text-[var(--color-text-muted)]", children: "This artist does not exist." })
2385
2397
  };
2386
2398
  case "error":
2387
2399
  return {
2388
2400
  ...panel,
2389
2401
  title: "Artist",
2390
- content: /* @__PURE__ */ (0, import_jsx_runtime41.jsx)("p", { className: "text-red-600", children: "Could not load this artist." })
2402
+ content: /* @__PURE__ */ (0, import_jsx_runtime42.jsx)("p", { className: "text-red-600", children: "Could not load this artist." })
2391
2403
  };
2392
2404
  case "success": {
2393
2405
  const { artist } = result;
@@ -2396,19 +2408,19 @@ function useArtistPanel({
2396
2408
  return {
2397
2409
  ...panel,
2398
2410
  title: artist.name,
2399
- content: /* @__PURE__ */ (0, import_jsx_runtime41.jsxs)(import_jsx_runtime41.Fragment, { children: [
2400
- /* @__PURE__ */ (0, import_jsx_runtime41.jsx)(
2411
+ content: /* @__PURE__ */ (0, import_jsx_runtime42.jsxs)(import_jsx_runtime42.Fragment, { children: [
2412
+ /* @__PURE__ */ (0, import_jsx_runtime42.jsx)(
2401
2413
  ArtistActionBand,
2402
2414
  {
2403
2415
  artist,
2404
- share: shareTarget && shareUrl ? /* @__PURE__ */ (0, import_jsx_runtime41.jsx)(ShareActionButton, { target: shareTarget, url: shareUrl }) : void 0
2416
+ share: shareTarget && shareUrl ? /* @__PURE__ */ (0, import_jsx_runtime42.jsx)(ShareActionButton, { target: shareTarget, url: shareUrl }) : void 0
2405
2417
  }
2406
2418
  ),
2407
- artist.image && /* @__PURE__ */ (0, import_jsx_runtime41.jsx)(ArtistPhoto, { image: artist.image, name: artist.name }),
2408
- /* @__PURE__ */ (0, import_jsx_runtime41.jsx)(ArtistHeader, { artist }),
2409
- /* @__PURE__ */ (0, import_jsx_runtime41.jsx)(ArtistMembers, { title: "Members", memberships: artist.members, renderArtistLink }),
2410
- /* @__PURE__ */ (0, import_jsx_runtime41.jsx)(ArtistMembers, { title: "Member of", memberships: artist.memberOf, renderArtistLink }),
2411
- artist.tracksCount > 0 && /* @__PURE__ */ (0, import_jsx_runtime41.jsx)(
2419
+ artist.image && /* @__PURE__ */ (0, import_jsx_runtime42.jsx)(ArtistPhoto, { image: artist.image, name: artist.name }),
2420
+ /* @__PURE__ */ (0, import_jsx_runtime42.jsx)(ArtistHeader, { artist }),
2421
+ /* @__PURE__ */ (0, import_jsx_runtime42.jsx)(ArtistMembers, { title: "Members", memberships: artist.members, renderArtistLink }),
2422
+ /* @__PURE__ */ (0, import_jsx_runtime42.jsx)(ArtistMembers, { title: "Member of", memberships: artist.memberOf, renderArtistLink }),
2423
+ artist.tracksCount > 0 && /* @__PURE__ */ (0, import_jsx_runtime42.jsx)(
2412
2424
  ArtistSongs,
2413
2425
  {
2414
2426
  artist: { uuid: artist.uuid, name: artist.name },
@@ -2418,7 +2430,7 @@ function useArtistPanel({
2418
2430
  }
2419
2431
  )
2420
2432
  ] }),
2421
- extraContent: artist.albums.length > 0 ? /* @__PURE__ */ (0, import_jsx_runtime41.jsx)(ArtistDiscography, { albums: artist.albums }) : void 0
2433
+ extraContent: artist.albums.length > 0 ? /* @__PURE__ */ (0, import_jsx_runtime42.jsx)(ArtistDiscography, { albums: artist.albums }) : void 0
2422
2434
  };
2423
2435
  }
2424
2436
  }
@@ -2430,11 +2442,11 @@ var import_genre_tree29 = require("@behindthemusictree/app-kit/genre-tree");
2430
2442
 
2431
2443
  // src/genre-tree/song/SongPanelContent.tsx
2432
2444
  var import_react18 = require("react");
2433
- var import_lucide_react13 = require("lucide-react");
2445
+ var import_lucide_react12 = require("lucide-react");
2434
2446
  var import_ui18 = require("@behindthemusictree/ui");
2435
2447
  var import_player12 = require("@behindthemusictree/app-kit/player");
2436
2448
  var import_genre_tree28 = require("@behindthemusictree/app-kit/genre-tree");
2437
- var import_jsx_runtime42 = require("react/jsx-runtime");
2449
+ var import_jsx_runtime43 = require("react/jsx-runtime");
2438
2450
  var numberFormat = new Intl.NumberFormat("en");
2439
2451
  var dateFormat = new Intl.DateTimeFormat("en", { dateStyle: "medium" });
2440
2452
  var externalLink = "text-[var(--color-primary)] hover:underline focus-visible:underline";
@@ -2442,27 +2454,27 @@ function SongHistory({ uuid, getBackendBaseUrl }) {
2442
2454
  const [open, setOpen] = (0, import_react18.useState)(false);
2443
2455
  const history = (0, import_genre_tree28.useSongHistory)(uuid, getBackendBaseUrl, open);
2444
2456
  let body = null;
2445
- if (history.status === "loading") body = /* @__PURE__ */ (0, import_jsx_runtime42.jsx)(import_ui18.Skeleton, { className: "h-10", "aria-busy": "true" });
2446
- else if (history.status === "error") body = /* @__PURE__ */ (0, import_jsx_runtime42.jsx)("p", { className: "text-red-600", children: "Could not load the history." });
2457
+ if (history.status === "loading") body = /* @__PURE__ */ (0, import_jsx_runtime43.jsx)(import_ui18.Skeleton, { className: "h-10", "aria-busy": "true" });
2458
+ else if (history.status === "error") body = /* @__PURE__ */ (0, import_jsx_runtime43.jsx)("p", { className: "text-red-600", children: "Could not load the history." });
2447
2459
  else if (history.status === "success")
2448
- 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: [
2449
- /* @__PURE__ */ (0, import_jsx_runtime42.jsx)("span", { className: "font-medium", children: entry.action }),
2450
- entry.oldValue !== null || entry.newValue !== null ? /* @__PURE__ */ (0, import_jsx_runtime42.jsxs)("span", { className: "text-[var(--color-text-muted)]", children: [
2460
+ 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: [
2461
+ /* @__PURE__ */ (0, import_jsx_runtime43.jsx)("span", { className: "font-medium", children: entry.action }),
2462
+ entry.oldValue !== null || entry.newValue !== null ? /* @__PURE__ */ (0, import_jsx_runtime43.jsxs)("span", { className: "text-[var(--color-text-muted)]", children: [
2451
2463
  " ",
2452
2464
  "\u2014 ",
2453
2465
  entry.oldValue ?? "none",
2454
2466
  " \u2192 ",
2455
2467
  entry.newValue ?? "none"
2456
2468
  ] }) : null,
2457
- /* @__PURE__ */ (0, import_jsx_runtime42.jsxs)("span", { className: "block text-caption text-[var(--color-text-muted)]", children: [
2469
+ /* @__PURE__ */ (0, import_jsx_runtime43.jsxs)("span", { className: "block text-caption text-[var(--color-text-muted)]", children: [
2458
2470
  entry.actorPseudo ?? "Anonymous",
2459
2471
  " \xB7 ",
2460
2472
  dateFormat.format(new Date(entry.createdOn))
2461
2473
  ] })
2462
2474
  ] }, entry.uuid)) });
2463
- 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: [
2464
- /* @__PURE__ */ (0, import_jsx_runtime42.jsx)("summary", { className: `mt-2.5 cursor-pointer ${import_ui18.PANEL_LABEL_CLASS}`, children: "History" }),
2465
- /* @__PURE__ */ (0, import_jsx_runtime42.jsx)("div", { className: "mt-1.5", children: body })
2475
+ 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: [
2476
+ /* @__PURE__ */ (0, import_jsx_runtime43.jsx)("summary", { className: `mt-2.5 cursor-pointer ${import_ui18.PANEL_LABEL_CLASS}`, children: "History" }),
2477
+ /* @__PURE__ */ (0, import_jsx_runtime43.jsx)("div", { className: "mt-1.5", children: body })
2466
2478
  ] }) });
2467
2479
  }
2468
2480
  function SongPanelContent(props) {
@@ -2481,13 +2493,13 @@ function SongPanelContent(props) {
2481
2493
  const genre = song.genre;
2482
2494
  fields.push({
2483
2495
  label: "Genre",
2484
- 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: [
2485
- index > 0 && /* @__PURE__ */ (0, import_jsx_runtime42.jsx)("span", { "aria-hidden": "true", children: "\u203A " }),
2486
- /* @__PURE__ */ (0, import_jsx_runtime42.jsx)("button", { type: "button", onClick: () => props.onSelectGenrePlaylist?.(genrePlaylist), className: "hover:underline", children: genrePlaylist.name })
2496
+ 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: [
2497
+ index > 0 && /* @__PURE__ */ (0, import_jsx_runtime43.jsx)("span", { "aria-hidden": "true", children: "\u203A " }),
2498
+ /* @__PURE__ */ (0, import_jsx_runtime43.jsx)("button", { type: "button", onClick: () => props.onSelectGenrePlaylist?.(genrePlaylist), className: "hover:underline", children: genrePlaylist.name })
2487
2499
  ] }, genrePlaylist.uuid)) }) : song.genre.name
2488
2500
  });
2489
2501
  } else {
2490
- fields.push({ label: "Genre", value: /* @__PURE__ */ (0, import_jsx_runtime42.jsx)("span", { className: "text-[var(--color-text-muted)]", children: "No genre" }) });
2502
+ fields.push({ label: "Genre", value: /* @__PURE__ */ (0, import_jsx_runtime43.jsx)("span", { className: "text-[var(--color-text-muted)]", children: "No genre" }) });
2491
2503
  }
2492
2504
  if (song.firstReleaseYear) fields.push({ label: "First released", value: song.firstReleaseYear });
2493
2505
  const viewCount = song.youtubeVideo?.viewCount;
@@ -2497,11 +2509,11 @@ function SongPanelContent(props) {
2497
2509
  const links = [];
2498
2510
  if (musicBrainzUrl)
2499
2511
  links.push(
2500
- /* @__PURE__ */ (0, import_jsx_runtime42.jsx)("a", { href: musicBrainzUrl, target: "_blank", rel: "noopener noreferrer", className: externalLink, children: "MusicBrainz" }, "mb")
2512
+ /* @__PURE__ */ (0, import_jsx_runtime43.jsx)("a", { href: musicBrainzUrl, target: "_blank", rel: "noopener noreferrer", className: externalLink, children: "MusicBrainz" }, "mb")
2501
2513
  );
2502
2514
  if (song.youtubeVideo)
2503
2515
  links.push(
2504
- /* @__PURE__ */ (0, import_jsx_runtime42.jsx)(
2516
+ /* @__PURE__ */ (0, import_jsx_runtime43.jsx)(
2505
2517
  "a",
2506
2518
  {
2507
2519
  href: `https://www.youtube.com/watch?v=${encodeURIComponent(song.youtubeVideo.id)}`,
@@ -2516,31 +2528,31 @@ function SongPanelContent(props) {
2516
2528
  if (links.length > 0)
2517
2529
  fields.push({
2518
2530
  label: "Links",
2519
- value: /* @__PURE__ */ (0, import_jsx_runtime42.jsx)("span", { className: "flex gap-3", children: links })
2531
+ value: /* @__PURE__ */ (0, import_jsx_runtime43.jsx)("span", { className: "flex gap-3", children: links })
2520
2532
  });
2521
- return /* @__PURE__ */ (0, import_jsx_runtime42.jsxs)(import_jsx_runtime42.Fragment, { children: [
2522
- /* @__PURE__ */ (0, import_jsx_runtime42.jsx)(import_ui18.PanelBleed, { children: /* @__PURE__ */ (0, import_jsx_runtime42.jsxs)(import_ui18.ActionBand, { children: [
2523
- /* @__PURE__ */ (0, import_jsx_runtime42.jsx)(
2533
+ return /* @__PURE__ */ (0, import_jsx_runtime43.jsxs)(import_jsx_runtime43.Fragment, { children: [
2534
+ /* @__PURE__ */ (0, import_jsx_runtime43.jsx)(import_ui18.PanelBleed, { children: /* @__PURE__ */ (0, import_jsx_runtime43.jsxs)(import_ui18.ActionBand, { children: [
2535
+ /* @__PURE__ */ (0, import_jsx_runtime43.jsx)(
2524
2536
  import_ui18.ActionButton,
2525
2537
  {
2526
- icon: isPlaying ? /* @__PURE__ */ (0, import_jsx_runtime42.jsx)(import_lucide_react13.Pause, { "aria-hidden": "true", className: "w-5 h-5" }) : /* @__PURE__ */ (0, import_jsx_runtime42.jsx)(import_lucide_react13.Play, { "aria-hidden": "true", className: "w-5 h-5" }),
2538
+ 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" }),
2527
2539
  label: isPlaying ? "Pause" : "Play",
2528
2540
  disabled: !!unplayableReason,
2529
2541
  title: unplayableReason ?? void 0,
2530
2542
  onClick: () => isCurrent ? handlePlayPauseAction() : playNewTrackListFromTrackUuid(song, scope)
2531
2543
  }
2532
2544
  ),
2533
- shareUrl && /* @__PURE__ */ (0, import_jsx_runtime42.jsx)(ShareActionButton, { target: shareTarget, url: shareUrl })
2545
+ shareUrl && /* @__PURE__ */ (0, import_jsx_runtime43.jsx)(ShareActionButton, { target: shareTarget, url: shareUrl })
2534
2546
  ] }) }),
2535
- 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 }) }),
2536
- /* @__PURE__ */ (0, import_jsx_runtime42.jsx)(import_ui18.FieldList, { items: fields }),
2537
- 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)) }) }),
2538
- /* @__PURE__ */ (0, import_jsx_runtime42.jsx)(SongHistory, { uuid: song.uuid, getBackendBaseUrl })
2547
+ 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 }) }),
2548
+ /* @__PURE__ */ (0, import_jsx_runtime43.jsx)(import_ui18.FieldList, { items: fields }),
2549
+ 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)) }) }),
2550
+ /* @__PURE__ */ (0, import_jsx_runtime43.jsx)(SongHistory, { uuid: song.uuid, getBackendBaseUrl })
2539
2551
  ] });
2540
2552
  }
2541
2553
 
2542
2554
  // src/genre-tree/song/useSongPanel.tsx
2543
- var import_jsx_runtime43 = require("react/jsx-runtime");
2555
+ var import_jsx_runtime44 = require("react/jsx-runtime");
2544
2556
  function useSongPanel({
2545
2557
  uuid,
2546
2558
  scope,
@@ -2561,24 +2573,24 @@ function useSongPanel({
2561
2573
  return {
2562
2574
  ...panel,
2563
2575
  title: "Song",
2564
- content: /* @__PURE__ */ (0, import_jsx_runtime43.jsxs)("div", { "aria-busy": "true", className: "flex flex-col gap-2.5", children: [
2565
- /* @__PURE__ */ (0, import_jsx_runtime43.jsx)(import_ui19.Skeleton, { className: "h-3 w-16" }),
2566
- /* @__PURE__ */ (0, import_jsx_runtime43.jsx)(import_ui19.Skeleton, { className: "h-4 w-32" }),
2567
- /* @__PURE__ */ (0, import_jsx_runtime43.jsx)(import_ui19.Skeleton, { className: "h-3 w-16" }),
2568
- /* @__PURE__ */ (0, import_jsx_runtime43.jsx)(import_ui19.Skeleton, { className: "h-4 w-24" })
2576
+ content: /* @__PURE__ */ (0, import_jsx_runtime44.jsxs)("div", { "aria-busy": "true", className: "flex flex-col gap-2.5", children: [
2577
+ /* @__PURE__ */ (0, import_jsx_runtime44.jsx)(import_ui19.Skeleton, { className: "h-3 w-16" }),
2578
+ /* @__PURE__ */ (0, import_jsx_runtime44.jsx)(import_ui19.Skeleton, { className: "h-4 w-32" }),
2579
+ /* @__PURE__ */ (0, import_jsx_runtime44.jsx)(import_ui19.Skeleton, { className: "h-3 w-16" }),
2580
+ /* @__PURE__ */ (0, import_jsx_runtime44.jsx)(import_ui19.Skeleton, { className: "h-4 w-24" })
2569
2581
  ] })
2570
2582
  };
2571
2583
  case "not-found":
2572
2584
  return {
2573
2585
  ...panel,
2574
2586
  title: "Song not found",
2575
- content: /* @__PURE__ */ (0, import_jsx_runtime43.jsx)("p", { className: "text-[var(--color-text-muted)]", children: "This song does not exist." })
2587
+ content: /* @__PURE__ */ (0, import_jsx_runtime44.jsx)("p", { className: "text-[var(--color-text-muted)]", children: "This song does not exist." })
2576
2588
  };
2577
2589
  case "error":
2578
2590
  return {
2579
2591
  ...panel,
2580
2592
  title: "Song",
2581
- content: /* @__PURE__ */ (0, import_jsx_runtime43.jsx)("p", { className: "text-red-600", children: "Could not load this song." })
2593
+ content: /* @__PURE__ */ (0, import_jsx_runtime44.jsx)("p", { className: "text-red-600", children: "Could not load this song." })
2582
2594
  };
2583
2595
  case "success": {
2584
2596
  const { song } = result;
@@ -2586,7 +2598,7 @@ function useSongPanel({
2586
2598
  return {
2587
2599
  ...panel,
2588
2600
  title: song.title,
2589
- content: /* @__PURE__ */ (0, import_jsx_runtime43.jsx)(
2601
+ content: /* @__PURE__ */ (0, import_jsx_runtime44.jsx)(
2590
2602
  SongPanelContent,
2591
2603
  {
2592
2604
  song,
@@ -2639,12 +2651,12 @@ function useEntityPanel({
2639
2651
  // src/genre-tree/SizeMetricToggle.tsx
2640
2652
  var import_md = require("react-icons/md");
2641
2653
  var import_ui20 = require("@behindthemusictree/ui");
2642
- var import_jsx_runtime44 = require("react/jsx-runtime");
2654
+ var import_jsx_runtime45 = require("react/jsx-runtime");
2643
2655
  function SizeMetricToggle({
2644
2656
  value,
2645
2657
  onChange
2646
2658
  }) {
2647
- return /* @__PURE__ */ (0, import_jsx_runtime44.jsx)(
2659
+ return /* @__PURE__ */ (0, import_jsx_runtime45.jsx)(
2648
2660
  import_ui20.SegmentedControl,
2649
2661
  {
2650
2662
  variant: "floating",
@@ -2652,15 +2664,15 @@ function SizeMetricToggle({
2652
2664
  value,
2653
2665
  onChange,
2654
2666
  options: [
2655
- { 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" },
2656
- { 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" }
2667
+ { 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" },
2668
+ { 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" }
2657
2669
  ]
2658
2670
  }
2659
2671
  );
2660
2672
  }
2661
2673
 
2662
2674
  // src/genre-tree/GenreTreeView.tsx
2663
- var import_jsx_runtime45 = require("react/jsx-runtime");
2675
+ var import_jsx_runtime46 = require("react/jsx-runtime");
2664
2676
  var NO_POP_ROOT_TITLE = "This genre tree has no 'Mainstream Pop' root yet";
2665
2677
  var REGIONS_CLASS = "[--gtv-center-left:var(--layout-center-left)] [--gtv-center-right:var(--layout-center-right)] [--gtv-center-top:var(--layout-band-top)]";
2666
2678
  var noop = () => {
@@ -2768,7 +2780,7 @@ function GenreTreeView({
2768
2780
  allowWheelRotation: internalAllowWheelRotation,
2769
2781
  toggleWheelRotation,
2770
2782
  onNodeHover: handleNodeHover
2771
- } = (0, import_genre_tree30.useGenreTreeViewState)({
2783
+ } = (0, import_genre_tree31.useGenreTreeViewState)({
2772
2784
  scope,
2773
2785
  getBackendBaseUrl,
2774
2786
  viewMode: controlledViewMode,
@@ -2787,12 +2799,12 @@ function GenreTreeView({
2787
2799
  restoreRootUuid = selectedGenrePlaylist.root.uuid;
2788
2800
  setStackedRestoreOwner({ revision: restoreRevision, rootUuid: restoreRootUuid });
2789
2801
  }
2790
- const isGenreUpdating = (0, import_react_query3.useIsMutating)({ mutationKey: import_genre_tree30.genreUpdateMutationKey }) > 0;
2791
- const { mutate: updateGenre } = (0, import_genre_tree30.useUpdateGenre)(scope, getBackendBaseUrl);
2802
+ const isGenreUpdating = (0, import_react_query3.useIsMutating)({ mutationKey: import_genre_tree31.genreUpdateMutationKey }) > 0;
2803
+ const { mutate: updateGenre } = (0, import_genre_tree31.useUpdateGenre)(scope, getBackendBaseUrl);
2792
2804
  const busy = deferredSizeMetric !== sizeMetric || isTreeFetching || isGenreUpdating;
2793
2805
  const isArtistFiltered = artistUuid !== null;
2794
2806
  const canSizeByViews = !isArtistFiltered && hasViewsCount(visibleGenrePlaylists);
2795
- const artistResult = (0, import_genre_tree30.useArtist)(artistUuid, artistSearchBaseUrl ?? getBackendBaseUrl);
2807
+ const artistResult = (0, import_genre_tree31.useArtist)(artistUuid, artistSearchBaseUrl ?? getBackendBaseUrl);
2796
2808
  const filterArtistName = artistResult.status === "success" ? artistResult.artist.name : null;
2797
2809
  const filterArtist = (0, import_react19.useMemo)(
2798
2810
  () => artistUuid !== null && filterArtistName !== null ? { uuid: artistUuid, name: filterArtistName } : null,
@@ -2812,14 +2824,14 @@ function GenreTreeView({
2812
2824
  const openSongRef = (0, import_react19.useRef)(openSong);
2813
2825
  openSongRef.current = openSong;
2814
2826
  const resolvedRenderSongLink = (0, import_react19.useCallback)(
2815
- (song) => renderSongLink ? renderSongLink(song) : /* @__PURE__ */ (0, import_jsx_runtime45.jsx)("button", { type: "button", onClick: () => openSongRef.current(song.uuid), className: "hover:underline", children: song.title }),
2827
+ (song) => renderSongLink ? renderSongLink(song) : /* @__PURE__ */ (0, import_jsx_runtime46.jsx)("button", { type: "button", onClick: () => openSongRef.current(song.uuid), className: "hover:underline", children: song.title }),
2816
2828
  [renderSongLink]
2817
2829
  );
2818
2830
  const [expandedSongUuid, setExpandedSongUuid] = (0, import_react19.useState)(null);
2819
2831
  const [expandedArtistUuid, setExpandedArtistUuid] = (0, import_react19.useState)(null);
2820
2832
  const artistBaseUrl = artistSearchBaseUrl ?? getBackendBaseUrl;
2821
2833
  const songBaseUrl = getBackendBaseUrl;
2822
- 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 }));
2834
+ 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 }));
2823
2835
  const getGenrePath = (genre) => {
2824
2836
  const path = [];
2825
2837
  let current = visibleGenrePlaylists.find((playlist) => playlist.criteria?.uuid === genre.uuid);
@@ -2895,9 +2907,11 @@ function GenreTreeView({
2895
2907
  !overlayPanel && hasSelectedGenreNode ? selectedNodeId : null,
2896
2908
  resolveStyle
2897
2909
  );
2898
- const [selectionHistory, setSelectionHistory] = (0, import_react19.useState)({ current: selectedGenreUuid, previous: null });
2899
- if (selectionHistory.current !== selectedGenreUuid) {
2900
- setSelectionHistory({ current: selectedGenreUuid, previous: selectionHistory.current ?? selectionHistory.previous });
2910
+ const treeStyles = (0, import_react19.useMemo)(() => toGenreNodeStyles(relations, visibleGenrePlaylists), [relations, visibleGenrePlaylists]);
2911
+ const validSelectedGenreUuid = (0, import_genre_tree30.toGenreUuid)(selectedGenreUuid);
2912
+ const [selectionHistory, setSelectionHistory] = (0, import_react19.useState)({ current: validSelectedGenreUuid, previous: null });
2913
+ if (selectionHistory.current !== validSelectedGenreUuid) {
2914
+ setSelectionHistory({ current: validSelectedGenreUuid, previous: selectionHistory.current ?? selectionHistory.previous });
2901
2915
  }
2902
2916
  const handlePrevious = selectionHistory.previous ? () => selectGenre(selectionHistory.previous) : void 0;
2903
2917
  const { showPopup, hidePopup } = (0, import_popup4.usePopup)();
@@ -2929,7 +2943,7 @@ function GenreTreeView({
2929
2943
  const canToggleSide = !readOnly && criteria !== void 0 && criteria !== null && parentPlaylist !== void 0 && parentPlaylist.parent === null;
2930
2944
  const side = criteria?.side ?? "core";
2931
2945
  const popSibling = canToggleSide && side === "core" ? visibleGenrePlaylists.find((sibling) => sibling.parent?.uuid === parentPlaylist.uuid && sibling.uuid !== genrePlaylist?.uuid && sibling.criteria?.side === "pop") : void 0;
2932
- const shareTarget = selectedGenreUuid ? { kind: "genre", genreUuid: selectedGenreUuid, name: criteria?.name ?? "Genre", scope, artistUuid } : void 0;
2946
+ const shareTarget = validSelectedGenreUuid ? { kind: "genre", genreUuid: validSelectedGenreUuid, name: criteria?.name ?? "Genre", scope, artistUuid } : void 0;
2933
2947
  const shareUrl = shareTarget ? getShareUrlRef.current?.(shareTarget) ?? void 0 : void 0;
2934
2948
  const genreContent = useGenreDetailContent({
2935
2949
  genreUuid: selectedGenreUuid,
@@ -2940,7 +2954,7 @@ function GenreTreeView({
2940
2954
  selectGenre(uuid);
2941
2955
  },
2942
2956
  expanded: isExpanded,
2943
- treeRelations: relations,
2957
+ treeStyles,
2944
2958
  playback,
2945
2959
  genrePlaylist: selectedGenrePlaylist,
2946
2960
  band: {
@@ -2951,7 +2965,7 @@ function GenreTreeView({
2951
2965
  onAddChild: readOnly || !criteria ? void 0 : () => handleGenreCreationAction(criteria),
2952
2966
  onRename: readOnly || !criteria ? void 0 : () => handleGenreRenameAction(criteria),
2953
2967
  onChangeParent: readOnly || !genrePlaylist || !criteria ? void 0 : () => showPopup(
2954
- /* @__PURE__ */ (0, import_jsx_runtime45.jsx)(
2968
+ /* @__PURE__ */ (0, import_jsx_runtime46.jsx)(
2955
2969
  ChangeParentPopup,
2956
2970
  {
2957
2971
  genrePlaylist: { ...genrePlaylist, criteria },
@@ -2995,7 +3009,7 @@ function GenreTreeView({
2995
3009
  onClose: otherPanel.onClose,
2996
3010
  searchName: otherPanel.title
2997
3011
  };
2998
- } else if (selectedGenreUuid) {
3012
+ } else if (selectedGenreUuid !== null) {
2999
3013
  activePanel = {
3000
3014
  title: genreContent.title,
3001
3015
  content: genreContent.content,
@@ -3012,19 +3026,19 @@ function GenreTreeView({
3012
3026
  edge: activePanel.expanded ? { label: "Shrink panel", onClick: () => activePanel.onExpandedChange?.(false) } : isCollapsed ? null : { label: "Hide panel", onClick: () => changeCollapsed(true) },
3013
3027
  center: activePanel.expanded ? null : isCollapsed ? { label: "Show panel", onClick: () => changeCollapsed(false) } : activePanel.onExpandedChange ? { label: "Extend panel", onClick: () => activePanel.onExpandedChange?.(true) } : null
3014
3028
  } : void 0;
3015
- const expandButton = placement === "default" && activePanel?.onExpandedChange ? /* @__PURE__ */ (0, import_jsx_runtime45.jsx)(
3029
+ const expandButton = placement === "default" && activePanel?.onExpandedChange ? /* @__PURE__ */ (0, import_jsx_runtime46.jsx)(
3016
3030
  import_ui21.IconButton,
3017
3031
  {
3018
3032
  "aria-label": activePanel.expanded ? "Shrink panel" : "Extend panel",
3019
3033
  style: activePanel.headerAccent && { color: "inherit" },
3020
3034
  onClick: () => activePanel.onExpandedChange?.(!activePanel.expanded),
3021
- children: activePanel.expanded ? /* @__PURE__ */ (0, import_jsx_runtime45.jsx)(import_lucide_react15.Minimize2, { "aria-hidden": "true", className: "h-4 w-4" }) : /* @__PURE__ */ (0, import_jsx_runtime45.jsx)(import_lucide_react15.Maximize2, { "aria-hidden": "true", className: "h-4 w-4" })
3035
+ 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" })
3022
3036
  }
3023
3037
  ) : null;
3024
3038
  const renderArtistFilter = () => {
3025
3039
  if (isLoading || !filterArtist) return null;
3026
- 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: [
3027
- /* @__PURE__ */ (0, import_jsx_runtime45.jsx)(
3040
+ 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: [
3041
+ /* @__PURE__ */ (0, import_jsx_runtime46.jsx)(
3028
3042
  "button",
3029
3043
  {
3030
3044
  type: "button",
@@ -3036,7 +3050,7 @@ function GenreTreeView({
3036
3050
  children: filterArtist.name
3037
3051
  }
3038
3052
  ),
3039
- /* @__PURE__ */ (0, import_jsx_runtime45.jsx)(
3053
+ /* @__PURE__ */ (0, import_jsx_runtime46.jsx)(
3040
3054
  "button",
3041
3055
  {
3042
3056
  type: "button",
@@ -3048,8 +3062,8 @@ function GenreTreeView({
3048
3062
  )
3049
3063
  ] });
3050
3064
  };
3051
- const actions = /* @__PURE__ */ (0, import_jsx_runtime45.jsxs)(import_jsx_runtime45.Fragment, { children: [
3052
- !isLoading && !isError && /* @__PURE__ */ (0, import_jsx_runtime45.jsx)(
3065
+ const actions = /* @__PURE__ */ (0, import_jsx_runtime46.jsxs)(import_jsx_runtime46.Fragment, { children: [
3066
+ !isLoading && !isError && /* @__PURE__ */ (0, import_jsx_runtime46.jsx)(
3053
3067
  GenreTreeSearch,
3054
3068
  {
3055
3069
  artistBaseUrl,
@@ -3082,9 +3096,9 @@ function GenreTreeView({
3082
3096
  extraGroups: extraSearchGroups
3083
3097
  }
3084
3098
  ),
3085
- !isLoading && !isError && filterArtist && /* @__PURE__ */ (0, import_jsx_runtime45.jsx)("div", { className: "hidden w-full justify-center max-md:flex", children: renderArtistFilter() }),
3086
- /* @__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: [
3087
- !isLoading && !isError && !isControlled && /* @__PURE__ */ (0, import_jsx_runtime45.jsx)(
3099
+ !isLoading && !isError && filterArtist && /* @__PURE__ */ (0, import_jsx_runtime46.jsx)("div", { className: "hidden w-full justify-center max-md:flex", children: renderArtistFilter() }),
3100
+ /* @__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: [
3101
+ !isLoading && !isError && !isControlled && /* @__PURE__ */ (0, import_jsx_runtime46.jsx)(
3088
3102
  import_ui21.SegmentedControl,
3089
3103
  {
3090
3104
  "aria-label": "Tree view mode",
@@ -3098,13 +3112,13 @@ function GenreTreeView({
3098
3112
  ]
3099
3113
  }
3100
3114
  ),
3101
- !isLoading && !isError && showDisplayOptions && /* @__PURE__ */ (0, import_jsx_runtime45.jsx)(
3115
+ !isLoading && !isError && showDisplayOptions && /* @__PURE__ */ (0, import_jsx_runtime46.jsx)(
3102
3116
  "div",
3103
3117
  {
3104
3118
  className: "flex items-center gap-1",
3105
3119
  role: "group",
3106
3120
  "aria-label": "Tree display options",
3107
- children: !isRotationControlled && viewMode !== "stacked" && viewMode !== "outline" && /* @__PURE__ */ (0, import_jsx_runtime45.jsx)(
3121
+ children: !isRotationControlled && viewMode !== "stacked" && viewMode !== "outline" && /* @__PURE__ */ (0, import_jsx_runtime46.jsx)(
3108
3122
  import_ui21.Button,
3109
3123
  {
3110
3124
  variant: allowWheelRotation ? "default" : "outline",
@@ -3115,48 +3129,48 @@ function GenreTreeView({
3115
3129
  )
3116
3130
  }
3117
3131
  ),
3118
- !isLoading && !isError && !readOnly && showAddRoot && /* @__PURE__ */ (0, import_jsx_runtime45.jsx)(
3132
+ !isLoading && !isError && !readOnly && showAddRoot && /* @__PURE__ */ (0, import_jsx_runtime46.jsx)(
3119
3133
  import_ui21.IconTextButton,
3120
3134
  {
3121
- icon: import_lucide_react14.Plus,
3135
+ icon: import_lucide_react13.Plus,
3122
3136
  text: "Add root",
3123
3137
  onClick: () => handleGenreCreationAction(null)
3124
3138
  }
3125
3139
  )
3126
3140
  ] })
3127
3141
  ] });
3128
- return /* @__PURE__ */ (0, import_jsx_runtime45.jsxs)(
3142
+ return /* @__PURE__ */ (0, import_jsx_runtime46.jsxs)(
3129
3143
  "div",
3130
3144
  {
3131
3145
  "data-left-panel": isPanelShown && !isCollapsed ? void 0 : "hidden",
3132
3146
  className: ["genre-tree-view relative flex flex-col h-full", placement === "regions" && REGIONS_CLASS, className2].filter(Boolean).join(" "),
3133
3147
  children: [
3134
- /* @__PURE__ */ (0, import_jsx_runtime45.jsx)(
3148
+ /* @__PURE__ */ (0, import_jsx_runtime46.jsx)(
3135
3149
  "div",
3136
3150
  {
3137
3151
  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"}`,
3138
3152
  children: actions
3139
3153
  }
3140
3154
  ),
3141
- !isLoading && !isError && filterArtist && /* @__PURE__ */ (0, import_jsx_runtime45.jsx)(
3155
+ !isLoading && !isError && filterArtist && /* @__PURE__ */ (0, import_jsx_runtime46.jsx)(
3142
3156
  "div",
3143
3157
  {
3144
3158
  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"}`,
3145
3159
  children: renderArtistFilter()
3146
3160
  }
3147
3161
  ),
3148
- /* @__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: [
3149
- !isLoading && !isError && viewMode === "outline" && floatingControlsLeading && /* @__PURE__ */ (0, import_jsx_runtime45.jsx)("div", { className: "absolute bottom-3 right-3 z-10", children: floatingControlsLeading }),
3150
- 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: [
3162
+ /* @__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: [
3163
+ !isLoading && !isError && viewMode === "outline" && floatingControlsLeading && /* @__PURE__ */ (0, import_jsx_runtime46.jsx)("div", { className: "absolute bottom-3 right-3 z-10", children: floatingControlsLeading }),
3164
+ 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: [
3151
3165
  "Couldn't load the genre tree.",
3152
3166
  " ",
3153
- /* @__PURE__ */ (0, import_jsx_runtime45.jsx)("button", { type: "button", className: "underline", onClick: refetch, children: "Retry" })
3167
+ /* @__PURE__ */ (0, import_jsx_runtime46.jsx)("button", { type: "button", className: "underline", onClick: refetch, children: "Retry" })
3154
3168
  ] }) : viewMode === "wheel" ? (
3155
3169
  // The info panel's top edge aligns with the tree's; the search bar floats `--layout-panel-inset` below it,
3156
3170
  // and the header inset (the bar's height plus that inset) extends the header's fill behind the bar,
3157
3171
  // Google Maps style; the 8px bottom padding mirrors the inset above the bar, and the hidden title's height is zeroed.
3158
3172
  // The panel's own Zoom in button is hidden: the search bar carries it.
3159
- /* @__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)(
3173
+ /* @__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)(
3160
3174
  GenrePlaylistTreeWheel,
3161
3175
  {
3162
3176
  scope,
@@ -3174,9 +3188,9 @@ function GenreTreeView({
3174
3188
  allowWheelRotation,
3175
3189
  sizeMetric: isArtistFiltered || !canSizeByViews ? "songs" : deferredSizeMetric,
3176
3190
  busy,
3177
- floatingControlsLeading: floatingControlsLeading || canSizeByViews ? /* @__PURE__ */ (0, import_jsx_runtime45.jsxs)("div", { className: "flex items-center gap-2", children: [
3191
+ floatingControlsLeading: floatingControlsLeading || canSizeByViews ? /* @__PURE__ */ (0, import_jsx_runtime46.jsxs)("div", { className: "flex items-center gap-2", children: [
3178
3192
  floatingControlsLeading,
3179
- canSizeByViews && /* @__PURE__ */ (0, import_jsx_runtime45.jsx)(SizeMetricToggle, { value: sizeMetric, onChange: changeSizeMetric })
3193
+ canSizeByViews && /* @__PURE__ */ (0, import_jsx_runtime46.jsx)(SizeMetricToggle, { value: sizeMetric, onChange: changeSizeMetric })
3180
3194
  ] }) : void 0,
3181
3195
  defaultViewport,
3182
3196
  viewportRestore,
@@ -3185,7 +3199,7 @@ function GenreTreeView({
3185
3199
  ) }) })
3186
3200
  ) : viewMode === "pop-core" ? (
3187
3201
  // Same as the wheel: the header inset extends its fill behind the floating search bar, Google Maps style.
3188
- /* @__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)(
3202
+ /* @__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)(
3189
3203
  GenrePlaylistTreeWheelRadialPopCore,
3190
3204
  {
3191
3205
  scope,
@@ -3203,9 +3217,9 @@ function GenreTreeView({
3203
3217
  allowWheelRotation,
3204
3218
  sizeMetric: isArtistFiltered || !canSizeByViews ? "songs" : deferredSizeMetric,
3205
3219
  busy,
3206
- floatingControlsLeading: floatingControlsLeading || canSizeByViews ? /* @__PURE__ */ (0, import_jsx_runtime45.jsxs)("div", { className: "flex items-center gap-2", children: [
3220
+ floatingControlsLeading: floatingControlsLeading || canSizeByViews ? /* @__PURE__ */ (0, import_jsx_runtime46.jsxs)("div", { className: "flex items-center gap-2", children: [
3207
3221
  floatingControlsLeading,
3208
- canSizeByViews && /* @__PURE__ */ (0, import_jsx_runtime45.jsx)(SizeMetricToggle, { value: sizeMetric, onChange: changeSizeMetric })
3222
+ canSizeByViews && /* @__PURE__ */ (0, import_jsx_runtime46.jsx)(SizeMetricToggle, { value: sizeMetric, onChange: changeSizeMetric })
3209
3223
  ] }) : void 0,
3210
3224
  defaultViewport,
3211
3225
  viewportRestore,
@@ -3213,7 +3227,7 @@ function GenreTreeView({
3213
3227
  ...focus
3214
3228
  }
3215
3229
  ) }) })
3216
- ) : 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)(
3230
+ ) : 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)(
3217
3231
  GenrePlaylistTreeWheelRadialPopCore,
3218
3232
  {
3219
3233
  outline: true,
@@ -3230,13 +3244,13 @@ function GenreTreeView({
3230
3244
  selectedNodeId,
3231
3245
  readOnly
3232
3246
  }
3233
- ) }) : /* @__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(
3247
+ ) }) : /* @__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(
3234
3248
  ([uuid, genrePlaylistTreePerRoot]) => {
3235
- return /* @__PURE__ */ (0, import_jsx_runtime45.jsx)(
3249
+ return /* @__PURE__ */ (0, import_jsx_runtime46.jsx)(
3236
3250
  "div",
3237
3251
  {
3238
3252
  className: "tree-per-root-container relative shrink-0 h-[28rem] mt-2 mr-16 p-2 bg-[var(--color-neutral-100)] rounded-lg",
3239
- children: /* @__PURE__ */ (0, import_jsx_runtime45.jsx)("div", { className: "graph-container relative z-10 h-full", children: /* @__PURE__ */ (0, import_jsx_runtime45.jsx)(
3253
+ children: /* @__PURE__ */ (0, import_jsx_runtime46.jsx)("div", { className: "graph-container relative z-10 h-full", children: /* @__PURE__ */ (0, import_jsx_runtime46.jsx)(
3240
3254
  GenrePlaylistTreePerRoot,
3241
3255
  {
3242
3256
  scope,
@@ -3263,7 +3277,7 @@ function GenreTreeView({
3263
3277
  );
3264
3278
  }
3265
3279
  ) }),
3266
- /* @__PURE__ */ (0, import_jsx_runtime45.jsx)(
3280
+ /* @__PURE__ */ (0, import_jsx_runtime46.jsx)(
3267
3281
  import_ui21.DetailPanel,
3268
3282
  {
3269
3283
  ref: panelRef,
@@ -3278,7 +3292,7 @@ function GenreTreeView({
3278
3292
  onExpandedChange: activePanel?.onExpandedChange,
3279
3293
  aside: activePanel?.aside,
3280
3294
  edgeTab: detailTab,
3281
- children: activePanel?.aside && !activePanel.expanded ? /* @__PURE__ */ (0, import_jsx_runtime45.jsxs)(import_jsx_runtime45.Fragment, { children: [
3295
+ children: activePanel?.aside && !activePanel.expanded ? /* @__PURE__ */ (0, import_jsx_runtime46.jsxs)(import_jsx_runtime46.Fragment, { children: [
3282
3296
  activePanel.content,
3283
3297
  activePanel.aside
3284
3298
  ] }) : activePanel?.content
@@ -3295,11 +3309,11 @@ var import_genre_tree_view6 = require("@behindthemusictree/genre-tree-view");
3295
3309
 
3296
3310
  // src/genre-tree/track-list-sidebar/TrackListSidebar.tsx
3297
3311
  var import_react20 = require("react");
3298
- var import_lucide_react16 = require("lucide-react");
3299
- var import_genre_tree31 = require("@behindthemusictree/app-kit/genre-tree");
3312
+ var import_lucide_react15 = require("lucide-react");
3300
3313
  var import_genre_tree32 = require("@behindthemusictree/app-kit/genre-tree");
3314
+ var import_genre_tree33 = require("@behindthemusictree/app-kit/genre-tree");
3301
3315
  var import_track_noun3 = require("@behindthemusictree/app-kit/track-noun");
3302
- var import_jsx_runtime46 = require("react/jsx-runtime");
3316
+ var import_jsx_runtime47 = require("react/jsx-runtime");
3303
3317
  var useBrowserLayoutEffect = typeof window === "undefined" ? import_react20.useEffect : import_react20.useLayoutEffect;
3304
3318
  function TrackListSidebar({
3305
3319
  className: className2 = "",
@@ -3312,7 +3326,7 @@ function TrackListSidebar({
3312
3326
  renderArtistLink,
3313
3327
  renderSongLink
3314
3328
  }) {
3315
- const { trackList, loadMore, autoAdvanceScrollRequest } = (0, import_genre_tree31.useTrackList)();
3329
+ const { trackList, loadMore, autoAdvanceScrollRequest } = (0, import_genre_tree32.useTrackList)();
3316
3330
  const trackNoun = (0, import_track_noun3.useTrackNoun)();
3317
3331
  const trackListRef = (0, import_react20.useRef)(null);
3318
3332
  const previousOriginRef = (0, import_react20.useRef)(trackList?.origin ?? null);
@@ -3401,25 +3415,25 @@ function TrackListSidebar({
3401
3415
  }
3402
3416
  };
3403
3417
  const positionClasses = layout === "inline" ? "relative w-full flex h-full flex-col" : "fixed right-0 w-[50rem] flex flex-col";
3404
- return trackList ? /* @__PURE__ */ (0, import_jsx_runtime46.jsxs)(
3418
+ return trackList ? /* @__PURE__ */ (0, import_jsx_runtime47.jsxs)(
3405
3419
  "div",
3406
3420
  {
3407
3421
  "data-theme": "dark",
3408
3422
  className: `track-list-sidebar ${positionClasses} rounded-2xl bg-[var(--color-surface)] pb-1 ${className2}`,
3409
3423
  style: layout === "fixed" ? { top: "63.5px", maxHeight: "calc(100vh - 63.5px - 79px)" } : void 0,
3410
3424
  children: [
3411
- /* @__PURE__ */ (0, import_jsx_runtime46.jsxs)("div", { className: "header flex h-16 items-center px-4 py-2 text-[var(--color-text)]", children: [
3412
- /* @__PURE__ */ (0, import_jsx_runtime46.jsxs)("div", { className: "min-w-0 flex flex-1 flex-col justify-center", children: [
3413
- /* @__PURE__ */ (0, import_jsx_runtime46.jsxs)("div", { className: "origin flex text-xl", children: [
3414
- /* @__PURE__ */ (0, import_jsx_runtime46.jsx)("div", { className: "from h-auto flex flex-col justify-center items-center mr-2", children: "From" }),
3415
- /* @__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 }) })
3425
+ /* @__PURE__ */ (0, import_jsx_runtime47.jsxs)("div", { className: "header flex h-16 items-center px-4 py-2 text-[var(--color-text)]", children: [
3426
+ /* @__PURE__ */ (0, import_jsx_runtime47.jsxs)("div", { className: "min-w-0 flex flex-1 flex-col justify-center", children: [
3427
+ /* @__PURE__ */ (0, import_jsx_runtime47.jsxs)("div", { className: "origin flex text-xl", children: [
3428
+ /* @__PURE__ */ (0, import_jsx_runtime47.jsx)("div", { className: "from h-auto flex flex-col justify-center items-center mr-2", children: "From" }),
3429
+ /* @__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 }) })
3416
3430
  ] }),
3417
- /* @__PURE__ */ (0, import_jsx_runtime46.jsxs)("div", { className: "info flex items-center text-body text-[var(--color-text-muted)]", children: [
3418
- 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 `,
3431
+ /* @__PURE__ */ (0, import_jsx_runtime47.jsxs)("div", { className: "info flex items-center text-body text-[var(--color-text-muted)]", children: [
3432
+ 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 `,
3419
3433
  `${trackList.total} ${trackList.total > 1 ? trackNoun.plural : trackNoun.singular}`
3420
3434
  ] })
3421
3435
  ] }),
3422
- (isCollapsed && onExpand || !isCollapsed && onMinimize) && /* @__PURE__ */ (0, import_jsx_runtime46.jsx)(
3436
+ (isCollapsed && onExpand || !isCollapsed && onMinimize) && /* @__PURE__ */ (0, import_jsx_runtime47.jsx)(
3423
3437
  "button",
3424
3438
  {
3425
3439
  type: "button",
@@ -3428,11 +3442,11 @@ function TrackListSidebar({
3428
3442
  "aria-expanded": !isCollapsed,
3429
3443
  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"}`,
3430
3444
  onClick: isCollapsed ? onExpand : onMinimize,
3431
- children: /* @__PURE__ */ (0, import_jsx_runtime46.jsx)(import_lucide_react16.ListMusic, { "aria-hidden": "true", className: "h-5 w-5" })
3445
+ children: /* @__PURE__ */ (0, import_jsx_runtime47.jsx)(import_lucide_react15.ListMusic, { "aria-hidden": "true", className: "h-5 w-5" })
3432
3446
  }
3433
3447
  )
3434
3448
  ] }),
3435
- !isCollapsed && /* @__PURE__ */ (0, import_jsx_runtime46.jsxs)(
3449
+ !isCollapsed && /* @__PURE__ */ (0, import_jsx_runtime47.jsxs)(
3436
3450
  "ul",
3437
3451
  {
3438
3452
  ref: trackListRef,
@@ -3442,7 +3456,7 @@ function TrackListSidebar({
3442
3456
  onPointerDown: markUserScrollIntent,
3443
3457
  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",
3444
3458
  children: [
3445
- trackList.tracks.map((track, index) => /* @__PURE__ */ (0, import_jsx_runtime46.jsx)("li", { "data-track-uuid": track.uuid, children: /* @__PURE__ */ (0, import_jsx_runtime46.jsx)(
3459
+ trackList.tracks.map((track, index) => /* @__PURE__ */ (0, import_jsx_runtime47.jsx)("li", { "data-track-uuid": track.uuid, children: /* @__PURE__ */ (0, import_jsx_runtime47.jsx)(
3446
3460
  TrackItem,
3447
3461
  {
3448
3462
  track,
@@ -3453,7 +3467,7 @@ function TrackListSidebar({
3453
3467
  renderSongLink
3454
3468
  }
3455
3469
  ) }, track.uuid)),
3456
- hasMore && /* @__PURE__ */ (0, import_jsx_runtime46.jsx)("li", { ref: loadMoreSentinelRef, "aria-hidden": "true", className: "h-px" })
3470
+ hasMore && /* @__PURE__ */ (0, import_jsx_runtime47.jsx)("li", { ref: loadMoreSentinelRef, "aria-hidden": "true", className: "h-px" })
3457
3471
  ]
3458
3472
  }
3459
3473
  )
@@ -3466,14 +3480,14 @@ function TrackListSidebar({
3466
3480
  var import_react21 = require("react");
3467
3481
  var import_tailwind_merge = require("tailwind-merge");
3468
3482
  var import_player14 = require("@behindthemusictree/app-kit/player");
3469
- var import_genre_tree34 = require("@behindthemusictree/app-kit/genre-tree");
3483
+ var import_genre_tree35 = require("@behindthemusictree/app-kit/genre-tree");
3470
3484
  var import_ui22 = require("@behindthemusictree/ui");
3471
3485
 
3472
3486
  // src/genre-tree/PlayerControls.tsx
3473
- var import_lucide_react17 = require("lucide-react");
3474
- var import_genre_tree33 = require("@behindthemusictree/app-kit/genre-tree");
3487
+ var import_lucide_react16 = require("lucide-react");
3488
+ var import_genre_tree34 = require("@behindthemusictree/app-kit/genre-tree");
3475
3489
  var import_player13 = require("@behindthemusictree/app-kit/player");
3476
- var import_jsx_runtime47 = require("react/jsx-runtime");
3490
+ var import_jsx_runtime48 = require("react/jsx-runtime");
3477
3491
  function PlayerControls() {
3478
3492
  const {
3479
3493
  duration,
@@ -3485,22 +3499,22 @@ function PlayerControls() {
3485
3499
  setVolume,
3486
3500
  volume
3487
3501
  } = (0, import_player13.usePlayer)();
3488
- const { hasNextTrack, hasPreviousTrack, playNextTrack, playPreviousTrack } = (0, import_genre_tree33.useTrackList)();
3502
+ const { hasNextTrack, hasPreviousTrack, playNextTrack, playPreviousTrack } = (0, import_genre_tree34.useTrackList)();
3489
3503
  const currentTime = (0, import_player13.useCurrentTime)();
3490
3504
  const isBusy = isLoading || playState === import_player13.PlayStates.LOADING;
3491
3505
  const isReady = !!playerTrackObject?.isReady && !isBusy && !playerTrackObject.loadError;
3492
3506
  const safeDuration = Number.isFinite(duration) && duration > 0 ? duration : 0;
3493
3507
  const safeCurrentTime = Number.isFinite(currentTime) ? Math.max(0, Math.min(currentTime, safeDuration)) : 0;
3494
3508
  const isPlaying = playState === import_player13.PlayStates.PLAYING;
3495
- return /* @__PURE__ */ (0, import_jsx_runtime47.jsxs)(
3509
+ return /* @__PURE__ */ (0, import_jsx_runtime48.jsxs)(
3496
3510
  "section",
3497
3511
  {
3498
3512
  "aria-label": "Player controls",
3499
3513
  className: "player-controls flex shrink-0 flex-col gap-2 border-b border-zinc-800 px-3 py-2 text-zinc-200",
3500
3514
  children: [
3501
- /* @__PURE__ */ (0, import_jsx_runtime47.jsxs)("div", { className: "flex min-w-0 items-center gap-2 text-xs tabular-nums text-zinc-400", children: [
3502
- /* @__PURE__ */ (0, import_jsx_runtime47.jsx)("span", { "aria-label": "Elapsed time", children: (0, import_genre_tree33.formatTime)(safeCurrentTime) }),
3503
- /* @__PURE__ */ (0, import_jsx_runtime47.jsx)(
3515
+ /* @__PURE__ */ (0, import_jsx_runtime48.jsxs)("div", { className: "flex min-w-0 items-center gap-2 text-xs tabular-nums text-zinc-400", children: [
3516
+ /* @__PURE__ */ (0, import_jsx_runtime48.jsx)("span", { "aria-label": "Elapsed time", children: (0, import_genre_tree34.formatTime)(safeCurrentTime) }),
3517
+ /* @__PURE__ */ (0, import_jsx_runtime48.jsx)(
3504
3518
  "input",
3505
3519
  {
3506
3520
  "aria-label": "Seek",
@@ -3514,11 +3528,11 @@ function PlayerControls() {
3514
3528
  onChange: (event) => seekTo(Number(event.currentTarget.value))
3515
3529
  }
3516
3530
  ),
3517
- /* @__PURE__ */ (0, import_jsx_runtime47.jsx)("span", { "aria-label": "Duration", children: (0, import_genre_tree33.formatTime)(safeDuration) })
3531
+ /* @__PURE__ */ (0, import_jsx_runtime48.jsx)("span", { "aria-label": "Duration", children: (0, import_genre_tree34.formatTime)(safeDuration) })
3518
3532
  ] }),
3519
- /* @__PURE__ */ (0, import_jsx_runtime47.jsxs)("div", { className: "grid min-h-9 grid-cols-[1fr_auto_1fr] items-center gap-3", children: [
3520
- /* @__PURE__ */ (0, import_jsx_runtime47.jsxs)("div", { className: "col-start-2 flex items-center justify-center gap-2", children: [
3521
- /* @__PURE__ */ (0, import_jsx_runtime47.jsx)(
3533
+ /* @__PURE__ */ (0, import_jsx_runtime48.jsxs)("div", { className: "grid min-h-9 grid-cols-[1fr_auto_1fr] items-center gap-3", children: [
3534
+ /* @__PURE__ */ (0, import_jsx_runtime48.jsxs)("div", { className: "col-start-2 flex items-center justify-center gap-2", children: [
3535
+ /* @__PURE__ */ (0, import_jsx_runtime48.jsx)(
3522
3536
  "button",
3523
3537
  {
3524
3538
  type: "button",
@@ -3527,10 +3541,10 @@ function PlayerControls() {
3527
3541
  disabled: !isReady || !hasPreviousTrack,
3528
3542
  onClick: playPreviousTrack,
3529
3543
  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",
3530
- children: /* @__PURE__ */ (0, import_jsx_runtime47.jsx)(import_lucide_react17.SkipBack, { "aria-hidden": "true", className: "h-4 w-4" })
3544
+ children: /* @__PURE__ */ (0, import_jsx_runtime48.jsx)(import_lucide_react16.SkipBack, { "aria-hidden": "true", className: "h-4 w-4" })
3531
3545
  }
3532
3546
  ),
3533
- /* @__PURE__ */ (0, import_jsx_runtime47.jsx)(
3547
+ /* @__PURE__ */ (0, import_jsx_runtime48.jsx)(
3534
3548
  "button",
3535
3549
  {
3536
3550
  type: "button",
@@ -3539,10 +3553,10 @@ function PlayerControls() {
3539
3553
  disabled: !isReady,
3540
3554
  onClick: handlePlayPauseAction,
3541
3555
  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",
3542
- children: isPlaying ? /* @__PURE__ */ (0, import_jsx_runtime47.jsx)(import_lucide_react17.Pause, { "aria-hidden": "true", className: "h-4 w-4", fill: "currentColor" }) : /* @__PURE__ */ (0, import_jsx_runtime47.jsx)(import_lucide_react17.Play, { "aria-hidden": "true", className: "h-4 w-4", fill: "currentColor" })
3556
+ 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" })
3543
3557
  }
3544
3558
  ),
3545
- /* @__PURE__ */ (0, import_jsx_runtime47.jsx)(
3559
+ /* @__PURE__ */ (0, import_jsx_runtime48.jsx)(
3546
3560
  "button",
3547
3561
  {
3548
3562
  type: "button",
@@ -3551,14 +3565,14 @@ function PlayerControls() {
3551
3565
  disabled: !isReady || !hasNextTrack,
3552
3566
  onClick: playNextTrack,
3553
3567
  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",
3554
- children: /* @__PURE__ */ (0, import_jsx_runtime47.jsx)(import_lucide_react17.SkipForward, { "aria-hidden": "true", className: "h-4 w-4" })
3568
+ children: /* @__PURE__ */ (0, import_jsx_runtime48.jsx)(import_lucide_react16.SkipForward, { "aria-hidden": "true", className: "h-4 w-4" })
3555
3569
  }
3556
3570
  )
3557
3571
  ] }),
3558
- /* @__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: [
3559
- volume === 0 ? /* @__PURE__ */ (0, import_jsx_runtime47.jsx)(import_lucide_react17.VolumeX, { "aria-hidden": "true", className: "h-[0.7rem] w-[0.7rem] shrink-0" }) : /* @__PURE__ */ (0, import_jsx_runtime47.jsx)(import_lucide_react17.Volume2, { "aria-hidden": "true", className: "h-[0.7rem] w-[0.7rem] shrink-0" }),
3560
- /* @__PURE__ */ (0, import_jsx_runtime47.jsx)("span", { className: "sr-only", children: "Volume" }),
3561
- /* @__PURE__ */ (0, import_jsx_runtime47.jsx)(
3572
+ /* @__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: [
3573
+ 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" }),
3574
+ /* @__PURE__ */ (0, import_jsx_runtime48.jsx)("span", { className: "sr-only", children: "Volume" }),
3575
+ /* @__PURE__ */ (0, import_jsx_runtime48.jsx)(
3562
3576
  "input",
3563
3577
  {
3564
3578
  "aria-label": "Volume",
@@ -3580,7 +3594,7 @@ function PlayerControls() {
3580
3594
  }
3581
3595
 
3582
3596
  // src/genre-tree/PlayerPanel.tsx
3583
- var import_jsx_runtime48 = require("react/jsx-runtime");
3597
+ var import_jsx_runtime49 = require("react/jsx-runtime");
3584
3598
  var PANEL_ID = "player-panel";
3585
3599
  var DEFAULT_EXTENDED_WIDTH = "calc(100vw - var(--layout-center-left) - var(--layout-gutter))";
3586
3600
  function PlayerPanel({
@@ -3593,7 +3607,7 @@ function PlayerPanel({
3593
3607
  renderSongLink
3594
3608
  }) {
3595
3609
  const { playerTrackObject, retryPlayback } = (0, import_player14.usePlayer)();
3596
- const { toggleTrackListSidebar, isTrackListSidebarVisible } = (0, import_genre_tree34.useTrackListSidebarVisibility)();
3610
+ const { toggleTrackListSidebar, isTrackListSidebarVisible } = (0, import_genre_tree35.useTrackListSidebarVisibility)();
3597
3611
  const [lastLoadedTrackTitle, setLastLoadedTrackTitle] = (0, import_react21.useState)("");
3598
3612
  const loadedTrackTitle = playerTrackObject?.track.title;
3599
3613
  (0, import_react21.useEffect)(() => {
@@ -3620,7 +3634,7 @@ function PlayerPanel({
3620
3634
  onClick: () => setIsExtendRequested(true),
3621
3635
  expanded: false
3622
3636
  } : null;
3623
- return /* @__PURE__ */ (0, import_jsx_runtime48.jsxs)(
3637
+ return /* @__PURE__ */ (0, import_jsx_runtime49.jsxs)(
3624
3638
  "div",
3625
3639
  {
3626
3640
  id: PANEL_ID,
@@ -3633,7 +3647,7 @@ function PlayerPanel({
3633
3647
  ),
3634
3648
  style: isExtended ? { width: extendedWidth } : void 0,
3635
3649
  children: [
3636
- /* @__PURE__ */ (0, import_jsx_runtime48.jsx)(
3650
+ /* @__PURE__ */ (0, import_jsx_runtime49.jsx)(
3637
3651
  import_ui22.PanelEdgeTab,
3638
3652
  {
3639
3653
  side: "right",
@@ -3643,7 +3657,7 @@ function PlayerPanel({
3643
3657
  className: "absolute right-full top-1/2 -translate-y-1/2"
3644
3658
  }
3645
3659
  ),
3646
- /* @__PURE__ */ (0, import_jsx_runtime48.jsxs)(
3660
+ /* @__PURE__ */ (0, import_jsx_runtime49.jsxs)(
3647
3661
  "div",
3648
3662
  {
3649
3663
  className: (0, import_tailwind_merge.twMerge)(
@@ -3651,20 +3665,20 @@ function PlayerPanel({
3651
3665
  isExtended ? "flex-row" : "flex-col"
3652
3666
  ),
3653
3667
  children: [
3654
- /* @__PURE__ */ (0, import_jsx_runtime48.jsxs)("div", { className: "flex w-[var(--layout-right-w)] shrink-0 flex-col", children: [
3655
- /* @__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 }) }),
3656
- /* @__PURE__ */ (0, import_jsx_runtime48.jsxs)("div", { className: "relative w-full shrink-0", children: [
3657
- /* @__PURE__ */ (0, import_jsx_runtime48.jsx)(import_player14.PlayerVideoSurface, { className: "aspect-video w-full bg-black" }),
3658
- playerTrackObject?.loadError && /* @__PURE__ */ (0, import_jsx_runtime48.jsxs)("span", { className: "absolute bottom-2 left-2 right-2 text-xs text-red-400", children: [
3668
+ /* @__PURE__ */ (0, import_jsx_runtime49.jsxs)("div", { className: "flex w-[var(--layout-right-w)] shrink-0 flex-col", children: [
3669
+ /* @__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 }) }),
3670
+ /* @__PURE__ */ (0, import_jsx_runtime49.jsxs)("div", { className: "relative w-full shrink-0", children: [
3671
+ /* @__PURE__ */ (0, import_jsx_runtime49.jsx)(import_player14.PlayerVideoSurface, { className: "aspect-video w-full bg-black" }),
3672
+ playerTrackObject?.loadError && /* @__PURE__ */ (0, import_jsx_runtime49.jsxs)("span", { className: "absolute bottom-2 left-2 right-2 text-xs text-red-400", children: [
3659
3673
  playerTrackObject.loadError,
3660
- /* @__PURE__ */ (0, import_jsx_runtime48.jsx)("button", { type: "button", className: "ml-2 underline", onClick: () => {
3674
+ /* @__PURE__ */ (0, import_jsx_runtime49.jsx)("button", { type: "button", className: "ml-2 underline", onClick: () => {
3661
3675
  void retryPlayback();
3662
3676
  }, children: "Retry" })
3663
3677
  ] })
3664
3678
  ] }),
3665
- /* @__PURE__ */ (0, import_jsx_runtime48.jsx)(PlayerControls, {})
3679
+ /* @__PURE__ */ (0, import_jsx_runtime49.jsx)(PlayerControls, {})
3666
3680
  ] }),
3667
- /* @__PURE__ */ (0, import_jsx_runtime48.jsx)(
3681
+ /* @__PURE__ */ (0, import_jsx_runtime49.jsx)(
3668
3682
  TrackListSidebar,
3669
3683
  {
3670
3684
  layout: "inline",
@@ -3690,33 +3704,33 @@ function PlayerPanel({
3690
3704
  // src/genre-tree/Rating.tsx
3691
3705
  var import_uuid = require("uuid");
3692
3706
  var import_ai = require("react-icons/ai");
3693
- var import_genre_tree35 = require("@behindthemusictree/app-kit/genre-tree");
3694
- var import_jsx_runtime49 = require("react/jsx-runtime");
3707
+ var import_genre_tree36 = require("@behindthemusictree/app-kit/genre-tree");
3708
+ var import_jsx_runtime50 = require("react/jsx-runtime");
3695
3709
  function Rating({ rating, handleChange }) {
3696
3710
  const instanceId = (0, import_uuid.v4)();
3697
- 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: [
3698
- /* @__PURE__ */ (0, import_jsx_runtime49.jsx)(
3711
+ 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: [
3712
+ /* @__PURE__ */ (0, import_jsx_runtime50.jsx)(
3699
3713
  "input",
3700
3714
  {
3701
3715
  id: `star-${instanceId}-${i}`,
3702
3716
  className: "w-6 h-6 opacity-0",
3703
3717
  type: "radio",
3704
3718
  name: "rating",
3705
- value: i === 0 ? import_genre_tree35.FORM_RATING_NULL_VALUE : i,
3719
+ value: i === 0 ? import_genre_tree36.FORM_RATING_NULL_VALUE : i,
3706
3720
  checked: rating === (i === 0 ? null : i),
3707
3721
  onChange: handleChange
3708
3722
  }
3709
3723
  ),
3710
- /* @__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: [
3711
- /* @__PURE__ */ (0, import_jsx_runtime49.jsx)(
3724
+ /* @__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: [
3725
+ /* @__PURE__ */ (0, import_jsx_runtime50.jsx)(
3712
3726
  import_ai.AiOutlineStar,
3713
3727
  {
3714
- 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)]"}`
3728
+ 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)]"}`
3715
3729
  }
3716
3730
  ),
3717
- /* @__PURE__ */ (0, import_jsx_runtime49.jsx)("span", { className: "flex-grow w-8" }),
3731
+ /* @__PURE__ */ (0, import_jsx_runtime50.jsx)("span", { className: "flex-grow w-8" }),
3718
3732
  " "
3719
- ] }) : /* @__PURE__ */ (0, import_jsx_runtime49.jsx)(
3733
+ ] }) : /* @__PURE__ */ (0, import_jsx_runtime50.jsx)(
3720
3734
  import_ai.AiFillStar,
3721
3735
  {
3722
3736
  className: `w-7 h-7 absolute top-0 left-0 ${(rating ?? -1) >= i ? "text-[var(--color-accent-rating)]" : "text-[var(--color-text-muted)]"}`
@@ -3728,7 +3742,7 @@ function Rating({ rating, handleChange }) {
3728
3742
  // src/genre-tree/EntityDetailPanel.tsx
3729
3743
  var import_react22 = require("react");
3730
3744
  var import_ui23 = require("@behindthemusictree/ui");
3731
- var import_jsx_runtime50 = require("react/jsx-runtime");
3745
+ var import_jsx_runtime51 = require("react/jsx-runtime");
3732
3746
  function EntityDetailPanel({
3733
3747
  scope,
3734
3748
  getBackendBaseUrl,
@@ -3751,8 +3765,8 @@ function EntityDetailPanel({
3751
3765
  getBackendBaseUrl,
3752
3766
  getArtistBackendBaseUrl,
3753
3767
  getShareUrl,
3754
- 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 })),
3755
- renderSongLink: renderSongLink ?? ((song) => /* @__PURE__ */ (0, import_jsx_runtime50.jsx)("button", { type: "button", onClick: () => onSongUuidChange(song.uuid), className: "hover:underline", children: song.title })),
3768
+ 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 })),
3769
+ renderSongLink: renderSongLink ?? ((song) => /* @__PURE__ */ (0, import_jsx_runtime51.jsx)("button", { type: "button", onClick: () => onSongUuidChange(song.uuid), className: "hover:underline", children: song.title })),
3756
3770
  song: {
3757
3771
  uuid: songUuid,
3758
3772
  expanded: songUuid !== null && expandedSongUuid === songUuid,
@@ -3768,7 +3782,7 @@ function EntityDetailPanel({
3768
3782
  ...navigation
3769
3783
  });
3770
3784
  if (!panel) return null;
3771
- return /* @__PURE__ */ (0, import_jsx_runtime50.jsx)(
3785
+ return /* @__PURE__ */ (0, import_jsx_runtime51.jsx)(
3772
3786
  import_ui23.DetailPanel,
3773
3787
  {
3774
3788
  side: "left",
@@ -3780,7 +3794,7 @@ function EntityDetailPanel({
3780
3794
  onExpandedChange: panel.onExpandedChange,
3781
3795
  aside: panel.extraContent,
3782
3796
  edgeTab,
3783
- children: panel.extraContent && !panel.expanded ? /* @__PURE__ */ (0, import_jsx_runtime50.jsxs)(import_jsx_runtime50.Fragment, { children: [
3797
+ children: panel.extraContent && !panel.expanded ? /* @__PURE__ */ (0, import_jsx_runtime51.jsxs)(import_jsx_runtime51.Fragment, { children: [
3784
3798
  panel.content,
3785
3799
  panel.extraContent
3786
3800
  ] }) : panel.content