@behindthemusictree/app-kit-ui 37.0.0 → 38.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.cjs CHANGED
@@ -24,6 +24,7 @@ __export(src_exports, {
24
24
  AuthErrorPopup: () => AuthErrorPopup,
25
25
  AuthPopup: () => AuthPopup,
26
26
  BasePopup: () => BasePopup,
27
+ EntityDetailPanel: () => EntityDetailPanel,
27
28
  GenreDetailPanel: () => GenreDetailPanel,
28
29
  GenreInfoPanelBody: () => GenreInfoPanelBody,
29
30
  GenreNodeActionBand: () => GenreNodeActionBand,
@@ -50,6 +51,7 @@ __export(src_exports, {
50
51
  TrackUploadPopup: () => TrackUploadPopup,
51
52
  searchStatus: () => searchStatus,
52
53
  useArtistSearchGroup: () => useArtistSearchGroup,
54
+ useEntityPanel: () => useEntityPanel,
53
55
  useGenrePlaylistPlayback: () => useGenrePlaylistPlayback,
54
56
  useGenreRemoteSearchGroup: () => useGenreRemoteSearchGroup,
55
57
  useSongSearchGroup: () => useSongSearchGroup
@@ -258,7 +260,7 @@ function Toast({ message, onClose, durationMs = 2200 }) {
258
260
  role: "status",
259
261
  "aria-live": "polite",
260
262
  "aria-atomic": "true",
261
- className: "pointer-events-auto w-full max-w-md rounded-md bg-[var(--color-surface)] px-4 py-4 text-sm text-[var(--color-text)] shadow-lg ring-1 ring-black/10",
263
+ className: "pointer-events-auto w-full max-w-md rounded-md bg-[var(--color-surface)] px-4 py-4 text-body text-[var(--color-text)] shadow-lg ring-1 ring-black/10",
262
264
  children: message
263
265
  }
264
266
  ) }),
@@ -330,14 +332,14 @@ function TrackUploadPopup({
330
332
  /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)("div", { className: "flex items-center space-x-3", children: [
331
333
  getStatusIcon(item.status),
332
334
  /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)("div", { children: [
333
- /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("div", { className: "font-medium text-sm truncate max-w-xs", title: item.file.name, children: item.file.name }),
334
- /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)("div", { className: "text-xs text-[var(--color-neutral-500)]", children: [
335
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("div", { className: "font-medium text-body truncate max-w-xs", title: item.file.name, children: item.file.name }),
336
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)("div", { className: "text-caption text-[var(--color-neutral-500)]", children: [
335
337
  (item.file.size / 1024 / 1024).toFixed(1),
336
338
  " MB"
337
339
  ] })
338
340
  ] })
339
341
  ] }),
340
- /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("div", { className: "text-sm text-[var(--color-neutral-600)]", children: getStatusText(item.status) })
342
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("div", { className: "text-body text-[var(--color-neutral-600)]", children: getStatusText(item.status) })
341
343
  ] }),
342
344
  item.status === "uploading" && /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("div", { className: "w-full bg-[var(--color-neutral-200)] rounded-full h-2", children: /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(
343
345
  "div",
@@ -346,9 +348,9 @@ function TrackUploadPopup({
346
348
  style: { width: `${item.progress}%` }
347
349
  }
348
350
  ) }),
349
- item.status === "error" && item.error && /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("div", { className: "text-[var(--color-red-500)] text-sm mt-2", children: item.error.name === "InvalidInputError" ? "Upload failed due to invalid file data. Please check your file and try again." : item.error.name === "ZodError" ? "Upload failed due to a server error. Please try again later." : item.error.message })
351
+ item.status === "error" && item.error && /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("div", { className: "text-[var(--color-red-500)] text-body mt-2", children: item.error.name === "InvalidInputError" ? "Upload failed due to invalid file data. Please check your file and try again." : item.error.name === "ZodError" ? "Upload failed due to a server error. Please try again later." : item.error.message })
350
352
  ] }, item.id)),
351
- allComplete && /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("div", { className: "flex justify-between items-center", children: /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)("div", { className: "text-sm text-[var(--color-neutral-600)]", children: [
353
+ allComplete && /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("div", { className: "flex justify-between items-center", children: /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)("div", { className: "text-body text-[var(--color-neutral-600)]", children: [
352
354
  successfulCount,
353
355
  " successful, ",
354
356
  errorCount,
@@ -397,7 +399,7 @@ function InternalErrorPopup({ errorCode, contactEmail, ...rest }) {
397
399
  " ",
398
400
  /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("a", { href: `mailto:${contactEmail}`, className: "text-[var(--color-primary)] hover:text-[var(--color-primary-hover)]", children: contactEmail })
399
401
  ] }) }),
400
- errorCode && /* @__PURE__ */ (0, import_jsx_runtime7.jsxs)("div", { className: "text-sm text-[var(--color-neutral-500)] bg-[var(--color-neutral-100)] p-3 rounded-lg border border-[var(--color-neutral-100)]", children: [
402
+ errorCode && /* @__PURE__ */ (0, import_jsx_runtime7.jsxs)("div", { className: "text-body text-[var(--color-neutral-500)] bg-[var(--color-neutral-100)] p-3 rounded-lg border border-[var(--color-neutral-100)]", children: [
401
403
  "Error Code: ",
402
404
  errorCode
403
405
  ] })
@@ -513,7 +515,7 @@ function SpotifyAuthErrorPopup({
513
515
  /* @__PURE__ */ (0, import_jsx_runtime10.jsx)(import_fa2.FaSpotify, { className: "text-[#1DB954] text-7xl" }),
514
516
  /* @__PURE__ */ (0, import_jsx_runtime10.jsxs)("div", { className: "space-y-3 text-center", children: [
515
517
  /* @__PURE__ */ (0, import_jsx_runtime10.jsx)("p", { className: "text-xl font-medium text-[var(--color-text)]", children: message }),
516
- details && /* @__PURE__ */ (0, import_jsx_runtime10.jsx)("p", { className: "text-sm text-[var(--color-neutral-500)] bg-[var(--color-neutral-100)] p-3 rounded-lg border border-[var(--color-neutral-100)]", children: details }),
518
+ details && /* @__PURE__ */ (0, import_jsx_runtime10.jsx)("p", { className: "text-body text-[var(--color-neutral-500)] bg-[var(--color-neutral-100)] p-3 rounded-lg border border-[var(--color-neutral-100)]", children: details }),
517
519
  /* @__PURE__ */ (0, import_jsx_runtime10.jsxs)("div", { className: "space-y-2 text-center", children: [
518
520
  /* @__PURE__ */ (0, import_jsx_runtime10.jsx)("p", { className: "text-base text-[var(--color-neutral-700)]", children: "The Spotify app is in development mode, so only allowlisted accounts can connect." }),
519
521
  requestAccessHref && contactEmail ? /* @__PURE__ */ (0, import_jsx_runtime10.jsxs)("p", { className: "text-base text-[var(--color-neutral-700)]", children: [
@@ -1109,7 +1111,7 @@ function useArtistSearchGroup(getBaseUrl, text, onSelect, enabled = true) {
1109
1111
  artist.disambiguation
1110
1112
  ] })
1111
1113
  ] }),
1112
- artist.matchedAlias && /* @__PURE__ */ (0, import_jsx_runtime17.jsxs)("span", { className: "block truncate text-xs text-[var(--color-neutral-500)]", children: [
1114
+ artist.matchedAlias && /* @__PURE__ */ (0, import_jsx_runtime17.jsxs)("span", { className: "block truncate text-caption text-[var(--color-neutral-500)]", children: [
1113
1115
  "also known as ",
1114
1116
  artist.matchedAlias
1115
1117
  ] })
@@ -1134,7 +1136,7 @@ function useSongSearchGroup(getBaseUrl, text, onSelect, enabled = true) {
1134
1136
  key: song.uuid,
1135
1137
  content: /* @__PURE__ */ (0, import_jsx_runtime18.jsxs)("span", { className: (0, import_genre_tree7.isSongSearchResultPlayable)(song) ? void 0 : "opacity-60", children: [
1136
1138
  /* @__PURE__ */ (0, import_jsx_runtime18.jsx)("span", { className: "block truncate font-medium", children: song.title }),
1137
- /* @__PURE__ */ (0, import_jsx_runtime18.jsx)("span", { className: "block truncate text-xs text-[var(--color-neutral-500)]", children: [song.artists.map((artist) => artist.name).join(", "), song.genre?.name ?? "No genre"].filter(Boolean).join(" \xB7 ") })
1139
+ /* @__PURE__ */ (0, import_jsx_runtime18.jsx)("span", { className: "block truncate text-caption text-[var(--color-neutral-500)]", children: [song.artists.map((artist) => artist.name).join(", "), song.genre?.name ?? "No genre"].filter(Boolean).join(" \xB7 ") })
1138
1140
  ] }),
1139
1141
  onSelect: () => onSelect(song)
1140
1142
  })),
@@ -1157,9 +1159,9 @@ function useGenreRemoteSearchGroup(getBaseUrl, text, onSelect, { key = "genres",
1157
1159
  content: /* @__PURE__ */ (0, import_jsx_runtime19.jsxs)(import_jsx_runtime19.Fragment, { children: [
1158
1160
  /* @__PURE__ */ (0, import_jsx_runtime19.jsxs)("span", { className: "block truncate font-medium", children: [
1159
1161
  genre.name,
1160
- genre.isUmbrella && /* @__PURE__ */ (0, import_jsx_runtime19.jsx)("span", { className: "ml-2 text-xs font-normal italic text-[var(--color-text-muted)]", children: "umbrella" })
1162
+ genre.isUmbrella && /* @__PURE__ */ (0, import_jsx_runtime19.jsx)("span", { className: "ml-2 text-caption font-normal italic text-[var(--color-text-muted)]", children: "umbrella" })
1161
1163
  ] }),
1162
- /* @__PURE__ */ (0, import_jsx_runtime19.jsx)("span", { className: "block truncate text-xs text-[var(--color-neutral-500)]", children: describe?.(genre) ?? `${genre.treeName} \xB7 ${genre.root.name}` })
1164
+ /* @__PURE__ */ (0, import_jsx_runtime19.jsx)("span", { className: "block truncate text-caption text-[var(--color-neutral-500)]", children: describe?.(genre) ?? `${genre.treeName} \xB7 ${genre.root.name}` })
1163
1165
  ] }),
1164
1166
  onSelect: () => onSelect(genre)
1165
1167
  })),
@@ -1200,7 +1202,7 @@ function GenreSearch({
1200
1202
  key: genrePlaylist.uuid,
1201
1203
  content: /* @__PURE__ */ (0, import_jsx_runtime20.jsxs)(import_jsx_runtime20.Fragment, { children: [
1202
1204
  genrePlaylist.name,
1203
- genrePlaylist.isUmbrella && /* @__PURE__ */ (0, import_jsx_runtime20.jsx)("span", { className: "ml-2 text-xs italic text-[var(--color-text-muted)]", children: "umbrella" })
1205
+ genrePlaylist.isUmbrella && /* @__PURE__ */ (0, import_jsx_runtime20.jsx)("span", { className: "ml-2 text-caption italic text-[var(--color-text-muted)]", children: "umbrella" })
1204
1206
  ] }),
1205
1207
  onSelect: () => select(genrePlaylist)
1206
1208
  }))
@@ -1301,7 +1303,7 @@ var import_si = require("react-icons/si");
1301
1303
 
1302
1304
  // src/genre-tree/ShareDestinationButton.tsx
1303
1305
  var import_jsx_runtime22 = require("react/jsx-runtime");
1304
- var className = "group flex min-w-0 flex-1 flex-col items-center justify-center gap-1.5 rounded-xl px-2 py-2 text-xs text-[var(--color-text)] transition-colors hover:bg-[var(--color-surface-muted)] focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[var(--color-focus-ring)] disabled:cursor-not-allowed disabled:opacity-50";
1306
+ var className = "group flex min-w-0 flex-1 flex-col items-center justify-center gap-1.5 rounded-xl px-2 py-2 text-caption text-[var(--color-text)] transition-colors hover:bg-[var(--color-surface-muted)] focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[var(--color-focus-ring)] disabled:cursor-not-allowed disabled:opacity-50";
1305
1307
  function Contents({ icon, label }) {
1306
1308
  return /* @__PURE__ */ (0, import_jsx_runtime22.jsxs)(import_jsx_runtime22.Fragment, { children: [
1307
1309
  /* @__PURE__ */ (0, import_jsx_runtime22.jsx)("span", { className: "flex h-12 w-12 items-center justify-center rounded-full bg-[var(--color-surface-muted)] transition-colors group-hover:bg-[var(--color-border)]", children: icon }),
@@ -1331,9 +1333,9 @@ function SharePreviewCard({ target, url }) {
1331
1333
  return /* @__PURE__ */ (0, import_jsx_runtime23.jsx)("div", { className: "rounded-3xl bg-[var(--color-surface-muted)] p-4", children: /* @__PURE__ */ (0, import_jsx_runtime23.jsxs)("div", { className: "flex min-h-32 overflow-hidden rounded-2xl bg-[var(--color-surface)] shadow-lg", children: [
1332
1334
  /* @__PURE__ */ (0, import_jsx_runtime23.jsx)("div", { className: "flex w-28 shrink-0 items-center justify-center bg-[var(--color-primary)]/15 text-[var(--color-primary)]", children: /* @__PURE__ */ (0, import_jsx_runtime23.jsx)(Icon, { "aria-hidden": "true", className: "h-10 w-10" }) }),
1333
1335
  /* @__PURE__ */ (0, import_jsx_runtime23.jsxs)("div", { className: "flex min-w-0 flex-1 flex-col justify-center gap-1 p-4", children: [
1334
- /* @__PURE__ */ (0, import_jsx_runtime23.jsx)("span", { className: "text-xs font-medium uppercase tracking-wide text-[var(--color-text-muted)]", children: typeLabel }),
1336
+ /* @__PURE__ */ (0, import_jsx_runtime23.jsx)("span", { className: "text-caption font-medium uppercase tracking-wide text-[var(--color-text-muted)]", children: typeLabel }),
1335
1337
  /* @__PURE__ */ (0, import_jsx_runtime23.jsx)("span", { className: "truncate text-base font-semibold text-[var(--color-text)]", title: target.name, children: target.name }),
1336
- /* @__PURE__ */ (0, import_jsx_runtime23.jsx)("span", { className: "truncate text-sm text-[var(--color-primary)]", title: url, children: url })
1338
+ /* @__PURE__ */ (0, import_jsx_runtime23.jsx)("span", { className: "truncate text-body text-[var(--color-primary)]", title: url, children: url })
1337
1339
  ] })
1338
1340
  ] }) });
1339
1341
  }
@@ -1474,8 +1476,8 @@ function SharePopup({ target, url, onClose }) {
1474
1476
  }
1475
1477
  )
1476
1478
  ] }),
1477
- copyStatus === "failed" && /* @__PURE__ */ (0, import_jsx_runtime24.jsx)("p", { role: "alert", className: "text-center text-sm text-[var(--color-red-500)]", children: "Could not copy the link." }),
1478
- shareError && /* @__PURE__ */ (0, import_jsx_runtime24.jsx)("p", { role: "alert", className: "text-center text-sm text-[var(--color-red-500)]", children: shareError })
1479
+ copyStatus === "failed" && /* @__PURE__ */ (0, import_jsx_runtime24.jsx)("p", { role: "alert", className: "text-center text-body text-[var(--color-red-500)]", children: "Could not copy the link." }),
1480
+ shareError && /* @__PURE__ */ (0, import_jsx_runtime24.jsx)("p", { role: "alert", className: "text-center text-body text-[var(--color-red-500)]", children: shareError })
1479
1481
  ] })
1480
1482
  }
1481
1483
  ),
@@ -1495,7 +1497,7 @@ function ShareActionButton({ target, url }) {
1495
1497
  {
1496
1498
  type: "button",
1497
1499
  onClick: openSharePopup,
1498
- className: "group flex flex-col items-center gap-1 w-16 text-xs text-center text-[var(--color-text)] focus-visible:outline-none",
1500
+ className: "group flex flex-col items-center gap-1 w-16 text-caption text-center text-[var(--color-text)] focus-visible:outline-none",
1499
1501
  "aria-label": `Share ${target.name}`,
1500
1502
  title: `Share ${target.name}`,
1501
1503
  children: [
@@ -1577,7 +1579,7 @@ function GenreActionBand({
1577
1579
  disabled: item.disabled,
1578
1580
  title: item.title,
1579
1581
  onClick: item.onClick,
1580
- className: "group flex flex-col items-center gap-1 w-16 text-xs text-center text-[var(--color-text)] focus-visible:outline-none disabled:opacity-50",
1582
+ className: "group flex flex-col items-center gap-1 w-16 text-caption text-center text-[var(--color-text)] focus-visible:outline-none disabled:opacity-50",
1581
1583
  children: [
1582
1584
  /* @__PURE__ */ (0, import_jsx_runtime26.jsx)("span", { className: "flex items-center justify-center w-10 h-10 rounded-full group-focus-visible:ring-2 group-focus-visible:ring-[var(--color-focus-ring)] bg-[var(--color-surface-muted)] transition-colors group-hover:bg-[var(--color-border)] group-disabled:group-hover:bg-[var(--color-surface-muted)]", children: item.icon }),
1583
1585
  item.label
@@ -1596,7 +1598,7 @@ function GenreActionBand({
1596
1598
  "aria-label": "Admin actions",
1597
1599
  className: `px-3.5 py-3 ${items.length > 0 ? "border-t border-[var(--color-border)]" : ""}`,
1598
1600
  children: [
1599
- /* @__PURE__ */ (0, import_jsx_runtime26.jsx)("h3", { className: "mb-2 text-xs font-semibold uppercase tracking-wide text-[var(--color-text-muted)]", children: "Admin" }),
1601
+ /* @__PURE__ */ (0, import_jsx_runtime26.jsx)("h3", { className: "mb-2 text-caption font-semibold uppercase tracking-wide text-[var(--color-text-muted)]", children: "Admin" }),
1600
1602
  /* @__PURE__ */ (0, import_jsx_runtime26.jsx)("div", { className: "flex flex-wrap gap-1", children: renderItems(adminItems) })
1601
1603
  ]
1602
1604
  }
@@ -1628,7 +1630,7 @@ function ClampedText({ children, className: className2 = "" }) {
1628
1630
  type: "button",
1629
1631
  "aria-expanded": expanded,
1630
1632
  onClick: () => setExpanded((value) => !value),
1631
- className: "mt-1 text-xs text-[var(--color-text-muted)] underline hover:text-[var(--color-text)]",
1633
+ className: "mt-1 text-caption text-[var(--color-text-muted)] underline hover:text-[var(--color-text)]",
1632
1634
  children: expanded ? "Show less" : "Show more"
1633
1635
  }
1634
1636
  )
@@ -1779,7 +1781,7 @@ function GenrePlaylistTracks({
1779
1781
  ".",
1780
1782
  " ",
1781
1783
  /* @__PURE__ */ (0, import_jsx_runtime29.jsx)("button", { type: "button", className: "underline", onClick: () => void refetch(), children: "Retry" })
1782
- ] }) : tracks.length === 0 ? /* @__PURE__ */ (0, import_jsx_runtime29.jsx)("p", { children: "\u2014" }) : /* @__PURE__ */ (0, import_jsx_runtime29.jsxs)("ul", { className: "mt-1 max-h-64 overflow-y-auto text-[0.8125rem] leading-[1.45] [container-type:inline-size]", "aria-label": `${genreName} ${trackNoun.plural}`, children: [
1784
+ ] }) : tracks.length === 0 ? /* @__PURE__ */ (0, import_jsx_runtime29.jsx)("p", { children: "\u2014" }) : /* @__PURE__ */ (0, import_jsx_runtime29.jsxs)("ul", { className: "mt-1 max-h-64 overflow-y-auto text-body [container-type:inline-size]", "aria-label": `${genreName} ${trackNoun.plural}`, children: [
1783
1785
  tracks.map((track, index) => {
1784
1786
  const label = track.artists?.length ? `${track.title} \u2014 ${track.artists.map((artist2) => artist2.name).join(", ")}` : track.title;
1785
1787
  const isCurrent = playerTrackObject?.track.id === track.uuid;
@@ -1877,7 +1879,7 @@ function GenreDetailExtras({
1877
1879
  const { summary, summarySourceUrl, essentialSongs } = overview;
1878
1880
  const wikidataUrl = wikidataId ? `https://www.wikidata.org/wiki/${encodeURIComponent(wikidataId)}` : null;
1879
1881
  return /* @__PURE__ */ (0, import_jsx_runtime30.jsxs)(import_jsx_runtime30.Fragment, { children: [
1880
- (summarySourceUrl || wikidataUrl) && /* @__PURE__ */ (0, import_jsx_runtime30.jsx)(import_ui9.PanelSection, { title: "Links", children: /* @__PURE__ */ (0, import_jsx_runtime30.jsxs)("div", { className: "flex flex-wrap gap-x-3 gap-y-1 text-xs text-[var(--color-text)]", children: [
1882
+ (summarySourceUrl || wikidataUrl) && /* @__PURE__ */ (0, import_jsx_runtime30.jsx)(import_ui9.PanelSection, { title: "Links", children: /* @__PURE__ */ (0, import_jsx_runtime30.jsxs)("div", { className: "flex flex-wrap gap-x-3 gap-y-1 text-caption text-[var(--color-text)]", children: [
1881
1883
  summarySourceUrl && /* @__PURE__ */ (0, import_jsx_runtime30.jsxs)(
1882
1884
  "a",
1883
1885
  {
@@ -1905,10 +1907,10 @@ function GenreDetailExtras({
1905
1907
  }
1906
1908
  )
1907
1909
  ] }) }),
1908
- /* @__PURE__ */ (0, import_jsx_runtime30.jsx)(import_ui9.PanelSection, { title: "Summary", children: renderSummary ? renderSummary(overview) : /* @__PURE__ */ (0, import_jsx_runtime30.jsx)(ClampedText, { className: "text-[0.8125rem] leading-[1.45] text-[var(--color-text)]", children: summary ?? "\u2014" }, genreUuid) }),
1910
+ /* @__PURE__ */ (0, import_jsx_runtime30.jsx)(import_ui9.PanelSection, { title: "Summary", children: renderSummary ? renderSummary(overview) : /* @__PURE__ */ (0, import_jsx_runtime30.jsx)(ClampedText, { className: "text-body text-[var(--color-text)]", children: summary ?? "\u2014" }, genreUuid) }),
1909
1911
  family,
1910
1912
  renderGenreDetailExtras?.(overview),
1911
- (renderEssentialTracks || essentialSongs) && /* @__PURE__ */ (0, import_jsx_runtime30.jsx)(import_ui9.PanelSection, { title: `Essential ${trackNoun.plural}`, children: renderEssentialTracks ? renderEssentialTracks(overview) : essentialSongs && essentialSongs.length > 0 ? /* @__PURE__ */ (0, import_jsx_runtime30.jsx)("ul", { className: "text-[0.8125rem] leading-[1.45] text-[var(--color-text)]", children: essentialSongs.map((track) => /* @__PURE__ */ (0, import_jsx_runtime30.jsx)("li", { className: "py-1", children: track.title }, track.uuid)) }) : /* @__PURE__ */ (0, import_jsx_runtime30.jsx)("p", { className: "text-[0.8125rem] leading-[1.45] text-[var(--color-text)]", children: "\u2014" }) }),
1913
+ (renderEssentialTracks || essentialSongs) && /* @__PURE__ */ (0, import_jsx_runtime30.jsx)(import_ui9.PanelSection, { title: `Essential ${trackNoun.plural}`, children: renderEssentialTracks ? renderEssentialTracks(overview) : essentialSongs && essentialSongs.length > 0 ? /* @__PURE__ */ (0, import_jsx_runtime30.jsx)("ul", { className: "text-body text-[var(--color-text)]", children: essentialSongs.map((track) => /* @__PURE__ */ (0, import_jsx_runtime30.jsx)("li", { className: "py-1", children: track.title }, track.uuid)) }) : /* @__PURE__ */ (0, import_jsx_runtime30.jsx)("p", { className: "text-body text-[var(--color-text)]", children: "\u2014" }) }),
1912
1914
  /* @__PURE__ */ (0, import_jsx_runtime30.jsx)(
1913
1915
  GenrePlaylistTracks,
1914
1916
  {
@@ -1974,7 +1976,7 @@ function GenreInfoPanelBody({
1974
1976
  return /* @__PURE__ */ (0, import_jsx_runtime31.jsxs)(import_jsx_runtime31.Fragment, { children: [
1975
1977
  toolbar,
1976
1978
  genrePlaylist ? /* @__PURE__ */ (0, import_jsx_runtime31.jsx)(GenreNodeActionBand, { ...band, genrePlaylist, playback }) : /* @__PURE__ */ (0, import_jsx_runtime31.jsx)(GenreActionBand, { ...band }),
1977
- summary && catalogue && /* @__PURE__ */ (0, import_jsx_runtime31.jsxs)("div", { className: "px-3.5 py-3 text-sm text-[var(--color-text)]", children: [
1979
+ summary && catalogue && /* @__PURE__ */ (0, import_jsx_runtime31.jsxs)("div", { className: "px-3.5 py-3 text-body text-[var(--color-text)]", children: [
1978
1980
  /* @__PURE__ */ (0, import_jsx_runtime31.jsxs)("p", { children: [
1979
1981
  "Catalogue ",
1980
1982
  noun.plural,
@@ -2064,7 +2066,7 @@ function useGenreDetailContent({
2064
2066
  if (isError) return {
2065
2067
  name: null,
2066
2068
  title: "Genre unavailable",
2067
- content: /* @__PURE__ */ (0, import_jsx_runtime32.jsx)("p", { role: "alert", className: "p-4 text-sm text-[var(--color-text)]", children: "This genre could not be loaded." })
2069
+ content: /* @__PURE__ */ (0, import_jsx_runtime32.jsx)("p", { role: "alert", className: "p-4 text-body text-[var(--color-text)]", children: "This genre could not be loaded." })
2068
2070
  };
2069
2071
  if (isPending || !detail || !view) return {
2070
2072
  name: null,
@@ -2200,7 +2202,7 @@ function ChangeParentPopup({
2200
2202
  return () => window.removeEventListener("keydown", handleKeyDown);
2201
2203
  }, [onClose]);
2202
2204
  return /* @__PURE__ */ (0, import_jsx_runtime33.jsxs)(BasePopup_default, { title: `Change parent of ${genrePlaylist.name}`, onClose, showCancelButton: true, fitContent: true, children: [
2203
- /* @__PURE__ */ (0, import_jsx_runtime33.jsxs)("p", { className: "mb-3 text-sm text-[var(--color-text-muted)]", children: [
2205
+ /* @__PURE__ */ (0, import_jsx_runtime33.jsxs)("p", { className: "mb-3 text-body text-[var(--color-text-muted)]", children: [
2204
2206
  "Current parent: ",
2205
2207
  genrePlaylist.parent?.name ?? "none"
2206
2208
  ] }),
@@ -2212,7 +2214,7 @@ function ChangeParentPopup({
2212
2214
  placeholder: "Search a new parent\u2026"
2213
2215
  }
2214
2216
  ) }),
2215
- formErrors.length > 0 && /* @__PURE__ */ (0, import_jsx_runtime33.jsx)("p", { role: "alert", className: "mt-3 text-sm text-[var(--color-red-500)]", children: formErrors.map(({ message }) => message).join(" ") })
2217
+ formErrors.length > 0 && /* @__PURE__ */ (0, import_jsx_runtime33.jsx)("p", { role: "alert", className: "mt-3 text-body text-[var(--color-red-500)]", children: formErrors.map(({ message }) => message).join(" ") })
2216
2218
  ] });
2217
2219
  }
2218
2220
 
@@ -2298,7 +2300,7 @@ function ExternalLink({ href, children }) {
2298
2300
  function ArtistPhoto({ image, name }) {
2299
2301
  return /* @__PURE__ */ (0, import_jsx_runtime36.jsxs)("figure", { className: "flex flex-col gap-1.5 px-6 py-2.5", children: [
2300
2302
  /* @__PURE__ */ (0, import_jsx_runtime36.jsx)("img", { src: image.url, alt: name, className: "h-64 w-full object-contain bg-[var(--color-surface-muted)] rounded" }),
2301
- /* @__PURE__ */ (0, import_jsx_runtime36.jsxs)("figcaption", { className: "text-xs text-[var(--color-text-muted)]", children: [
2303
+ /* @__PURE__ */ (0, import_jsx_runtime36.jsxs)("figcaption", { className: "text-caption text-[var(--color-text-muted)]", children: [
2302
2304
  "Photo: ",
2303
2305
  /* @__PURE__ */ (0, import_jsx_runtime36.jsx)(ExternalLink, { href: image.pageUrl, children: image.author ?? "Wikimedia Commons" }),
2304
2306
  ",",
@@ -2497,7 +2499,7 @@ function TrackItem({
2497
2499
  /* @__PURE__ */ (0, import_jsx_runtime40.jsx)("div", { className: "flex max-w-full items-center gap-2", children: /* @__PURE__ */ (0, import_jsx_runtime40.jsx)("div", { className: "title text-lg font-bold text-[var(--color-text)] max-w-full truncate", children: renderSongLink ? renderSongLink(track) : (0, import_genre_tree22.isSong)(track) ? /* @__PURE__ */ (0, import_jsx_runtime40.jsx)(MusicBrainzRecordingLink, { song: track, className: "hover:underline focus-visible:underline", children: track.title }) : track.title }) }),
2498
2500
  track.artists && track.artists.length > 0 ? /* @__PURE__ */ (0, import_jsx_runtime40.jsx)("div", { className: "artist text-base max-w-full truncate text-[var(--color-text-muted)]", children: /* @__PURE__ */ (0, import_jsx_runtime40.jsx)(import_genre_tree23.ArtistNames, { artists: track.artists, renderArtistLink }) }) : ""
2499
2501
  ] }),
2500
- /* @__PURE__ */ (0, import_jsx_runtime40.jsx)("div", { className: "genre-name-container flex items-center justify-start w-[21.6%] min-w-0 shrink-0", children: track.genre ? /* @__PURE__ */ (0, import_jsx_runtime40.jsx)("div", { className: "genre-name max-w-full whitespace-normal break-words p-1 text-xs font-bold", children: track.genre.name }) : "" }),
2502
+ /* @__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 }) : "" }),
2501
2503
  renderDuration && /* @__PURE__ */ (0, import_jsx_runtime40.jsx)(
2502
2504
  "div",
2503
2505
  {
@@ -2553,14 +2555,14 @@ function ArtistSongs({ artist, getBackendBaseUrl, renderArtistLink, renderSongLi
2553
2555
  {
2554
2556
  type: "button",
2555
2557
  onClick: () => void play(),
2556
- className: "flex w-fit items-center gap-1.5 px-3 py-1 text-xs font-medium bg-gray-900 text-white rounded-full hover:bg-gray-700",
2558
+ 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",
2557
2559
  children: [
2558
2560
  /* @__PURE__ */ (0, import_jsx_runtime41.jsx)(import_lucide_react14.Play, { className: "h-3.5 w-3.5", "aria-hidden": "true" }),
2559
2561
  "Play all"
2560
2562
  ]
2561
2563
  }
2562
2564
  ),
2563
- /* @__PURE__ */ (0, import_jsx_runtime41.jsx)("div", { role: "group", "aria-label": "Sort songs by", className: "flex gap-1 text-xs", children: SORTS.map((sort) => /* @__PURE__ */ (0, import_jsx_runtime41.jsx)(
2565
+ /* @__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)(
2564
2566
  "button",
2565
2567
  {
2566
2568
  type: "button",
@@ -2681,7 +2683,7 @@ function SongHistory({ uuid, getBackendBaseUrl }) {
2681
2683
  if (history.status === "loading") body = /* @__PURE__ */ (0, import_jsx_runtime43.jsx)(import_ui17.Skeleton, { className: "h-10", "aria-busy": "true" });
2682
2684
  else if (history.status === "error") body = /* @__PURE__ */ (0, import_jsx_runtime43.jsx)("p", { className: "text-red-600", children: "Could not load the history." });
2683
2685
  else if (history.status === "success")
2684
- body = history.entries.length === 0 ? /* @__PURE__ */ (0, import_jsx_runtime43.jsx)("p", { className: "text-[var(--color-text-muted)]", children: "No changes yet." }) : /* @__PURE__ */ (0, import_jsx_runtime43.jsx)("ul", { className: "divide-y divide-[var(--color-border)]", children: history.entries.map((entry) => /* @__PURE__ */ (0, import_jsx_runtime43.jsxs)("li", { className: "py-1.5 text-sm", children: [
2686
+ 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: [
2685
2687
  /* @__PURE__ */ (0, import_jsx_runtime43.jsx)("span", { className: "font-medium", children: entry.action }),
2686
2688
  entry.oldValue !== null || entry.newValue !== null ? /* @__PURE__ */ (0, import_jsx_runtime43.jsxs)("span", { className: "text-[var(--color-text-muted)]", children: [
2687
2689
  " ",
@@ -2690,7 +2692,7 @@ function SongHistory({ uuid, getBackendBaseUrl }) {
2690
2692
  " \u2192 ",
2691
2693
  entry.newValue ?? "none"
2692
2694
  ] }) : null,
2693
- /* @__PURE__ */ (0, import_jsx_runtime43.jsxs)("span", { className: "block text-xs text-[var(--color-text-muted)]", children: [
2695
+ /* @__PURE__ */ (0, import_jsx_runtime43.jsxs)("span", { className: "block text-caption text-[var(--color-text-muted)]", children: [
2694
2696
  entry.actorPseudo ?? "Anonymous",
2695
2697
  " \xB7 ",
2696
2698
  dateFormat.format(new Date(entry.createdOn))
@@ -2708,15 +2710,8 @@ function SongHistory({ uuid, getBackendBaseUrl }) {
2708
2710
  }
2709
2711
  );
2710
2712
  }
2711
- function SongPanelContent({
2712
- song,
2713
- scope,
2714
- getBackendBaseUrl,
2715
- shareUrl,
2716
- renderArtistLink,
2717
- genrePath,
2718
- onSelectGenre
2719
- }) {
2713
+ function SongPanelContent(props) {
2714
+ const { song, scope, getBackendBaseUrl, shareUrl, renderArtistLink } = props;
2720
2715
  const { playNewTrackListFromTrackUuid } = (0, import_genre_tree26.useTrackList)();
2721
2716
  const { playerTrackObject, playState, handlePlayPauseAction } = (0, import_player11.usePlayer)();
2722
2717
  const unplayableReason = (0, import_genre_tree26.songUnplayableReason)(song);
@@ -2726,12 +2721,14 @@ function SongPanelContent({
2726
2721
  const musicBrainzUrl = (0, import_genre_tree26.musicBrainzRecordingUrl)(song);
2727
2722
  const fields = [];
2728
2723
  if (song.genre) {
2729
- const path = genrePath.length > 0 ? genrePath : null;
2724
+ const path = props.genrePath && props.genrePath.length > 0 ? props.genrePath : null;
2725
+ const { onSelectGenre } = props;
2726
+ const genre = song.genre;
2730
2727
  fields.push({
2731
2728
  label: "Genre",
2732
- value: path ? /* @__PURE__ */ (0, import_jsx_runtime43.jsx)("span", { className: "flex flex-wrap items-center gap-x-1", children: path.map((genrePlaylist, index) => /* @__PURE__ */ (0, import_jsx_runtime43.jsxs)("span", { children: [
2729
+ 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: [
2733
2730
  index > 0 && /* @__PURE__ */ (0, import_jsx_runtime43.jsx)("span", { "aria-hidden": "true", children: "\u203A " }),
2734
- /* @__PURE__ */ (0, import_jsx_runtime43.jsx)("button", { type: "button", onClick: () => onSelectGenre(genrePlaylist), className: "hover:underline", children: genrePlaylist.name })
2731
+ /* @__PURE__ */ (0, import_jsx_runtime43.jsx)("button", { type: "button", onClick: () => props.onSelectGenrePlaylist?.(genrePlaylist), className: "hover:underline", children: genrePlaylist.name })
2735
2732
  ] }, genrePlaylist.uuid)) }) : song.genre.name
2736
2733
  });
2737
2734
  } else {
@@ -2768,12 +2765,12 @@ function SongPanelContent({
2768
2765
  });
2769
2766
  return /* @__PURE__ */ (0, import_jsx_runtime43.jsxs)(import_jsx_runtime43.Fragment, { children: [
2770
2767
  /* @__PURE__ */ (0, import_jsx_runtime43.jsx)("div", { className: "genre-action-band border-b border-[var(--color-border)]", children: /* @__PURE__ */ (0, import_jsx_runtime43.jsxs)("div", { className: "flex flex-wrap gap-1 px-3.5 py-3", children: [
2771
- unplayableReason ? /* @__PURE__ */ (0, import_jsx_runtime43.jsx)("p", { className: "flex items-center px-2.5 text-sm text-[var(--color-text-muted)]", children: unplayableReason }) : /* @__PURE__ */ (0, import_jsx_runtime43.jsxs)(
2768
+ unplayableReason ? /* @__PURE__ */ (0, import_jsx_runtime43.jsx)("p", { className: "flex items-center px-2.5 text-body text-[var(--color-text-muted)]", children: unplayableReason }) : /* @__PURE__ */ (0, import_jsx_runtime43.jsxs)(
2772
2769
  "button",
2773
2770
  {
2774
2771
  type: "button",
2775
2772
  onClick: () => isCurrent ? handlePlayPauseAction() : playNewTrackListFromTrackUuid(song, scope),
2776
- className: "group flex flex-col items-center gap-1 w-16 text-xs text-center text-[var(--color-text)] focus-visible:outline-none",
2773
+ className: "group flex flex-col items-center gap-1 w-16 text-caption text-center text-[var(--color-text)] focus-visible:outline-none",
2777
2774
  children: [
2778
2775
  /* @__PURE__ */ (0, import_jsx_runtime43.jsx)("span", { className: "flex items-center justify-center w-10 h-10 rounded-full group-focus-visible:ring-2 group-focus-visible:ring-[var(--color-focus-ring)] bg-[var(--color-surface-muted)] transition-colors group-hover:bg-[var(--color-border)]", children: isPlaying ? /* @__PURE__ */ (0, import_jsx_runtime43.jsx)(import_lucide_react15.Pause, { "aria-hidden": "true", className: "w-5 h-5" }) : /* @__PURE__ */ (0, import_jsx_runtime43.jsx)(import_lucide_react15.Play, { "aria-hidden": "true", className: "w-5 h-5" }) }),
2779
2776
  isPlaying ? "Pause" : "Play"
@@ -2797,11 +2794,10 @@ function useSongPanel({
2797
2794
  getBackendBaseUrl,
2798
2795
  getShareUrl,
2799
2796
  renderArtistLink,
2800
- getGenrePath,
2801
- onSelectGenre,
2802
2797
  expanded,
2803
2798
  onExpandedChange,
2804
- onClose
2799
+ onClose,
2800
+ ...navigation
2805
2801
  }) {
2806
2802
  const result = (0, import_genre_tree27.useSong)(uuid, getBackendBaseUrl);
2807
2803
  const panel = { expanded, onExpandedChange, onClose };
@@ -2845,8 +2841,10 @@ function useSongPanel({
2845
2841
  getBackendBaseUrl,
2846
2842
  shareUrl: shareUrl ?? void 0,
2847
2843
  renderArtistLink,
2848
- genrePath: song.genre ? getGenrePath(song.genre) : [],
2849
- onSelectGenre
2844
+ ...navigation.getGenrePath ? {
2845
+ genrePath: song.genre ? navigation.getGenrePath(song.genre) : [],
2846
+ onSelectGenrePlaylist: navigation.onSelectGenrePlaylist
2847
+ } : { onSelectGenre: navigation.onSelectGenre }
2850
2848
  }
2851
2849
  )
2852
2850
  };
@@ -2854,6 +2852,37 @@ function useSongPanel({
2854
2852
  }
2855
2853
  }
2856
2854
 
2855
+ // src/genre-tree/useEntityPanel.tsx
2856
+ function useEntityPanel({
2857
+ scope,
2858
+ getBackendBaseUrl,
2859
+ getArtistBackendBaseUrl,
2860
+ getShareUrl,
2861
+ renderArtistLink,
2862
+ renderSongLink,
2863
+ song,
2864
+ artist,
2865
+ ...navigation
2866
+ }) {
2867
+ const songPanel = useSongPanel({
2868
+ ...song,
2869
+ scope,
2870
+ getBackendBaseUrl,
2871
+ getShareUrl,
2872
+ renderArtistLink,
2873
+ ...navigation
2874
+ });
2875
+ const artistPanel = useArtistPanel({
2876
+ ...artist,
2877
+ scope,
2878
+ getBackendBaseUrl: getArtistBackendBaseUrl ?? getBackendBaseUrl,
2879
+ getShareUrl,
2880
+ renderArtistLink,
2881
+ renderSongLink
2882
+ });
2883
+ return songPanel ?? artistPanel;
2884
+ }
2885
+
2857
2886
  // src/genre-tree/SizeMetricToggle.tsx
2858
2887
  var import_md2 = require("react-icons/md");
2859
2888
  var import_ui19 = require("@behindthemusictree/ui");
@@ -3036,20 +3065,6 @@ function GenreTreeView({
3036
3065
  const [expandedArtistUuid, setExpandedArtistUuid] = (0, import_react18.useState)(null);
3037
3066
  const artistBaseUrl = artistSearchBaseUrl ?? getBackendBaseUrl;
3038
3067
  const songBaseUrl = getBackendBaseUrl;
3039
- const artistPanel = useArtistPanel({
3040
- uuid: artistUuid,
3041
- scope,
3042
- getBackendBaseUrl: artistBaseUrl,
3043
- getShareUrl: getShareUrlRef.current,
3044
- renderArtistLink: renderArtistLink ?? ((artist) => artist.isSpecialPurpose ? artist.name : /* @__PURE__ */ (0, import_jsx_runtime46.jsx)("button", { type: "button", onClick: () => openArtist(artist.uuid), className: "hover:underline", children: artist.name })),
3045
- expanded: controlledArtistExpanded ?? (artistUuid !== null && expandedArtistUuid === artistUuid),
3046
- onExpandedChange: (on) => {
3047
- if (controlledArtistExpanded === void 0) setExpandedArtistUuid(on ? artistUuid : null);
3048
- onArtistExpandedChange?.(on);
3049
- },
3050
- renderSongLink: resolvedRenderSongLink,
3051
- onClose: () => changeArtist(null)
3052
- });
3053
3068
  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 }));
3054
3069
  const getGenrePath = (genre) => {
3055
3070
  const path = [];
@@ -3061,27 +3076,38 @@ function GenreTreeView({
3061
3076
  }
3062
3077
  return path;
3063
3078
  };
3064
- const songPanel = useSongPanel({
3065
- uuid: songUuid,
3079
+ const entityPanel = useEntityPanel({
3066
3080
  scope,
3067
3081
  getBackendBaseUrl: songBaseUrl,
3082
+ getArtistBackendBaseUrl: artistBaseUrl,
3068
3083
  getShareUrl: getShareUrlRef.current,
3069
3084
  renderArtistLink: resolvedRenderArtistLink,
3085
+ renderSongLink: resolvedRenderSongLink,
3086
+ song: {
3087
+ uuid: songUuid,
3088
+ expanded: controlledSongExpanded ?? (songUuid !== null && expandedSongUuid === songUuid),
3089
+ onExpandedChange: (on) => {
3090
+ if (controlledSongExpanded === void 0) setExpandedSongUuid(on ? songUuid : null);
3091
+ onSongExpandedChange?.(on);
3092
+ },
3093
+ onClose: () => changeSong(null)
3094
+ },
3095
+ artist: {
3096
+ uuid: artistUuid,
3097
+ expanded: controlledArtistExpanded ?? (artistUuid !== null && expandedArtistUuid === artistUuid),
3098
+ onExpandedChange: (on) => {
3099
+ if (controlledArtistExpanded === void 0) setExpandedArtistUuid(on ? artistUuid : null);
3100
+ onArtistExpandedChange?.(on);
3101
+ },
3102
+ onClose: () => changeArtist(null)
3103
+ },
3070
3104
  getGenrePath,
3071
- onSelectGenre: (genrePlaylist2) => {
3105
+ onSelectGenrePlaylist: (genrePlaylist2) => {
3072
3106
  changeSong(null);
3073
3107
  selectGenrePlaylist(genrePlaylist2);
3074
- },
3075
- expanded: controlledSongExpanded ?? (songUuid !== null && expandedSongUuid === songUuid),
3076
- onExpandedChange: (on) => {
3077
- if (controlledSongExpanded === void 0) setExpandedSongUuid(on ? songUuid : null);
3078
- onSongExpandedChange?.(on);
3079
- },
3080
- onClose: () => changeSong(null)
3108
+ }
3081
3109
  });
3082
- const builtInSongPanel = overlayPanel || selectedGenreUuid !== null ? void 0 : songPanel;
3083
- const builtInArtistPanel = overlayPanel || selectedGenreUuid !== null || builtInSongPanel ? void 0 : artistPanel;
3084
- const builtInPanel = builtInSongPanel ?? builtInArtistPanel;
3110
+ const builtInPanel = overlayPanel || selectedGenreUuid !== null ? void 0 : entityPanel;
3085
3111
  const [uncontrolledCollapsed, setUncontrolledCollapsed] = (0, import_react18.useState)(false);
3086
3112
  const [uncontrolledExpanded, setUncontrolledExpanded] = (0, import_react18.useState)(false);
3087
3113
  const isCollapsed = collapsed ?? uncontrolledCollapsed;
@@ -3252,7 +3278,7 @@ function GenreTreeView({
3252
3278
  ) : null;
3253
3279
  const renderArtistFilter = () => {
3254
3280
  if (isLoading || !filterArtist) return null;
3255
- return /* @__PURE__ */ (0, import_jsx_runtime46.jsxs)("span", { className: "inline-flex items-center overflow-hidden rounded-full bg-[var(--color-neutral-100)] text-sm", children: [
3281
+ 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: [
3256
3282
  /* @__PURE__ */ (0, import_jsx_runtime46.jsx)(
3257
3283
  "button",
3258
3284
  {
@@ -3363,14 +3389,14 @@ function GenreTreeView({
3363
3389
  /* @__PURE__ */ (0, import_jsx_runtime46.jsx)(
3364
3390
  "div",
3365
3391
  {
3366
- className: `actions-container absolute z-30 flex items-center gap-2 ${placement === "regions" ? "left-[calc(var(--layout-gutter)+var(--layout-panel-inset))] top-[calc(var(--layout-gutter)+var(--layout-panel-inset))] max-md:right-[calc(var(--layout-gutter)+var(--layout-panel-inset))] max-md:top-[var(--layout-band-top)] max-md:flex-col max-md:items-stretch" : "left-5 top-5"}`,
3392
+ 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"}`,
3367
3393
  children: actions
3368
3394
  }
3369
3395
  ),
3370
3396
  !isLoading && !isError && filterArtist && /* @__PURE__ */ (0, import_jsx_runtime46.jsx)(
3371
3397
  "div",
3372
3398
  {
3373
- className: `absolute left-1/2 z-30 hidden -translate-x-1/2 md:block ${placement === "regions" ? "top-[calc(var(--layout-gutter)+var(--layout-panel-inset))]" : "top-5"}`,
3399
+ 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"}`,
3374
3400
  children: renderArtistFilter()
3375
3401
  }
3376
3402
  ),
@@ -3640,7 +3666,7 @@ function TrackListSidebar({
3640
3666
  /* @__PURE__ */ (0, import_jsx_runtime47.jsx)("div", { className: "from h-auto flex flex-col justify-center items-center mr-2", children: "From" }),
3641
3667
  /* @__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 }) })
3642
3668
  ] }),
3643
- /* @__PURE__ */ (0, import_jsx_runtime47.jsxs)("div", { className: "info flex items-center text-sm text-[var(--color-text-muted)]", children: [
3669
+ /* @__PURE__ */ (0, import_jsx_runtime47.jsxs)("div", { className: "info flex items-center text-body text-[var(--color-text-muted)]", children: [
3644
3670
  trackList.origin.type === import_genre_tree30.TrackListOriginType.GENRE_PLAYLIST ? "Genre \u2022 " : trackList.origin.type === import_genre_tree30.TrackListOriginType.ARTIST ? "Artist \u2022 " : `${trackNoun.singular} playlist \u2022 `,
3645
3671
  `${trackList.total} ${trackList.total > 1 ? trackNoun.plural : trackNoun.singular}`
3646
3672
  ] })
@@ -3950,12 +3976,76 @@ function Rating({ rating, handleChange }) {
3950
3976
  ) })
3951
3977
  ] }, i)) });
3952
3978
  }
3979
+
3980
+ // src/genre-tree/EntityDetailPanel.tsx
3981
+ var import_react21 = require("react");
3982
+ var import_ui22 = require("@behindthemusictree/ui");
3983
+ var import_jsx_runtime51 = require("react/jsx-runtime");
3984
+ function EntityDetailPanel({
3985
+ scope,
3986
+ getBackendBaseUrl,
3987
+ getArtistBackendBaseUrl,
3988
+ getShareUrl,
3989
+ songUuid,
3990
+ onSongUuidChange,
3991
+ artistUuid,
3992
+ onArtistUuidChange,
3993
+ renderArtistLink,
3994
+ renderSongLink,
3995
+ collapsed,
3996
+ edgeTab,
3997
+ ...navigation
3998
+ }) {
3999
+ const [expandedSongUuid, setExpandedSongUuid] = (0, import_react21.useState)(null);
4000
+ const [expandedArtistUuid, setExpandedArtistUuid] = (0, import_react21.useState)(null);
4001
+ const panel = useEntityPanel({
4002
+ scope,
4003
+ getBackendBaseUrl,
4004
+ getArtistBackendBaseUrl,
4005
+ getShareUrl,
4006
+ 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 })),
4007
+ renderSongLink: renderSongLink ?? ((song) => /* @__PURE__ */ (0, import_jsx_runtime51.jsx)("button", { type: "button", onClick: () => onSongUuidChange(song.uuid), className: "hover:underline", children: song.title })),
4008
+ song: {
4009
+ uuid: songUuid,
4010
+ expanded: songUuid !== null && expandedSongUuid === songUuid,
4011
+ onExpandedChange: (on) => setExpandedSongUuid(on ? songUuid : null),
4012
+ onClose: () => onSongUuidChange(null)
4013
+ },
4014
+ artist: {
4015
+ uuid: artistUuid,
4016
+ expanded: artistUuid !== null && expandedArtistUuid === artistUuid,
4017
+ onExpandedChange: (on) => setExpandedArtistUuid(on ? artistUuid : null),
4018
+ onClose: () => onArtistUuidChange(null)
4019
+ },
4020
+ ...navigation
4021
+ });
4022
+ if (!panel) return null;
4023
+ return /* @__PURE__ */ (0, import_jsx_runtime51.jsx)(
4024
+ import_ui22.DetailPanel,
4025
+ {
4026
+ side: "left",
4027
+ title: panel.title,
4028
+ titleInSearch: true,
4029
+ onClose: panel.onClose,
4030
+ collapsed,
4031
+ expanded: panel.expanded,
4032
+ onExpandedChange: panel.onExpandedChange,
4033
+ aside: panel.extraContent,
4034
+ edgeTab,
4035
+ children: panel.extraContent && !panel.expanded ? /* @__PURE__ */ (0, import_jsx_runtime51.jsxs)(import_jsx_runtime51.Fragment, { children: [
4036
+ panel.content,
4037
+ panel.extraContent
4038
+ ] }) : panel.content
4039
+ }
4040
+ );
4041
+ }
3953
4042
  // Annotate the CommonJS export names for ESM import in node:
3954
4043
  0 && (module.exports = {
3955
4044
  AuthCallbackHandler,
3956
4045
  AuthErrorPopup,
3957
4046
  AuthPopup,
3958
4047
  BasePopup,
4048
+ EntityDetailPanel,
3959
4049
  GenreDetailPanel,
3960
4050
  GenreInfoPanelBody,
3961
4051
  GenreNodeActionBand,
@@ -3982,6 +4072,7 @@ function Rating({ rating, handleChange }) {
3982
4072
  TrackUploadPopup,
3983
4073
  searchStatus,
3984
4074
  useArtistSearchGroup,
4075
+ useEntityPanel,
3985
4076
  useGenrePlaylistPlayback,
3986
4077
  useGenreRemoteSearchGroup,
3987
4078
  useSongSearchGroup