@behindthemusictree/app-kit-ui 34.1.0 → 36.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.
@@ -5,14 +5,12 @@ import {
5
5
  } from "./chunk-DKQQLLMY.js";
6
6
 
7
7
  // src/genre-tree/GenreTreeView.tsx
8
- import { useCallback as useCallback6, useDeferredValue as useDeferredValue2, useEffect as useEffect5, useMemo as useMemo7, useRef as useRef3, useState as useState9 } from "react";
8
+ import { useCallback as useCallback6, useDeferredValue as useDeferredValue2, useEffect as useEffect5, useMemo as useMemo8, useRef as useRef3, useState as useState9 } from "react";
9
9
  import { useIsMutating } from "@tanstack/react-query";
10
10
  import { Plus as Plus2 } from "lucide-react";
11
- import { Maximize2, Minimize2 } from "lucide-react";
12
- import { IconTextButton, IconButton, Button, SegmentedControl as SegmentedControl2, DetailPanel, useObscuredInset } from "@behindthemusictree/ui";
11
+ import { Maximize2 as Maximize22, Minimize2 as Minimize22 } from "lucide-react";
12
+ import { IconTextButton, IconButton as IconButton2, Button, SegmentedControl as SegmentedControl2, DetailPanel as DetailPanel2, useObscuredInset } from "@behindthemusictree/ui";
13
13
  import {
14
- GenreNodeAncestors,
15
- GenreNodeFamily,
16
14
  GenreTreeWheelSkeleton,
17
15
  GenreTreeViewSkeleton,
18
16
  useGenreNodeRelations
@@ -683,6 +681,7 @@ function GenreTreeSearch({
683
681
  artistSearch,
684
682
  songSearch,
685
683
  SearchGroups,
684
+ onSelectGenre,
686
685
  extraGroups = [],
687
686
  onQueryChange,
688
687
  onFocus,
@@ -708,9 +707,30 @@ function GenreTreeSearch({
708
707
  extraGroups: [...extraGroups, ...hostGroups, artistGroup, songGroup].filter((group) => group !== void 0)
709
708
  }
710
709
  );
711
- return SearchGroups ? /* @__PURE__ */ jsx9(SearchGroups, { query: deferredQuery, focused, children: render }) : render([]);
710
+ return SearchGroups ? /* @__PURE__ */ jsx9(SearchGroups, { query: deferredQuery, focused, onSelectGenre, children: render }) : render([]);
712
711
  }
713
712
 
713
+ // src/genre-tree/GenreDetailPanel.tsx
714
+ import { useMemo as useMemo6 } from "react";
715
+ import {
716
+ ACCENT_TEXT_COLOR,
717
+ GenreNodeAncestors,
718
+ GenreNodeFamily,
719
+ getGenreTreeColor as getGenreTreeColor2,
720
+ TEXT_COLOR,
721
+ tintSurface
722
+ } from "@behindthemusictree/genre-tree-view";
723
+ import { DetailPanel, IconButton, Skeleton as Skeleton3 } from "@behindthemusictree/ui";
724
+ import { Maximize2, Minimize2 } from "lucide-react";
725
+ import {
726
+ useFetchGenreDetail,
727
+ genreMusicCount as genreMusicCount2,
728
+ GenrePlaylistSummarySchema,
729
+ GenreCatalogueCountsSchema as GenreCatalogueCountsSchema3,
730
+ CriteriaMinimumSchema,
731
+ GenreUuidSchema
732
+ } from "@behindthemusictree/app-kit/genre-tree";
733
+
714
734
  // src/genre-tree/GenreInfoPanelBody.tsx
715
735
  import {
716
736
  genreMusicCount,
@@ -1076,7 +1096,7 @@ import { z } from "zod";
1076
1096
  import { FaPlay, FaPause } from "react-icons/fa";
1077
1097
  import { PanelSection, RingLoader, Skeleton } from "@behindthemusictree/ui";
1078
1098
  import { useSession } from "@behindthemusictree/app-kit/auth";
1079
- import { usePlaybackContext, useFetchWrapper } from "@behindthemusictree/app-kit/transport";
1099
+ import { usePlaybackContext, usePlaybackFetch } from "@behindthemusictree/app-kit/transport";
1080
1100
  import { PaginatedResponseSchema } from "@behindthemusictree/app-kit/transport";
1081
1101
  import { parseWithLog } from "@behindthemusictree/app-kit/transport";
1082
1102
  import { usePlayer as usePlayer4 } from "@behindthemusictree/app-kit/player";
@@ -1136,6 +1156,7 @@ var nextCursor = (next) => {
1136
1156
  function GenrePlaylistTracks({
1137
1157
  genreUuid,
1138
1158
  genrePlaylist,
1159
+ genreName: selectedGenreName,
1139
1160
  selection,
1140
1161
  scope,
1141
1162
  getBackendBaseUrl,
@@ -1151,14 +1172,18 @@ function GenrePlaylistTracks({
1151
1172
  const { playerTrackObject, playState, handlePlayPauseAction } = usePlayer4();
1152
1173
  const { showPopup } = usePopup4();
1153
1174
  const trackNoun = useTrackNoun();
1154
- const { uuid } = genrePlaylist;
1155
- const { fetch } = useFetchWrapper(getBackendBaseUrl);
1175
+ const uuid = genrePlaylist?.uuid;
1176
+ const genreName = selectedGenreName ?? genrePlaylist?.name ?? "Genre";
1177
+ const { fetchBound } = usePlaybackFetch(getBackendBaseUrl);
1156
1178
  const playback = usePlaybackContext();
1157
1179
  const { data, isPending, isError, refetch, hasNextPage, isFetchingNextPage, fetchNextPage } = useInfiniteQuery({
1158
- queryKey: [...scope === "me" ? genrePlaylistQueryKeys.me.tracks(uuid) : artist ? [...libraryQueryKeys.reference.songs.byGenre(getBackendBaseUrl(), genreUuid, selection), "artist", artist.uuid] : libraryQueryKeys.reference.songs.byGenre(getBackendBaseUrl(), genreUuid, selection), ...playback.key ? [playback.key] : []],
1180
+ queryKey: [...scope === "me" ? uuid ? genrePlaylistQueryKeys.me.tracks(uuid) : ["missing-personal-genre-playlist", genreUuid] : artist ? [...libraryQueryKeys.reference.songs.byGenre(getBackendBaseUrl(), genreUuid, selection), "artist", artist.uuid] : libraryQueryKeys.reference.songs.byGenre(getBackendBaseUrl(), genreUuid, selection), ...playback.key ? [playback.key] : []],
1159
1181
  queryFn: async ({ pageParam }) => {
1160
- if (scope === "me") return fetchGenrePlaylistTracksPage(uuid, scope, Number(pageParam));
1161
- const response = await fetch(
1182
+ if (scope === "me") {
1183
+ if (!uuid) throw new Error("Personal genre has no playlist");
1184
+ return fetchGenrePlaylistTracksPage(uuid, scope, Number(pageParam));
1185
+ }
1186
+ const response = await fetchBound(
1162
1187
  libraryEndpoints.reference.songs.list(),
1163
1188
  true,
1164
1189
  false,
@@ -1170,7 +1195,9 @@ function GenrePlaylistTracks({
1170
1195
  },
1171
1196
  initialPageParam: scope === "me" ? 1 : "",
1172
1197
  getNextPageParam: (lastPage) => lastPage.nextPage ?? void 0,
1173
- enabled: playback.ready && (scope !== "me" || sessionRestored && !!session?.accessToken)
1198
+ enabled: playback.ready && (scope !== "me" || uuid !== void 0 && sessionRestored && !!session?.accessToken),
1199
+ retry: (failureCount, error) => failureCount < 1 && error instanceof DOMException && error.name === "AbortError",
1200
+ retryDelay: 0
1174
1201
  });
1175
1202
  const [sentinel, setSentinel] = useState6(null);
1176
1203
  useEffect3(() => {
@@ -1192,7 +1219,7 @@ function GenrePlaylistTracks({
1192
1219
  ordering: "title",
1193
1220
  genreUuid,
1194
1221
  ...selection && { selection }
1195
- }) : scope === "reference" ? playNewTrackListFromGenreCatalogue({ uuid: genreUuid, name: genrePlaylist.name }, scope, { startSongUuid, ...selection && { selection } }) : playNewTrackListFromGenrePlaylist(genrePlaylist, scope, { startSongUuid });
1222
+ }) : scope === "reference" ? playNewTrackListFromGenreCatalogue({ uuid: genreUuid, name: genreName }, scope, { startSongUuid, ...selection && { selection } }) : genrePlaylist ? playNewTrackListFromGenrePlaylist(genrePlaylist, scope, { startSongUuid }) : Promise.reject(new Error("Personal genre has no playlist"));
1196
1223
  started.catch((error) => {
1197
1224
  console.error("Failed to play genre playlist:", error);
1198
1225
  showPopup(/* @__PURE__ */ jsx17(InternalErrorPopup, { errorCode: ErrorCode3.CLIENT_UNKNOWN }));
@@ -1200,13 +1227,13 @@ function GenrePlaylistTracks({
1200
1227
  };
1201
1228
  return /* @__PURE__ */ jsx17(PanelSection, { title: `${capitalize(trackNoun.plural)}${total !== void 0 ? ` (${total})` : ""}`, children: /* @__PURE__ */ jsxs12("div", { children: [
1202
1229
  renderTracksToolbar?.(),
1203
- isPending ? /* @__PURE__ */ jsx17(Skeleton, { className: "h-4 w-3/4", "aria-busy": "true" }) : isError && tracks.length === 0 ? /* @__PURE__ */ jsxs12("p", { role: "alert", children: [
1230
+ scope === "me" && !genrePlaylist ? /* @__PURE__ */ jsx17("p", { children: "\u2014" }) : isPending ? /* @__PURE__ */ jsx17(Skeleton, { className: "h-4 w-3/4", "aria-busy": "true" }) : isError && tracks.length === 0 ? /* @__PURE__ */ jsxs12("p", { role: "alert", children: [
1204
1231
  "Couldn't load ",
1205
1232
  trackNoun.plural,
1206
1233
  ".",
1207
1234
  " ",
1208
1235
  /* @__PURE__ */ jsx17("button", { type: "button", className: "underline", onClick: () => void refetch(), children: "Retry" })
1209
- ] }) : tracks.length === 0 ? /* @__PURE__ */ jsx17("p", { children: "\u2014" }) : /* @__PURE__ */ jsxs12("ul", { className: "mt-1 max-h-64 overflow-y-auto text-[0.8125rem] leading-[1.45] [container-type:inline-size]", "aria-label": `${genrePlaylist.name} ${trackNoun.plural}`, children: [
1236
+ ] }) : tracks.length === 0 ? /* @__PURE__ */ jsx17("p", { children: "\u2014" }) : /* @__PURE__ */ jsxs12("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: [
1210
1237
  tracks.map((track, index) => {
1211
1238
  const label = track.artists?.length ? `${track.title} \u2014 ${track.artists.map((artist2) => artist2.name).join(", ")}` : track.title;
1212
1239
  const isCurrent = playerTrackObject?.track.id === track.uuid;
@@ -1273,6 +1300,7 @@ import { Fragment as Fragment8, jsx as jsx18, jsxs as jsxs13 } from "react/jsx-r
1273
1300
  function GenreDetailExtras({
1274
1301
  genreUuid,
1275
1302
  genrePlaylist,
1303
+ genreName,
1276
1304
  selection,
1277
1305
  wikidataId,
1278
1306
  scope,
@@ -1340,6 +1368,7 @@ function GenreDetailExtras({
1340
1368
  {
1341
1369
  genreUuid,
1342
1370
  genrePlaylist,
1371
+ genreName,
1343
1372
  selection,
1344
1373
  scope,
1345
1374
  getBackendBaseUrl,
@@ -1381,6 +1410,7 @@ function GenreInfoPanelBody({
1381
1410
  genreUuid,
1382
1411
  genrePlaylist,
1383
1412
  catalogue,
1413
+ countsContext,
1384
1414
  wikidataId,
1385
1415
  scope,
1386
1416
  getBackendBaseUrl,
@@ -1389,7 +1419,7 @@ function GenreInfoPanelBody({
1389
1419
  extras
1390
1420
  }) {
1391
1421
  const noun = useTrackNoun3();
1392
- const summary = scope === "reference" ? GenreCatalogueCountsSchema2.parse(genrePlaylist) : void 0;
1422
+ const summary = scope === "reference" ? GenreCatalogueCountsSchema2.parse(genrePlaylist ?? catalogue) : void 0;
1393
1423
  const selection = summary ? defaultGenreMusicSelection2(summary) : void 0;
1394
1424
  const describeCounts = (counts) => {
1395
1425
  const scopes = counts.genreAndSubgenresTracksCount;
@@ -1397,17 +1427,19 @@ function GenreInfoPanelBody({
1397
1427
  };
1398
1428
  return /* @__PURE__ */ jsxs14(Fragment9, { children: [
1399
1429
  toolbar,
1400
- /* @__PURE__ */ jsx19(GenreNodeActionBand, { ...band, genrePlaylist, playback }),
1430
+ genrePlaylist ? /* @__PURE__ */ jsx19(GenreNodeActionBand, { ...band, genrePlaylist, playback }) : /* @__PURE__ */ jsx19(GenreActionBand, { ...band }),
1401
1431
  summary && catalogue && /* @__PURE__ */ jsxs14("div", { className: "px-3.5 py-3 text-sm text-[var(--color-text)]", children: [
1402
1432
  /* @__PURE__ */ jsxs14("p", { children: [
1403
1433
  "Catalogue ",
1404
1434
  noun.plural,
1435
+ countsContext ? ` (${countsContext})` : "",
1405
1436
  ": ",
1406
1437
  describeCounts(catalogue)
1407
1438
  ] }),
1408
- /* @__PURE__ */ jsxs14("p", { children: [
1439
+ genrePlaylist && /* @__PURE__ */ jsxs14("p", { children: [
1409
1440
  "Playlist ",
1410
1441
  noun.plural,
1442
+ countsContext ? ` (${countsContext})` : "",
1411
1443
  ": ",
1412
1444
  describeCounts(summary)
1413
1445
  ] })
@@ -1420,6 +1452,7 @@ function GenreInfoPanelBody({
1420
1452
  family,
1421
1453
  genreUuid,
1422
1454
  genrePlaylist,
1455
+ genreName: band.node.name,
1423
1456
  selection,
1424
1457
  wikidataId,
1425
1458
  scope,
@@ -1429,13 +1462,163 @@ function GenreInfoPanelBody({
1429
1462
  ] });
1430
1463
  }
1431
1464
 
1465
+ // src/genre-tree/GenreDetailPanel.tsx
1466
+ import { Fragment as Fragment10, jsx as jsx20, jsxs as jsxs15 } from "react/jsx-runtime";
1467
+ var RELATIVE_ITEM_COUNT = 1;
1468
+ function toNode(genre, parentId) {
1469
+ return {
1470
+ id: genre.uuid,
1471
+ parentId,
1472
+ name: genre.name,
1473
+ itemCount: RELATIVE_ITEM_COUNT,
1474
+ side: genre.side ?? void 0,
1475
+ wikidataId: genre.wikidataId
1476
+ };
1477
+ }
1478
+ function useGenreDetailContent({
1479
+ genreUuid,
1480
+ scope,
1481
+ getBackendBaseUrl,
1482
+ onSelectGenre,
1483
+ extras,
1484
+ band,
1485
+ playback: hostPlayback,
1486
+ trailing,
1487
+ genrePlaylist: hostPlaylist,
1488
+ expanded
1489
+ }) {
1490
+ const { data: detail, isPending, isError } = useFetchGenreDetail(genreUuid, scope, getBackendBaseUrl);
1491
+ const defaultPlayback = useGenrePlaylistPlayback({ scope, genrePlaylists: [], artist: extras?.artist });
1492
+ const playback = hostPlayback ?? defaultPlayback;
1493
+ const view = useMemo6(() => {
1494
+ if (!detail) return null;
1495
+ const rootColor = getGenreTreeColor2(detail.root.wikidataId);
1496
+ const relative = (genre, parentId) => ({
1497
+ node: toNode(genre, parentId),
1498
+ fill: tintSurface(rootColor),
1499
+ textColor: TEXT_COLOR
1500
+ });
1501
+ const ancestors2 = [...detail.ascendants].sort((a, b) => b.degree - a.degree);
1502
+ return {
1503
+ node: {
1504
+ id: detail.uuid,
1505
+ parentId: detail.parent?.uuid ?? null,
1506
+ name: detail.name,
1507
+ itemCount: scope === "reference" ? genreMusicCount2(detail.criteriaPlaylist ? GenrePlaylistSummarySchema.parse(detail.criteriaPlaylist) : GenreCatalogueCountsSchema3.parse(detail.catalogue)) : detail.tracksCount,
1508
+ wikidataId: detail.wikidataId
1509
+ },
1510
+ fill: rootColor,
1511
+ parentNode: detail.parent ? relative(detail.parent, null) : null,
1512
+ childNodes: detail.children.map((child) => relative(child, detail.uuid)),
1513
+ ancestorNodes: ancestors2.map(
1514
+ ({ ascendant }, index) => relative(ascendant, index === 0 ? null : ancestors2[index - 1].ascendant.uuid)
1515
+ )
1516
+ };
1517
+ }, [detail, scope]);
1518
+ if (isError) return {
1519
+ title: "Genre unavailable",
1520
+ content: /* @__PURE__ */ jsx20("p", { role: "alert", className: "p-4 text-sm text-[var(--color-text)]", children: "This genre could not be loaded." })
1521
+ };
1522
+ if (isPending || !detail || !view) return {
1523
+ title: "Loading genre\u2026",
1524
+ content: /* @__PURE__ */ jsxs15("div", { className: "flex flex-col gap-3 p-4", "aria-busy": "true", "data-testid": "genre-detail-panel-skeleton", children: [
1525
+ /* @__PURE__ */ jsx20(Skeleton3, { className: "h-6 w-2/3" }),
1526
+ /* @__PURE__ */ jsx20(Skeleton3, { className: "h-4 w-full" }),
1527
+ /* @__PURE__ */ jsx20(Skeleton3, { className: "h-4 w-3/4" })
1528
+ ] })
1529
+ };
1530
+ const selectGenre = (nodeId) => onSelectGenre(GenreUuidSchema.parse(nodeId));
1531
+ const family = /* @__PURE__ */ jsx20(
1532
+ GenreNodeFamily,
1533
+ {
1534
+ node: view.node,
1535
+ fill: view.fill,
1536
+ textColor: ACCENT_TEXT_COLOR,
1537
+ parentNode: view.parentNode,
1538
+ childNodes: view.childNodes,
1539
+ onSelectNode: selectGenre
1540
+ }
1541
+ );
1542
+ const ancestors = /* @__PURE__ */ jsx20(GenreNodeAncestors, { ancestorNodes: view.ancestorNodes, onSelectNode: selectGenre });
1543
+ return {
1544
+ title: detail.name,
1545
+ headerAccent: { background: view.fill, color: ACCENT_TEXT_COLOR },
1546
+ aside: expanded ? ancestors : void 0,
1547
+ content: /* @__PURE__ */ jsxs15(Fragment10, { children: [
1548
+ /* @__PURE__ */ jsx20(
1549
+ GenreInfoPanelBody,
1550
+ {
1551
+ family: /* @__PURE__ */ jsxs15(Fragment10, { children: [
1552
+ family,
1553
+ !expanded && ancestors
1554
+ ] }),
1555
+ genreUuid: detail.uuid,
1556
+ genrePlaylist: hostPlaylist ?? (detail.criteriaPlaylist === null ? null : scope === "reference" ? { ...GenrePlaylistSummarySchema.parse(detail.criteriaPlaylist), criteria: CriteriaMinimumSchema.parse(detail) } : { ...detail.criteriaPlaylist, criteria: CriteriaMinimumSchema.parse(detail), tracksCount: detail.tracksCount }),
1557
+ catalogue: detail.catalogue,
1558
+ countsContext: !hostPlaylist && extras?.artist ? "all artists" : void 0,
1559
+ wikidataId: detail.wikidataId,
1560
+ scope,
1561
+ getBackendBaseUrl,
1562
+ playback,
1563
+ band: { node: view.node, ...band, shareTarget: band?.shareTarget?.kind === "genre" ? { ...band.shareTarget, name: detail.name } : band?.shareTarget },
1564
+ extras
1565
+ }
1566
+ ),
1567
+ trailing
1568
+ ] })
1569
+ };
1570
+ }
1571
+ function GenreDetailPanel({
1572
+ genreUuid,
1573
+ scope,
1574
+ getBackendBaseUrl,
1575
+ onClose,
1576
+ onSelectGenre,
1577
+ onZoomIn,
1578
+ shareUrl,
1579
+ shareTarget,
1580
+ collapsed,
1581
+ expanded,
1582
+ onExpandedChange,
1583
+ edgeTab,
1584
+ extras,
1585
+ headerClassName
1586
+ }) {
1587
+ const panel = useGenreDetailContent({
1588
+ genreUuid,
1589
+ scope,
1590
+ getBackendBaseUrl,
1591
+ onSelectGenre,
1592
+ extras,
1593
+ expanded,
1594
+ band: { onZoomIn, shareUrl, shareTarget: shareUrl ? shareTarget : void 0 }
1595
+ });
1596
+ return /* @__PURE__ */ jsx20(
1597
+ DetailPanel,
1598
+ {
1599
+ side: "left",
1600
+ title: panel.title,
1601
+ headerAccent: panel.headerAccent,
1602
+ onClose,
1603
+ collapsed,
1604
+ expanded,
1605
+ onExpandedChange,
1606
+ edgeTab,
1607
+ aside: panel.aside,
1608
+ headerClassName,
1609
+ headerActions: onExpandedChange ? /* @__PURE__ */ jsx20(IconButton, { "aria-label": expanded ? "Shrink panel" : "Extend panel", onClick: () => onExpandedChange(!expanded), children: expanded ? /* @__PURE__ */ jsx20(Minimize2, { "aria-hidden": "true", className: "h-4 w-4" }) : /* @__PURE__ */ jsx20(Maximize2, { "aria-hidden": "true", className: "h-4 w-4" }) }) : void 0,
1610
+ children: panel.content
1611
+ }
1612
+ );
1613
+ }
1614
+
1432
1615
  // src/genre-tree/ChangeParentPopup.tsx
1433
- import { useEffect as useEffect4, useMemo as useMemo6 } from "react";
1616
+ import { useEffect as useEffect4, useMemo as useMemo7 } from "react";
1434
1617
  import {
1435
1618
  getReparentCandidates,
1436
1619
  useUpdateGenre
1437
1620
  } from "@behindthemusictree/app-kit/genre-tree";
1438
- import { jsx as jsx20, jsxs as jsxs15 } from "react/jsx-runtime";
1621
+ import { jsx as jsx21, jsxs as jsxs16 } from "react/jsx-runtime";
1439
1622
  function ChangeParentPopup({
1440
1623
  genrePlaylist,
1441
1624
  genrePlaylists,
@@ -1445,7 +1628,7 @@ function ChangeParentPopup({
1445
1628
  onParentChanged
1446
1629
  }) {
1447
1630
  const { mutate, isPending, formErrors } = useUpdateGenre(scope, getBackendBaseUrl);
1448
- const candidates = useMemo6(
1631
+ const candidates = useMemo7(
1449
1632
  () => getReparentCandidates(genrePlaylists, genrePlaylist.uuid),
1450
1633
  [genrePlaylists, genrePlaylist.uuid]
1451
1634
  );
@@ -1470,12 +1653,12 @@ function ChangeParentPopup({
1470
1653
  window.addEventListener("keydown", handleKeyDown);
1471
1654
  return () => window.removeEventListener("keydown", handleKeyDown);
1472
1655
  }, [onClose]);
1473
- return /* @__PURE__ */ jsxs15(BasePopup_default, { title: `Change parent of ${genrePlaylist.name}`, onClose, showCancelButton: true, fitContent: true, children: [
1474
- /* @__PURE__ */ jsxs15("p", { className: "mb-3 text-sm text-[var(--color-text-muted)]", children: [
1656
+ return /* @__PURE__ */ jsxs16(BasePopup_default, { title: `Change parent of ${genrePlaylist.name}`, onClose, showCancelButton: true, fitContent: true, children: [
1657
+ /* @__PURE__ */ jsxs16("p", { className: "mb-3 text-sm text-[var(--color-text-muted)]", children: [
1475
1658
  "Current parent: ",
1476
1659
  genrePlaylist.parent?.name ?? "none"
1477
1660
  ] }),
1478
- /* @__PURE__ */ jsx20("div", { "aria-busy": isPending, className: isPending ? "pointer-events-none opacity-60" : void 0, children: /* @__PURE__ */ jsx20(
1661
+ /* @__PURE__ */ jsx21("div", { "aria-busy": isPending, className: isPending ? "pointer-events-none opacity-60" : void 0, children: /* @__PURE__ */ jsx21(
1479
1662
  GenreSearch,
1480
1663
  {
1481
1664
  genrePlaylists: candidates,
@@ -1483,12 +1666,12 @@ function ChangeParentPopup({
1483
1666
  placeholder: "Search a new parent\u2026"
1484
1667
  }
1485
1668
  ) }),
1486
- formErrors.length > 0 && /* @__PURE__ */ jsx20("p", { role: "alert", className: "mt-3 text-sm text-[var(--color-red-500)]", children: formErrors.map(({ message }) => message).join(" ") })
1669
+ formErrors.length > 0 && /* @__PURE__ */ jsx21("p", { role: "alert", className: "mt-3 text-sm text-[var(--color-red-500)]", children: formErrors.map(({ message }) => message).join(" ") })
1487
1670
  ] });
1488
1671
  }
1489
1672
 
1490
1673
  // src/genre-tree/artist/useArtistPanel.tsx
1491
- import { Skeleton as Skeleton4 } from "@behindthemusictree/ui";
1674
+ import { Skeleton as Skeleton5 } from "@behindthemusictree/ui";
1492
1675
  import { useArtist } from "@behindthemusictree/app-kit/genre-tree";
1493
1676
 
1494
1677
  // src/genre-tree/artist/ArtistHeader.tsx
@@ -1507,7 +1690,7 @@ import {
1507
1690
  SiYoutube
1508
1691
  } from "react-icons/si";
1509
1692
  import { Chip } from "@behindthemusictree/ui";
1510
- import { jsx as jsx21 } from "react/jsx-runtime";
1693
+ import { jsx as jsx22 } from "react/jsx-runtime";
1511
1694
  var LINK_KINDS = {
1512
1695
  official: { label: "Official website", Icon: Globe },
1513
1696
  wikipedia: { label: "Wikipedia", Icon: SiWikipedia2 },
@@ -1544,7 +1727,7 @@ function artistLinks(artist) {
1544
1727
  return links;
1545
1728
  }
1546
1729
  function ArtistLinks({ links }) {
1547
- return /* @__PURE__ */ jsx21("ul", { className: "flex flex-wrap gap-1.5", children: links.map(({ key, url, Icon, label }) => /* @__PURE__ */ jsx21("li", { children: /* @__PURE__ */ jsx21(Chip, { href: url, icon: /* @__PURE__ */ jsx21(Icon, { className: "h-3.5 w-3.5 shrink-0", "aria-hidden": "true" }), children: label }) }, key)) });
1730
+ return /* @__PURE__ */ jsx22("ul", { className: "flex flex-wrap gap-1.5", children: links.map(({ key, url, Icon, label }) => /* @__PURE__ */ jsx22("li", { children: /* @__PURE__ */ jsx22(Chip, { href: url, icon: /* @__PURE__ */ jsx22(Icon, { className: "h-3.5 w-3.5 shrink-0", "aria-hidden": "true" }), children: label }) }, key)) });
1548
1731
  }
1549
1732
 
1550
1733
  // src/genre-tree/artist/formatYears.ts
@@ -1555,7 +1738,7 @@ function formatYears(beginYear, endYear, ended) {
1555
1738
  }
1556
1739
 
1557
1740
  // src/genre-tree/artist/ArtistHeader.tsx
1558
- import { jsx as jsx22 } from "react/jsx-runtime";
1741
+ import { jsx as jsx23 } from "react/jsx-runtime";
1559
1742
  var countryNames = new Intl.DisplayNames(["en"], { type: "region" });
1560
1743
  function ArtistHeader({ artist }) {
1561
1744
  const fields = [];
@@ -1565,35 +1748,35 @@ function ArtistHeader({ artist }) {
1565
1748
  const years = formatYears(artist.beginYear, artist.endYear, artist.ended);
1566
1749
  if (years) fields.push({ label: "Active", value: years });
1567
1750
  const links = artistLinks(artist);
1568
- if (links.length > 0) fields.push({ label: "Links", value: /* @__PURE__ */ jsx22(ArtistLinks, { links }) });
1751
+ if (links.length > 0) fields.push({ label: "Links", value: /* @__PURE__ */ jsx23(ArtistLinks, { links }) });
1569
1752
  if (fields.length === 0) return null;
1570
- return /* @__PURE__ */ jsx22(FieldList, { items: fields });
1753
+ return /* @__PURE__ */ jsx23(FieldList, { items: fields });
1571
1754
  }
1572
1755
 
1573
1756
  // src/genre-tree/artist/ArtistPhoto.tsx
1574
- import { jsx as jsx23, jsxs as jsxs16 } from "react/jsx-runtime";
1757
+ import { jsx as jsx24, jsxs as jsxs17 } from "react/jsx-runtime";
1575
1758
  function ExternalLink({ href, children }) {
1576
- return /* @__PURE__ */ jsx23("a", { href, target: "_blank", rel: "noopener noreferrer", className: "underline-offset-2 hover:underline", children });
1759
+ return /* @__PURE__ */ jsx24("a", { href, target: "_blank", rel: "noopener noreferrer", className: "underline-offset-2 hover:underline", children });
1577
1760
  }
1578
1761
  function ArtistPhoto({ image, name }) {
1579
- return /* @__PURE__ */ jsxs16("figure", { className: "flex flex-col gap-1.5 px-6 py-2.5", children: [
1580
- /* @__PURE__ */ jsx23("img", { src: image.url, alt: name, className: "h-64 w-full object-contain bg-[var(--color-surface-muted)] rounded" }),
1581
- /* @__PURE__ */ jsxs16("figcaption", { className: "text-xs text-[var(--color-text-muted)]", children: [
1762
+ return /* @__PURE__ */ jsxs17("figure", { className: "flex flex-col gap-1.5 px-6 py-2.5", children: [
1763
+ /* @__PURE__ */ jsx24("img", { src: image.url, alt: name, className: "h-64 w-full object-contain bg-[var(--color-surface-muted)] rounded" }),
1764
+ /* @__PURE__ */ jsxs17("figcaption", { className: "text-xs text-[var(--color-text-muted)]", children: [
1582
1765
  "Photo: ",
1583
- /* @__PURE__ */ jsx23(ExternalLink, { href: image.pageUrl, children: image.author ?? "Wikimedia Commons" }),
1766
+ /* @__PURE__ */ jsx24(ExternalLink, { href: image.pageUrl, children: image.author ?? "Wikimedia Commons" }),
1584
1767
  ",",
1585
1768
  " ",
1586
- image.licenseUrl ? /* @__PURE__ */ jsx23(ExternalLink, { href: image.licenseUrl, children: image.license }) : image.license
1769
+ image.licenseUrl ? /* @__PURE__ */ jsx24(ExternalLink, { href: image.licenseUrl, children: image.license }) : image.license
1587
1770
  ] })
1588
1771
  ] });
1589
1772
  }
1590
1773
 
1591
1774
  // src/genre-tree/artist/ArtistMembers.tsx
1592
1775
  import { PanelGroup, PanelSection as PanelSection3 } from "@behindthemusictree/ui";
1593
- import { jsx as jsx24, jsxs as jsxs17 } from "react/jsx-runtime";
1776
+ import { jsx as jsx25, jsxs as jsxs18 } from "react/jsx-runtime";
1594
1777
  function MembershipList({ title, memberships, renderArtistLink }) {
1595
1778
  if (memberships.length === 0) return null;
1596
- return /* @__PURE__ */ jsx24(PanelGroup, { title, children: /* @__PURE__ */ jsx24("ul", { children: memberships.map((membership, index) => {
1779
+ return /* @__PURE__ */ jsx25(PanelGroup, { title, children: /* @__PURE__ */ jsx25("ul", { children: memberships.map((membership, index) => {
1597
1780
  const details = [
1598
1781
  formatYears(membership.beginYear, membership.endYear, membership.ended),
1599
1782
  ...membership.roles
@@ -1601,9 +1784,9 @@ function MembershipList({ title, memberships, renderArtistLink }) {
1601
1784
  const detailsText = details.filter(Boolean).join(" \xB7 ");
1602
1785
  return (
1603
1786
  // A member can have several periods in the same group.
1604
- /* @__PURE__ */ jsxs17("li", { className: "truncate py-0.5", title: detailsText || void 0, children: [
1605
- /* @__PURE__ */ jsx24("span", { className: "font-medium text-[var(--color-text)]", children: renderArtistLink(membership.artist) }),
1606
- detailsText && /* @__PURE__ */ jsxs17("span", { className: "text-[var(--color-text-muted)]", children: [
1787
+ /* @__PURE__ */ jsxs18("li", { className: "truncate py-0.5", title: detailsText || void 0, children: [
1788
+ /* @__PURE__ */ jsx25("span", { className: "font-medium text-[var(--color-text)]", children: renderArtistLink(membership.artist) }),
1789
+ detailsText && /* @__PURE__ */ jsxs18("span", { className: "text-[var(--color-text-muted)]", children: [
1607
1790
  " \u2014 ",
1608
1791
  detailsText
1609
1792
  ] })
@@ -1614,9 +1797,9 @@ function MembershipList({ title, memberships, renderArtistLink }) {
1614
1797
  function ArtistMembers({ title, memberships, renderArtistLink }) {
1615
1798
  if (memberships.length === 0) return null;
1616
1799
  const isCurrent = (membership) => !membership.ended && membership.endYear === null;
1617
- return /* @__PURE__ */ jsxs17(PanelSection3, { title, children: [
1618
- /* @__PURE__ */ jsx24(MembershipList, { title: "Current", memberships: memberships.filter(isCurrent), renderArtistLink }),
1619
- /* @__PURE__ */ jsx24(
1800
+ return /* @__PURE__ */ jsxs18(PanelSection3, { title, children: [
1801
+ /* @__PURE__ */ jsx25(MembershipList, { title: "Current", memberships: memberships.filter(isCurrent), renderArtistLink }),
1802
+ /* @__PURE__ */ jsx25(
1620
1803
  MembershipList,
1621
1804
  {
1622
1805
  title: "Past",
@@ -1629,7 +1812,7 @@ function ArtistMembers({ title, memberships, renderArtistLink }) {
1629
1812
 
1630
1813
  // src/genre-tree/artist/ArtistDiscography.tsx
1631
1814
  import { PanelGroup as PanelGroup2, PanelSection as PanelSection4 } from "@behindthemusictree/ui";
1632
- import { jsx as jsx25, jsxs as jsxs18 } from "react/jsx-runtime";
1815
+ import { jsx as jsx26, jsxs as jsxs19 } from "react/jsx-runtime";
1633
1816
  var GROUPS = ["Albums", "EPs", "Singles", "Live", "Compilations", "Other"];
1634
1817
  var PRIMARY_GROUPS = { album: "Albums", ep: "EPs", single: "Singles" };
1635
1818
  function discographyGroup({ primaryType, secondaryTypes }) {
@@ -1649,12 +1832,12 @@ function byYear(a, b) {
1649
1832
  function ArtistDiscography({ albums }) {
1650
1833
  if (albums.length === 0) return null;
1651
1834
  const sorted = [...albums].sort(byYear);
1652
- return /* @__PURE__ */ jsx25(PanelSection4, { title: "Discography", children: GROUPS.map((group) => {
1835
+ return /* @__PURE__ */ jsx26(PanelSection4, { title: "Discography", children: GROUPS.map((group) => {
1653
1836
  const groupAlbums = sorted.filter((album) => discographyGroup(album) === group);
1654
1837
  if (groupAlbums.length === 0) return null;
1655
- return /* @__PURE__ */ jsx25(PanelGroup2, { title: group, children: /* @__PURE__ */ jsx25("ul", { children: groupAlbums.map((album) => /* @__PURE__ */ jsxs18("li", { className: "grid grid-cols-[2.5rem_minmax(0,1fr)] gap-2 py-0.5", children: [
1656
- /* @__PURE__ */ jsx25("span", { className: "tabular-nums text-[var(--color-text-muted)]", children: album.year ?? "\u2014" }),
1657
- /* @__PURE__ */ jsx25("span", { className: "truncate text-[var(--color-text)]", title: album.name, children: album.name })
1838
+ return /* @__PURE__ */ jsx26(PanelGroup2, { title: group, children: /* @__PURE__ */ jsx26("ul", { children: groupAlbums.map((album) => /* @__PURE__ */ jsxs19("li", { className: "grid grid-cols-[2.5rem_minmax(0,1fr)] gap-2 py-0.5", children: [
1839
+ /* @__PURE__ */ jsx26("span", { className: "tabular-nums text-[var(--color-text-muted)]", children: album.year ?? "\u2014" }),
1840
+ /* @__PURE__ */ jsx26("span", { className: "truncate text-[var(--color-text)]", title: album.name, children: album.name })
1658
1841
  ] }, album.uuid)) }) }, group);
1659
1842
  }) });
1660
1843
  }
@@ -1663,7 +1846,7 @@ function ArtistDiscography({ albums }) {
1663
1846
  import { useState as useState7 } from "react";
1664
1847
  import { keepPreviousData } from "@tanstack/react-query";
1665
1848
  import { Play as Play2 } from "lucide-react";
1666
- import { Pagination, PanelSection as PanelSection5, Skeleton as Skeleton3 } from "@behindthemusictree/ui";
1849
+ import { Pagination, PanelSection as PanelSection5, Skeleton as Skeleton4 } from "@behindthemusictree/ui";
1667
1850
  import { usePlayer as usePlayer8 } from "@behindthemusictree/app-kit/player";
1668
1851
  import {
1669
1852
  SongDetailedSchema as SongDetailedSchema2,
@@ -1671,7 +1854,7 @@ import {
1671
1854
  songUnplayableReason as songUnplayableReason3,
1672
1855
  useTrackList as useTrackList4
1673
1856
  } from "@behindthemusictree/app-kit/genre-tree";
1674
- import { PaginatedResponseSchema as PaginatedResponseSchema2, useFetchWrapper as useFetchWrapper2, useQueryWithParse } from "@behindthemusictree/app-kit/transport";
1857
+ import { PaginatedResponseSchema as PaginatedResponseSchema2, usePlaybackQuery } from "@behindthemusictree/app-kit/transport";
1675
1858
 
1676
1859
  // src/genre-tree/track-list-sidebar/TrackItem.tsx
1677
1860
  import { usePlayer as usePlayer7 } from "@behindthemusictree/app-kit/player";
@@ -1681,7 +1864,7 @@ import { useTrackList as useTrackList3 } from "@behindthemusictree/app-kit/genre
1681
1864
  import { FaPlay as FaPlay2, FaPause as FaPause2 } from "react-icons/fa";
1682
1865
  import { usePlayer as usePlayer6 } from "@behindthemusictree/app-kit/player";
1683
1866
  import { PlayStates as PlayStates2 } from "@behindthemusictree/app-kit/player";
1684
- import { jsx as jsx26, jsxs as jsxs19 } from "react/jsx-runtime";
1867
+ import { jsx as jsx27, jsxs as jsxs20 } from "react/jsx-runtime";
1685
1868
  function TrackPositionPlayPause({
1686
1869
  position,
1687
1870
  uuid,
@@ -1695,14 +1878,14 @@ function TrackPositionPlayPause({
1695
1878
  const isPlaying = isThisTrack && isConfirmedPlayable && playState === PlayStates2.PLAYING;
1696
1879
  const isPaused = isThisTrack && isConfirmedPlayable && playState === PlayStates2.PAUSED;
1697
1880
  const isCurrentTrackActive = isPlaying || isPaused;
1698
- return /* @__PURE__ */ jsxs19(
1881
+ return /* @__PURE__ */ jsxs20(
1699
1882
  "div",
1700
1883
  {
1701
1884
  className: "track-position-play-pause relative flex items-center justify-center gap-2 text-lg w-16 cursor-pointer",
1702
1885
  style: { minWidth: "4rem", maxWidth: "4rem" },
1703
1886
  onClick: handlePlayPauseClick,
1704
1887
  children: [
1705
- /* @__PURE__ */ jsx26("div", { className: "track-position-play-pause__status group-hover:hidden flex items-center justify-center", children: isAwaitingPlayback ? /* @__PURE__ */ jsx26("span", { className: "track-position-play-pause__spinner", role: "status", "aria-label": "Loading track" }) : isCurrentTrackActive ? /* @__PURE__ */ jsxs19(
1888
+ /* @__PURE__ */ jsx27("div", { className: "track-position-play-pause__status group-hover:hidden flex items-center justify-center", children: isAwaitingPlayback ? /* @__PURE__ */ jsx27("span", { className: "track-position-play-pause__spinner", role: "status", "aria-label": "Loading track" }) : isCurrentTrackActive ? /* @__PURE__ */ jsxs20(
1706
1889
  "span",
1707
1890
  {
1708
1891
  className: "track-position-play-pause__bars",
@@ -1710,18 +1893,18 @@ function TrackPositionPlayPause({
1710
1893
  role: "img",
1711
1894
  "aria-label": isPlaying ? "Playing" : "Paused",
1712
1895
  children: [
1713
- /* @__PURE__ */ jsx26("span", { className: "track-position-play-pause__bar" }),
1714
- /* @__PURE__ */ jsx26("span", { className: "track-position-play-pause__bar" }),
1715
- /* @__PURE__ */ jsx26("span", { className: "track-position-play-pause__bar" })
1896
+ /* @__PURE__ */ jsx27("span", { className: "track-position-play-pause__bar" }),
1897
+ /* @__PURE__ */ jsx27("span", { className: "track-position-play-pause__bar" }),
1898
+ /* @__PURE__ */ jsx27("span", { className: "track-position-play-pause__bar" })
1716
1899
  ]
1717
1900
  }
1718
1901
  ) : position }),
1719
- /* @__PURE__ */ jsx26(
1902
+ /* @__PURE__ */ jsx27(
1720
1903
  "div",
1721
1904
  {
1722
1905
  className: `hidden group-hover:flex items-center justify-center ${isCurrentTrackActive ? "" : "absolute inset-0"}`,
1723
1906
  "aria-hidden": "true",
1724
- children: isAwaitingPlayback ? /* @__PURE__ */ jsx26("span", { className: "track-position-play-pause__spinner" }) : isPlaying ? /* @__PURE__ */ jsx26(FaPause2, {}) : /* @__PURE__ */ jsx26(FaPlay2, {})
1907
+ children: isAwaitingPlayback ? /* @__PURE__ */ jsx27("span", { className: "track-position-play-pause__spinner" }) : isPlaying ? /* @__PURE__ */ jsx27(FaPause2, {}) : /* @__PURE__ */ jsx27(FaPlay2, {})
1725
1908
  }
1726
1909
  )
1727
1910
  ]
@@ -1732,7 +1915,7 @@ function TrackPositionPlayPause({
1732
1915
  // src/genre-tree/track-list-sidebar/TrackItem.tsx
1733
1916
  import { isSong as isSong2, songUnplayableReason as songUnplayableReason2 } from "@behindthemusictree/app-kit/genre-tree";
1734
1917
  import { ArtistNames as ArtistNames2 } from "@behindthemusictree/app-kit/genre-tree";
1735
- import { jsx as jsx27, jsxs as jsxs20 } from "react/jsx-runtime";
1918
+ import { jsx as jsx28, jsxs as jsxs21 } from "react/jsx-runtime";
1736
1919
  function TrackItem({
1737
1920
  track,
1738
1921
  position,
@@ -1756,12 +1939,12 @@ function TrackItem({
1756
1939
  loadTrackForPlayer(track.uuid);
1757
1940
  }
1758
1941
  };
1759
- return /* @__PURE__ */ jsxs20(
1942
+ return /* @__PURE__ */ jsxs21(
1760
1943
  "div",
1761
1944
  {
1762
1945
  className: `track-item flex h-14 text-[var(--color-text)] ${isUnplayable ? "opacity-50" : "hover:bg-[var(--color-surface-muted)] group"}`,
1763
1946
  children: [
1764
- isUnplayable ? /* @__PURE__ */ jsx27(
1947
+ isUnplayable ? /* @__PURE__ */ jsx28(
1765
1948
  "div",
1766
1949
  {
1767
1950
  className: "flex items-center justify-center text-lg w-16",
@@ -1769,7 +1952,7 @@ function TrackItem({
1769
1952
  "aria-disabled": "true",
1770
1953
  children: position
1771
1954
  }
1772
- ) : /* @__PURE__ */ jsx27(
1955
+ ) : /* @__PURE__ */ jsx28(
1773
1956
  TrackPositionPlayPause,
1774
1957
  {
1775
1958
  position,
@@ -1778,12 +1961,12 @@ function TrackItem({
1778
1961
  isSelected: selectedTrack?.uuid === track.uuid
1779
1962
  }
1780
1963
  ),
1781
- /* @__PURE__ */ jsxs20("div", { className: "title-artist-container flex flex-col items-start justify-center min-w-0 flex-1", children: [
1782
- /* @__PURE__ */ jsx27("div", { className: "flex max-w-full items-center gap-2", children: /* @__PURE__ */ jsx27("div", { className: "title text-lg font-bold text-[var(--color-text)] max-w-full truncate", children: renderSongLink ? renderSongLink(track) : isSong2(track) ? /* @__PURE__ */ jsx27(MusicBrainzRecordingLink, { song: track, className: "hover:underline focus-visible:underline", children: track.title }) : track.title }) }),
1783
- track.artists && track.artists.length > 0 ? /* @__PURE__ */ jsx27("div", { className: "artist text-base max-w-full truncate text-[var(--color-text-muted)]", children: /* @__PURE__ */ jsx27(ArtistNames2, { artists: track.artists, renderArtistLink }) }) : ""
1964
+ /* @__PURE__ */ jsxs21("div", { className: "title-artist-container flex flex-col items-start justify-center min-w-0 flex-1", children: [
1965
+ /* @__PURE__ */ jsx28("div", { className: "flex max-w-full items-center gap-2", children: /* @__PURE__ */ jsx28("div", { className: "title text-lg font-bold text-[var(--color-text)] max-w-full truncate", children: renderSongLink ? renderSongLink(track) : isSong2(track) ? /* @__PURE__ */ jsx28(MusicBrainzRecordingLink, { song: track, className: "hover:underline focus-visible:underline", children: track.title }) : track.title }) }),
1966
+ track.artists && track.artists.length > 0 ? /* @__PURE__ */ jsx28("div", { className: "artist text-base max-w-full truncate text-[var(--color-text-muted)]", children: /* @__PURE__ */ jsx28(ArtistNames2, { artists: track.artists, renderArtistLink }) }) : ""
1784
1967
  ] }),
1785
- /* @__PURE__ */ jsx27("div", { className: "genre-name-container flex items-center justify-start w-[21.6%] min-w-0 shrink-0", children: track.genre ? /* @__PURE__ */ jsx27("div", { className: "genre-name max-w-full whitespace-normal break-words p-1 text-xs font-bold", children: track.genre.name }) : "" }),
1786
- renderDuration && /* @__PURE__ */ jsx27(
1968
+ /* @__PURE__ */ jsx28("div", { className: "genre-name-container flex items-center justify-start w-[21.6%] min-w-0 shrink-0", children: track.genre ? /* @__PURE__ */ jsx28("div", { className: "genre-name max-w-full whitespace-normal break-words p-1 text-xs font-bold", children: track.genre.name }) : "" }),
1969
+ renderDuration && /* @__PURE__ */ jsx28(
1787
1970
  "div",
1788
1971
  {
1789
1972
  className: "duration flex text-base w-16 items-center justify-center",
@@ -1791,14 +1974,14 @@ function TrackItem({
1791
1974
  children: renderDuration(track)
1792
1975
  }
1793
1976
  ),
1794
- renderActions && /* @__PURE__ */ jsx27("div", { className: "edit flex text-base w-6 items-center justify-center mr-2 cursor-pointer", children: renderActions(track) })
1977
+ renderActions && /* @__PURE__ */ jsx28("div", { className: "edit flex text-base w-6 items-center justify-center mr-2 cursor-pointer", children: renderActions(track) })
1795
1978
  ]
1796
1979
  }
1797
1980
  );
1798
1981
  }
1799
1982
 
1800
1983
  // src/genre-tree/artist/ArtistSongs.tsx
1801
- import { Fragment as Fragment10, jsx as jsx28, jsxs as jsxs21 } from "react/jsx-runtime";
1984
+ import { Fragment as Fragment11, jsx as jsx29, jsxs as jsxs22 } from "react/jsx-runtime";
1802
1985
  var SongPageSchema2 = PaginatedResponseSchema2(SongDetailedSchema2);
1803
1986
  var SORTS = [
1804
1987
  { ordering: "title", label: "Title" },
@@ -1807,12 +1990,12 @@ var SORTS = [
1807
1990
  function ArtistSongs({ artist, getBackendBaseUrl, renderArtistLink, renderSongLink }) {
1808
1991
  const [page, setPage] = useState7(1);
1809
1992
  const [ordering, setOrdering] = useState7("title");
1810
- const { fetch } = useFetchWrapper2(getBackendBaseUrl);
1811
1993
  const { playNewTrackListFromArtist } = useTrackList4();
1812
1994
  const { playerTrackObject } = usePlayer8();
1813
- const { data, isPending, isError } = useQueryWithParse({
1995
+ const { data, isPending, isError } = usePlaybackQuery({
1996
+ getBackendBaseUrl,
1814
1997
  queryKey: ["referenceSongs", "artist", artist.uuid, ordering, page],
1815
- queryFn: () => fetch(libraryEndpoints2.reference.songs.list(), true, false, void 0, {
1998
+ queryFn: (fetchBound) => fetchBound(libraryEndpoints2.reference.songs.list(), true, false, void 0, {
1816
1999
  artist: artist.uuid,
1817
2000
  ordering,
1818
2001
  page
@@ -1831,21 +2014,21 @@ function ArtistSongs({ artist, getBackendBaseUrl, renderArtistLink, renderSongLi
1831
2014
  event.stopPropagation();
1832
2015
  void play(song.uuid);
1833
2016
  };
1834
- return /* @__PURE__ */ jsx28(PanelSection5, { title: "Songs", children: isPending ? /* @__PURE__ */ jsx28(Skeleton3, { className: "h-14", "aria-busy": "true" }) : isError ? /* @__PURE__ */ jsx28("p", { className: "text-red-600", children: "Could not load songs." }) : data.results.length === 0 ? /* @__PURE__ */ jsx28("p", { className: "text-[var(--color-text-muted)]", children: "No songs yet." }) : /* @__PURE__ */ jsxs21(Fragment10, { children: [
1835
- /* @__PURE__ */ jsxs21("div", { className: "flex flex-wrap items-center justify-between gap-2 mb-2", children: [
1836
- /* @__PURE__ */ jsxs21(
2017
+ return /* @__PURE__ */ jsx29(PanelSection5, { title: "Songs", children: isPending ? /* @__PURE__ */ jsx29(Skeleton4, { className: "h-14", "aria-busy": "true" }) : isError ? /* @__PURE__ */ jsx29("p", { className: "text-red-600", children: "Could not load songs." }) : data.results.length === 0 ? /* @__PURE__ */ jsx29("p", { className: "text-[var(--color-text-muted)]", children: "No songs yet." }) : /* @__PURE__ */ jsxs22(Fragment11, { children: [
2018
+ /* @__PURE__ */ jsxs22("div", { className: "flex flex-wrap items-center justify-between gap-2 mb-2", children: [
2019
+ /* @__PURE__ */ jsxs22(
1837
2020
  "button",
1838
2021
  {
1839
2022
  type: "button",
1840
2023
  onClick: () => void play(),
1841
2024
  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",
1842
2025
  children: [
1843
- /* @__PURE__ */ jsx28(Play2, { className: "h-3.5 w-3.5", "aria-hidden": "true" }),
2026
+ /* @__PURE__ */ jsx29(Play2, { className: "h-3.5 w-3.5", "aria-hidden": "true" }),
1844
2027
  "Play all"
1845
2028
  ]
1846
2029
  }
1847
2030
  ),
1848
- /* @__PURE__ */ jsx28("div", { role: "group", "aria-label": "Sort songs by", className: "flex gap-1 text-xs", children: SORTS.map((sort) => /* @__PURE__ */ jsx28(
2031
+ /* @__PURE__ */ jsx29("div", { role: "group", "aria-label": "Sort songs by", className: "flex gap-1 text-xs", children: SORTS.map((sort) => /* @__PURE__ */ jsx29(
1849
2032
  "button",
1850
2033
  {
1851
2034
  type: "button",
@@ -1860,7 +2043,7 @@ function ArtistSongs({ artist, getBackendBaseUrl, renderArtistLink, renderSongLi
1860
2043
  sort.ordering
1861
2044
  )) })
1862
2045
  ] }),
1863
- /* @__PURE__ */ jsx28("ul", { className: "overflow-hidden mb-2 bg-zinc-950 rounded-lg", children: data.results.map((song, index) => /* @__PURE__ */ jsx28("li", { onClickCapture: (event) => handleRowClickCapture(event, song), children: /* @__PURE__ */ jsx28(
2046
+ /* @__PURE__ */ jsx29("ul", { className: "overflow-hidden mb-2 bg-zinc-950 rounded-lg", children: data.results.map((song, index) => /* @__PURE__ */ jsx29("li", { onClickCapture: (event) => handleRowClickCapture(event, song), children: /* @__PURE__ */ jsx29(
1864
2047
  TrackItem,
1865
2048
  {
1866
2049
  track: song,
@@ -1869,12 +2052,12 @@ function ArtistSongs({ artist, getBackendBaseUrl, renderArtistLink, renderSongLi
1869
2052
  renderSongLink
1870
2053
  }
1871
2054
  ) }, song.uuid)) }),
1872
- /* @__PURE__ */ jsx28(Pagination, { currentPage: page, totalPages: data.totalPages, onPageChange: setPage })
2055
+ /* @__PURE__ */ jsx29(Pagination, { currentPage: page, totalPages: data.totalPages, onPageChange: setPage })
1873
2056
  ] }) });
1874
2057
  }
1875
2058
 
1876
2059
  // src/genre-tree/artist/useArtistPanel.tsx
1877
- import { Fragment as Fragment11, jsx as jsx29, jsxs as jsxs22 } from "react/jsx-runtime";
2060
+ import { Fragment as Fragment12, jsx as jsx30, jsxs as jsxs23 } from "react/jsx-runtime";
1878
2061
  function useArtistPanel({
1879
2062
  uuid,
1880
2063
  scope,
@@ -1895,26 +2078,26 @@ function useArtistPanel({
1895
2078
  return {
1896
2079
  ...panel,
1897
2080
  title: "Artist",
1898
- content: /* @__PURE__ */ jsxs22("div", { "aria-busy": "true", className: "flex flex-col gap-2.5 px-6 py-2.5", children: [
1899
- /* @__PURE__ */ jsx29(Skeleton4, { className: "h-3 w-16" }),
1900
- /* @__PURE__ */ jsx29(Skeleton4, { className: "h-4 w-32" }),
1901
- /* @__PURE__ */ jsx29(Skeleton4, { className: "h-3 w-16" }),
1902
- /* @__PURE__ */ jsx29(Skeleton4, { className: "h-4 w-24" }),
1903
- /* @__PURE__ */ jsx29(Skeleton4, { className: "h-16 mt-2.5" }),
1904
- /* @__PURE__ */ jsx29(Skeleton4, { className: "h-16" })
2081
+ content: /* @__PURE__ */ jsxs23("div", { "aria-busy": "true", className: "flex flex-col gap-2.5 px-6 py-2.5", children: [
2082
+ /* @__PURE__ */ jsx30(Skeleton5, { className: "h-3 w-16" }),
2083
+ /* @__PURE__ */ jsx30(Skeleton5, { className: "h-4 w-32" }),
2084
+ /* @__PURE__ */ jsx30(Skeleton5, { className: "h-3 w-16" }),
2085
+ /* @__PURE__ */ jsx30(Skeleton5, { className: "h-4 w-24" }),
2086
+ /* @__PURE__ */ jsx30(Skeleton5, { className: "h-16 mt-2.5" }),
2087
+ /* @__PURE__ */ jsx30(Skeleton5, { className: "h-16" })
1905
2088
  ] })
1906
2089
  };
1907
2090
  case "not-found":
1908
2091
  return {
1909
2092
  ...panel,
1910
2093
  title: "Artist not found",
1911
- content: /* @__PURE__ */ jsx29("p", { className: "px-6 py-2.5 text-[var(--color-text-muted)]", children: "This artist does not exist." })
2094
+ content: /* @__PURE__ */ jsx30("p", { className: "px-6 py-2.5 text-[var(--color-text-muted)]", children: "This artist does not exist." })
1912
2095
  };
1913
2096
  case "error":
1914
2097
  return {
1915
2098
  ...panel,
1916
2099
  title: "Artist",
1917
- content: /* @__PURE__ */ jsx29("p", { className: "px-6 py-2.5 text-red-600", children: "Could not load this artist." })
2100
+ content: /* @__PURE__ */ jsx30("p", { className: "px-6 py-2.5 text-red-600", children: "Could not load this artist." })
1918
2101
  };
1919
2102
  case "success": {
1920
2103
  const { artist } = result;
@@ -1923,13 +2106,13 @@ function useArtistPanel({
1923
2106
  return {
1924
2107
  ...panel,
1925
2108
  title: artist.name,
1926
- content: /* @__PURE__ */ jsxs22(Fragment11, { children: [
1927
- shareUrl && /* @__PURE__ */ jsx29("div", { className: "genre-action-band border-b border-[var(--color-border)]", children: /* @__PURE__ */ jsx29("div", { className: "flex flex-wrap gap-1 px-3.5 py-3", children: shareTarget && /* @__PURE__ */ jsx29(ShareActionButton, { target: shareTarget, url: shareUrl }) }) }),
1928
- artist.image && /* @__PURE__ */ jsx29(ArtistPhoto, { image: artist.image, name: artist.name }),
1929
- /* @__PURE__ */ jsx29(ArtistHeader, { artist }),
1930
- /* @__PURE__ */ jsx29(ArtistMembers, { title: "Members", memberships: artist.members, renderArtistLink }),
1931
- /* @__PURE__ */ jsx29(ArtistMembers, { title: "Member of", memberships: artist.memberOf, renderArtistLink }),
1932
- artist.tracksCount > 0 && /* @__PURE__ */ jsx29(
2109
+ content: /* @__PURE__ */ jsxs23(Fragment12, { children: [
2110
+ shareUrl && /* @__PURE__ */ jsx30("div", { className: "genre-action-band border-b border-[var(--color-border)]", children: /* @__PURE__ */ jsx30("div", { className: "flex flex-wrap gap-1 px-3.5 py-3", children: shareTarget && /* @__PURE__ */ jsx30(ShareActionButton, { target: shareTarget, url: shareUrl }) }) }),
2111
+ artist.image && /* @__PURE__ */ jsx30(ArtistPhoto, { image: artist.image, name: artist.name }),
2112
+ /* @__PURE__ */ jsx30(ArtistHeader, { artist }),
2113
+ /* @__PURE__ */ jsx30(ArtistMembers, { title: "Members", memberships: artist.members, renderArtistLink }),
2114
+ /* @__PURE__ */ jsx30(ArtistMembers, { title: "Member of", memberships: artist.memberOf, renderArtistLink }),
2115
+ artist.tracksCount > 0 && /* @__PURE__ */ jsx30(
1933
2116
  ArtistSongs,
1934
2117
  {
1935
2118
  artist: { uuid: artist.uuid, name: artist.name },
@@ -1939,14 +2122,14 @@ function useArtistPanel({
1939
2122
  }
1940
2123
  )
1941
2124
  ] }),
1942
- extraContent: artist.albums.length > 0 ? /* @__PURE__ */ jsx29(ArtistDiscography, { albums: artist.albums }) : void 0
2125
+ extraContent: artist.albums.length > 0 ? /* @__PURE__ */ jsx30(ArtistDiscography, { albums: artist.albums }) : void 0
1943
2126
  };
1944
2127
  }
1945
2128
  }
1946
2129
  }
1947
2130
 
1948
2131
  // src/genre-tree/song/useSongPanel.tsx
1949
- import { Skeleton as Skeleton6 } from "@behindthemusictree/ui";
2132
+ import { Skeleton as Skeleton7 } from "@behindthemusictree/ui";
1950
2133
  import {
1951
2134
  useSong
1952
2135
  } from "@behindthemusictree/app-kit/genre-tree";
@@ -1954,7 +2137,7 @@ import {
1954
2137
  // src/genre-tree/song/SongPanelContent.tsx
1955
2138
  import { useState as useState8 } from "react";
1956
2139
  import { Pause as Pause2, Play as Play3 } from "lucide-react";
1957
- import { FieldList as FieldList2, PanelSection as PanelSection6, Skeleton as Skeleton5 } from "@behindthemusictree/ui";
2140
+ import { FieldList as FieldList2, PanelSection as PanelSection6, Skeleton as Skeleton6 } from "@behindthemusictree/ui";
1958
2141
  import { PlayStates as PlayStates3, usePlayer as usePlayer9 } from "@behindthemusictree/app-kit/player";
1959
2142
  import {
1960
2143
  ArtistNames as ArtistNames3,
@@ -1963,7 +2146,7 @@ import {
1963
2146
  useSongHistory,
1964
2147
  useTrackList as useTrackList5
1965
2148
  } from "@behindthemusictree/app-kit/genre-tree";
1966
- import { Fragment as Fragment12, jsx as jsx30, jsxs as jsxs23 } from "react/jsx-runtime";
2149
+ import { Fragment as Fragment13, jsx as jsx31, jsxs as jsxs24 } from "react/jsx-runtime";
1967
2150
  var numberFormat = new Intl.NumberFormat("en");
1968
2151
  var dateFormat = new Intl.DateTimeFormat("en", { dateStyle: "medium" });
1969
2152
  var externalLink = "text-[var(--color-primary)] hover:underline focus-visible:underline";
@@ -1971,32 +2154,32 @@ function SongHistory({ uuid, getBackendBaseUrl }) {
1971
2154
  const [open, setOpen] = useState8(false);
1972
2155
  const history = useSongHistory(uuid, getBackendBaseUrl, open);
1973
2156
  let body = null;
1974
- if (history.status === "loading") body = /* @__PURE__ */ jsx30(Skeleton5, { className: "h-10", "aria-busy": "true" });
1975
- else if (history.status === "error") body = /* @__PURE__ */ jsx30("p", { className: "text-red-600", children: "Could not load the history." });
2157
+ if (history.status === "loading") body = /* @__PURE__ */ jsx31(Skeleton6, { className: "h-10", "aria-busy": "true" });
2158
+ else if (history.status === "error") body = /* @__PURE__ */ jsx31("p", { className: "text-red-600", children: "Could not load the history." });
1976
2159
  else if (history.status === "success")
1977
- body = history.entries.length === 0 ? /* @__PURE__ */ jsx30("p", { className: "text-[var(--color-text-muted)]", children: "No changes yet." }) : /* @__PURE__ */ jsx30("ul", { className: "divide-y divide-[var(--color-border)]", children: history.entries.map((entry) => /* @__PURE__ */ jsxs23("li", { className: "py-1.5 text-sm", children: [
1978
- /* @__PURE__ */ jsx30("span", { className: "font-medium", children: entry.action }),
1979
- entry.oldValue !== null || entry.newValue !== null ? /* @__PURE__ */ jsxs23("span", { className: "text-[var(--color-text-muted)]", children: [
2160
+ body = history.entries.length === 0 ? /* @__PURE__ */ jsx31("p", { className: "text-[var(--color-text-muted)]", children: "No changes yet." }) : /* @__PURE__ */ jsx31("ul", { className: "divide-y divide-[var(--color-border)]", children: history.entries.map((entry) => /* @__PURE__ */ jsxs24("li", { className: "py-1.5 text-sm", children: [
2161
+ /* @__PURE__ */ jsx31("span", { className: "font-medium", children: entry.action }),
2162
+ entry.oldValue !== null || entry.newValue !== null ? /* @__PURE__ */ jsxs24("span", { className: "text-[var(--color-text-muted)]", children: [
1980
2163
  " ",
1981
2164
  "\u2014 ",
1982
2165
  entry.oldValue ?? "none",
1983
2166
  " \u2192 ",
1984
2167
  entry.newValue ?? "none"
1985
2168
  ] }) : null,
1986
- /* @__PURE__ */ jsxs23("span", { className: "block text-xs text-[var(--color-text-muted)]", children: [
2169
+ /* @__PURE__ */ jsxs24("span", { className: "block text-xs text-[var(--color-text-muted)]", children: [
1987
2170
  entry.actorPseudo ?? "Anonymous",
1988
2171
  " \xB7 ",
1989
2172
  dateFormat.format(new Date(entry.createdOn))
1990
2173
  ] })
1991
2174
  ] }, entry.uuid)) });
1992
- return /* @__PURE__ */ jsxs23(
2175
+ return /* @__PURE__ */ jsxs24(
1993
2176
  "details",
1994
2177
  {
1995
2178
  onToggle: (event) => setOpen(event.currentTarget.open),
1996
2179
  className: "px-6 pb-2.5 mt-2.5 border-t border-[var(--color-border)]",
1997
2180
  children: [
1998
- /* @__PURE__ */ jsx30("summary", { className: "mt-2.5 cursor-pointer text-[0.6875rem] font-semibold uppercase text-[var(--color-text-muted)]", children: "History" }),
1999
- /* @__PURE__ */ jsx30("div", { className: "mt-1.5", children: body })
2181
+ /* @__PURE__ */ jsx31("summary", { className: "mt-2.5 cursor-pointer text-[0.6875rem] font-semibold uppercase text-[var(--color-text-muted)]", children: "History" }),
2182
+ /* @__PURE__ */ jsx31("div", { className: "mt-1.5", children: body })
2000
2183
  ]
2001
2184
  }
2002
2185
  );
@@ -2022,13 +2205,13 @@ function SongPanelContent({
2022
2205
  const path = genrePath.length > 0 ? genrePath : null;
2023
2206
  fields.push({
2024
2207
  label: "Genre",
2025
- value: path ? /* @__PURE__ */ jsx30("span", { className: "flex flex-wrap items-center gap-x-1", children: path.map((genrePlaylist, index) => /* @__PURE__ */ jsxs23("span", { children: [
2026
- index > 0 && /* @__PURE__ */ jsx30("span", { "aria-hidden": "true", children: "\u203A " }),
2027
- /* @__PURE__ */ jsx30("button", { type: "button", onClick: () => onSelectGenre(genrePlaylist), className: "hover:underline", children: genrePlaylist.name })
2208
+ value: path ? /* @__PURE__ */ jsx31("span", { className: "flex flex-wrap items-center gap-x-1", children: path.map((genrePlaylist, index) => /* @__PURE__ */ jsxs24("span", { children: [
2209
+ index > 0 && /* @__PURE__ */ jsx31("span", { "aria-hidden": "true", children: "\u203A " }),
2210
+ /* @__PURE__ */ jsx31("button", { type: "button", onClick: () => onSelectGenre(genrePlaylist), className: "hover:underline", children: genrePlaylist.name })
2028
2211
  ] }, genrePlaylist.uuid)) }) : song.genre.name
2029
2212
  });
2030
2213
  } else {
2031
- fields.push({ label: "Genre", value: /* @__PURE__ */ jsx30("span", { className: "text-[var(--color-text-muted)]", children: "No genre" }) });
2214
+ fields.push({ label: "Genre", value: /* @__PURE__ */ jsx31("span", { className: "text-[var(--color-text-muted)]", children: "No genre" }) });
2032
2215
  }
2033
2216
  if (song.firstReleaseYear) fields.push({ label: "First released", value: song.firstReleaseYear });
2034
2217
  const viewCount = song.youtubeVideo?.viewCount;
@@ -2038,11 +2221,11 @@ function SongPanelContent({
2038
2221
  const links = [];
2039
2222
  if (musicBrainzUrl)
2040
2223
  links.push(
2041
- /* @__PURE__ */ jsx30("a", { href: musicBrainzUrl, target: "_blank", rel: "noopener noreferrer", className: externalLink, children: "MusicBrainz" }, "mb")
2224
+ /* @__PURE__ */ jsx31("a", { href: musicBrainzUrl, target: "_blank", rel: "noopener noreferrer", className: externalLink, children: "MusicBrainz" }, "mb")
2042
2225
  );
2043
2226
  if (song.youtubeVideo)
2044
2227
  links.push(
2045
- /* @__PURE__ */ jsx30(
2228
+ /* @__PURE__ */ jsx31(
2046
2229
  "a",
2047
2230
  {
2048
2231
  href: `https://www.youtube.com/watch?v=${encodeURIComponent(song.youtubeVideo.id)}`,
@@ -2057,33 +2240,33 @@ function SongPanelContent({
2057
2240
  if (links.length > 0)
2058
2241
  fields.push({
2059
2242
  label: "Links",
2060
- value: /* @__PURE__ */ jsx30("span", { className: "flex gap-3", children: links })
2243
+ value: /* @__PURE__ */ jsx31("span", { className: "flex gap-3", children: links })
2061
2244
  });
2062
- return /* @__PURE__ */ jsxs23(Fragment12, { children: [
2063
- /* @__PURE__ */ jsx30("div", { className: "genre-action-band border-b border-[var(--color-border)]", children: /* @__PURE__ */ jsxs23("div", { className: "flex flex-wrap gap-1 px-3.5 py-3", children: [
2064
- unplayableReason ? /* @__PURE__ */ jsx30("p", { className: "flex items-center px-2.5 text-sm text-[var(--color-text-muted)]", children: unplayableReason }) : /* @__PURE__ */ jsxs23(
2245
+ return /* @__PURE__ */ jsxs24(Fragment13, { children: [
2246
+ /* @__PURE__ */ jsx31("div", { className: "genre-action-band border-b border-[var(--color-border)]", children: /* @__PURE__ */ jsxs24("div", { className: "flex flex-wrap gap-1 px-3.5 py-3", children: [
2247
+ unplayableReason ? /* @__PURE__ */ jsx31("p", { className: "flex items-center px-2.5 text-sm text-[var(--color-text-muted)]", children: unplayableReason }) : /* @__PURE__ */ jsxs24(
2065
2248
  "button",
2066
2249
  {
2067
2250
  type: "button",
2068
2251
  onClick: () => isCurrent ? handlePlayPauseAction() : playNewTrackListFromTrackUuid(song, scope),
2069
2252
  className: "group flex flex-col items-center gap-1 w-16 text-xs text-center text-[var(--color-text)] focus-visible:outline-none",
2070
2253
  children: [
2071
- /* @__PURE__ */ jsx30("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__ */ jsx30(Pause2, { "aria-hidden": "true", className: "w-5 h-5" }) : /* @__PURE__ */ jsx30(Play3, { "aria-hidden": "true", className: "w-5 h-5" }) }),
2254
+ /* @__PURE__ */ jsx31("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__ */ jsx31(Pause2, { "aria-hidden": "true", className: "w-5 h-5" }) : /* @__PURE__ */ jsx31(Play3, { "aria-hidden": "true", className: "w-5 h-5" }) }),
2072
2255
  isPlaying ? "Pause" : "Play"
2073
2256
  ]
2074
2257
  }
2075
2258
  ),
2076
- shareUrl && /* @__PURE__ */ jsx30(ShareActionButton, { target: shareTarget, url: shareUrl })
2259
+ shareUrl && /* @__PURE__ */ jsx31(ShareActionButton, { target: shareTarget, url: shareUrl })
2077
2260
  ] }) }),
2078
- song.artists && song.artists.length > 0 && /* @__PURE__ */ jsx30("p", { className: "px-6 pt-2.5 text-[var(--color-text-muted)]", children: /* @__PURE__ */ jsx30(ArtistNames3, { artists: song.artists, renderArtistLink }) }),
2079
- /* @__PURE__ */ jsx30(FieldList2, { items: fields }),
2080
- song.essentialGenres && song.essentialGenres.length > 0 && /* @__PURE__ */ jsx30(PanelSection6, { title: "Essential in", children: /* @__PURE__ */ jsx30("ul", { children: song.essentialGenres.map((genre) => /* @__PURE__ */ jsx30("li", { className: "py-0.5", children: genre.name }, genre.uuid)) }) }),
2081
- /* @__PURE__ */ jsx30(SongHistory, { uuid: song.uuid, getBackendBaseUrl })
2261
+ song.artists && song.artists.length > 0 && /* @__PURE__ */ jsx31("p", { className: "px-6 pt-2.5 text-[var(--color-text-muted)]", children: /* @__PURE__ */ jsx31(ArtistNames3, { artists: song.artists, renderArtistLink }) }),
2262
+ /* @__PURE__ */ jsx31(FieldList2, { items: fields }),
2263
+ song.essentialGenres && song.essentialGenres.length > 0 && /* @__PURE__ */ jsx31(PanelSection6, { title: "Essential in", children: /* @__PURE__ */ jsx31("ul", { children: song.essentialGenres.map((genre) => /* @__PURE__ */ jsx31("li", { className: "py-0.5", children: genre.name }, genre.uuid)) }) }),
2264
+ /* @__PURE__ */ jsx31(SongHistory, { uuid: song.uuid, getBackendBaseUrl })
2082
2265
  ] });
2083
2266
  }
2084
2267
 
2085
2268
  // src/genre-tree/song/useSongPanel.tsx
2086
- import { jsx as jsx31, jsxs as jsxs24 } from "react/jsx-runtime";
2269
+ import { jsx as jsx32, jsxs as jsxs25 } from "react/jsx-runtime";
2087
2270
  function useSongPanel({
2088
2271
  uuid,
2089
2272
  scope,
@@ -2105,24 +2288,24 @@ function useSongPanel({
2105
2288
  return {
2106
2289
  ...panel,
2107
2290
  title: "Song",
2108
- content: /* @__PURE__ */ jsxs24("div", { "aria-busy": "true", className: "flex flex-col gap-2.5 px-6 py-2.5", children: [
2109
- /* @__PURE__ */ jsx31(Skeleton6, { className: "h-3 w-16" }),
2110
- /* @__PURE__ */ jsx31(Skeleton6, { className: "h-4 w-32" }),
2111
- /* @__PURE__ */ jsx31(Skeleton6, { className: "h-3 w-16" }),
2112
- /* @__PURE__ */ jsx31(Skeleton6, { className: "h-4 w-24" })
2291
+ content: /* @__PURE__ */ jsxs25("div", { "aria-busy": "true", className: "flex flex-col gap-2.5 px-6 py-2.5", children: [
2292
+ /* @__PURE__ */ jsx32(Skeleton7, { className: "h-3 w-16" }),
2293
+ /* @__PURE__ */ jsx32(Skeleton7, { className: "h-4 w-32" }),
2294
+ /* @__PURE__ */ jsx32(Skeleton7, { className: "h-3 w-16" }),
2295
+ /* @__PURE__ */ jsx32(Skeleton7, { className: "h-4 w-24" })
2113
2296
  ] })
2114
2297
  };
2115
2298
  case "not-found":
2116
2299
  return {
2117
2300
  ...panel,
2118
2301
  title: "Song not found",
2119
- content: /* @__PURE__ */ jsx31("p", { className: "px-6 py-2.5 text-[var(--color-text-muted)]", children: "This song does not exist." })
2302
+ content: /* @__PURE__ */ jsx32("p", { className: "px-6 py-2.5 text-[var(--color-text-muted)]", children: "This song does not exist." })
2120
2303
  };
2121
2304
  case "error":
2122
2305
  return {
2123
2306
  ...panel,
2124
2307
  title: "Song",
2125
- content: /* @__PURE__ */ jsx31("p", { className: "px-6 py-2.5 text-red-600", children: "Could not load this song." })
2308
+ content: /* @__PURE__ */ jsx32("p", { className: "px-6 py-2.5 text-red-600", children: "Could not load this song." })
2126
2309
  };
2127
2310
  case "success": {
2128
2311
  const { song } = result;
@@ -2130,7 +2313,7 @@ function useSongPanel({
2130
2313
  return {
2131
2314
  ...panel,
2132
2315
  title: song.title,
2133
- content: /* @__PURE__ */ jsx31(
2316
+ content: /* @__PURE__ */ jsx32(
2134
2317
  SongPanelContent,
2135
2318
  {
2136
2319
  song,
@@ -2150,12 +2333,12 @@ function useSongPanel({
2150
2333
  // src/genre-tree/SizeMetricToggle.tsx
2151
2334
  import { MdMusicNote, MdVisibility } from "react-icons/md";
2152
2335
  import { SegmentedControl } from "@behindthemusictree/ui";
2153
- import { jsx as jsx32 } from "react/jsx-runtime";
2336
+ import { jsx as jsx33 } from "react/jsx-runtime";
2154
2337
  function SizeMetricToggle({
2155
2338
  value,
2156
2339
  onChange
2157
2340
  }) {
2158
- return /* @__PURE__ */ jsx32(
2341
+ return /* @__PURE__ */ jsx33(
2159
2342
  SegmentedControl,
2160
2343
  {
2161
2344
  variant: "floating",
@@ -2163,15 +2346,15 @@ function SizeMetricToggle({
2163
2346
  value,
2164
2347
  onChange,
2165
2348
  options: [
2166
- { value: "songs", label: /* @__PURE__ */ jsx32(MdMusicNote, { size: 18, "aria-hidden": true }), "aria-label": "Size by songs", title: "Node size by song count" },
2167
- { value: "views", label: /* @__PURE__ */ jsx32(MdVisibility, { size: 18, "aria-hidden": true }), "aria-label": "Size by views", title: "Node size by song views" }
2349
+ { value: "songs", label: /* @__PURE__ */ jsx33(MdMusicNote, { size: 18, "aria-hidden": true }), "aria-label": "Size by songs", title: "Node size by song count" },
2350
+ { value: "views", label: /* @__PURE__ */ jsx33(MdVisibility, { size: 18, "aria-hidden": true }), "aria-label": "Size by views", title: "Node size by song views" }
2168
2351
  ]
2169
2352
  }
2170
2353
  );
2171
2354
  }
2172
2355
 
2173
2356
  // src/genre-tree/GenreTreeView.tsx
2174
- import { Fragment as Fragment13, jsx as jsx33, jsxs as jsxs25 } from "react/jsx-runtime";
2357
+ import { Fragment as Fragment14, jsx as jsx34, jsxs as jsxs26 } from "react/jsx-runtime";
2175
2358
  var NO_POP_ROOT_TITLE = "This genre tree has no 'Mainstream Pop' root yet";
2176
2359
  var REGIONS_CLASS = "[--gtv-center-left:var(--layout-center-left)] [--gtv-center-right:var(--layout-center-right)] [--gtv-center-top:var(--layout-band-top)]";
2177
2360
  var noop = () => {
@@ -2184,8 +2367,8 @@ function GenreTreeView({
2184
2367
  getBackendBaseUrl,
2185
2368
  additionalActions,
2186
2369
  viewMode: controlledViewMode,
2187
- selectedNodeId: controlledSelectedNodeId,
2188
- onSelectedNodeIdChange,
2370
+ selectedGenreUuid: controlledSelectedGenreUuid,
2371
+ onSelectedGenreUuidChange,
2189
2372
  defaultViewport,
2190
2373
  viewportRestore,
2191
2374
  onViewportChange,
@@ -2258,10 +2441,14 @@ function GenreTreeView({
2258
2441
  };
2259
2442
  const {
2260
2443
  isLoading,
2444
+ isError,
2445
+ refetch,
2261
2446
  isFetching: isTreeFetching,
2262
2447
  genrePlaylistByNodeId,
2263
2448
  visibleGenrePlaylists,
2264
2449
  groupedGenrePlaylistsByRoot,
2450
+ selectedGenreUuid,
2451
+ selectGenre,
2265
2452
  selectedNodeId,
2266
2453
  selectedGenrePlaylist,
2267
2454
  selectNode,
@@ -2278,8 +2465,8 @@ function GenreTreeView({
2278
2465
  scope,
2279
2466
  getBackendBaseUrl,
2280
2467
  viewMode: controlledViewMode,
2281
- selectedNodeId: controlledSelectedNodeId,
2282
- onSelectedNodeIdChange,
2468
+ selectedGenreUuid: controlledSelectedGenreUuid,
2469
+ onSelectedGenreUuidChange,
2283
2470
  criteriaOverviewSchema,
2284
2471
  artistUuid
2285
2472
  });
@@ -2300,25 +2487,25 @@ function GenreTreeView({
2300
2487
  const canSizeByViews = !isArtistFiltered && hasViewsCount(visibleGenrePlaylists);
2301
2488
  const artistResult = useArtist2(artistUuid, artistSearchBaseUrl ?? getBackendBaseUrl);
2302
2489
  const filterArtistName = artistResult.status === "success" ? artistResult.artist.name : null;
2303
- const filterArtist = useMemo7(
2490
+ const filterArtist = useMemo8(
2304
2491
  () => artistUuid !== null && filterArtistName !== null ? { uuid: artistUuid, name: filterArtistName } : null,
2305
2492
  [artistUuid, filterArtistName]
2306
2493
  );
2307
2494
  const hasSelectedGenreNode = selectedGenrePlaylist !== void 0 && visibleGenrePlaylists.some(({ uuid }) => uuid === selectedGenrePlaylist.uuid);
2308
2495
  const playback = useGenrePlaylistPlayback({ scope, genrePlaylists: visibleGenrePlaylists, artist: filterArtist });
2309
2496
  const openArtist = (uuid) => {
2310
- if (selectedNodeId !== null) clearSelection();
2497
+ if (selectedGenreUuid !== null) clearSelection();
2311
2498
  if (songUuid !== null) changeSong(null);
2312
2499
  changeArtist(uuid);
2313
2500
  };
2314
2501
  const openSong = (uuid) => {
2315
- if (selectedNodeId !== null) clearSelection();
2502
+ if (selectedGenreUuid !== null) clearSelection();
2316
2503
  changeSong(uuid);
2317
2504
  };
2318
2505
  const openSongRef = useRef3(openSong);
2319
2506
  openSongRef.current = openSong;
2320
2507
  const resolvedRenderSongLink = useCallback6(
2321
- (song) => renderSongLink ? renderSongLink(song) : /* @__PURE__ */ jsx33("button", { type: "button", onClick: () => openSongRef.current(song.uuid), className: "hover:underline", children: song.title }),
2508
+ (song) => renderSongLink ? renderSongLink(song) : /* @__PURE__ */ jsx34("button", { type: "button", onClick: () => openSongRef.current(song.uuid), className: "hover:underline", children: song.title }),
2322
2509
  [renderSongLink]
2323
2510
  );
2324
2511
  const [expandedSongUuid, setExpandedSongUuid] = useState9(null);
@@ -2330,7 +2517,7 @@ function GenreTreeView({
2330
2517
  scope,
2331
2518
  getBackendBaseUrl: artistBaseUrl,
2332
2519
  getShareUrl: getShareUrlRef.current,
2333
- renderArtistLink: renderArtistLink ?? ((artist) => artist.isSpecialPurpose ? artist.name : /* @__PURE__ */ jsx33("button", { type: "button", onClick: () => openArtist(artist.uuid), className: "hover:underline", children: artist.name })),
2520
+ renderArtistLink: renderArtistLink ?? ((artist) => artist.isSpecialPurpose ? artist.name : /* @__PURE__ */ jsx34("button", { type: "button", onClick: () => openArtist(artist.uuid), className: "hover:underline", children: artist.name })),
2334
2521
  expanded: controlledArtistExpanded ?? (artistUuid !== null && expandedArtistUuid === artistUuid),
2335
2522
  onExpandedChange: (on) => {
2336
2523
  if (controlledArtistExpanded === void 0) setExpandedArtistUuid(on ? artistUuid : null);
@@ -2339,7 +2526,7 @@ function GenreTreeView({
2339
2526
  renderSongLink: resolvedRenderSongLink,
2340
2527
  onClose: () => changeArtist(null)
2341
2528
  });
2342
- const resolvedRenderArtistLink = renderArtistLink ?? ((artist) => artist.isSpecialPurpose ? artist.name : /* @__PURE__ */ jsx33("button", { type: "button", onClick: () => openArtist(artist.uuid), className: "hover:underline", children: artist.name }));
2529
+ const resolvedRenderArtistLink = renderArtistLink ?? ((artist) => artist.isSpecialPurpose ? artist.name : /* @__PURE__ */ jsx34("button", { type: "button", onClick: () => openArtist(artist.uuid), className: "hover:underline", children: artist.name }));
2343
2530
  const getGenrePath = (genre) => {
2344
2531
  const path = [];
2345
2532
  let current = visibleGenrePlaylists.find((playlist) => playlist.criteria?.uuid === genre.uuid);
@@ -2357,9 +2544,9 @@ function GenreTreeView({
2357
2544
  getShareUrl: getShareUrlRef.current,
2358
2545
  renderArtistLink: resolvedRenderArtistLink,
2359
2546
  getGenrePath,
2360
- onSelectGenre: (genrePlaylist) => {
2547
+ onSelectGenre: (genrePlaylist2) => {
2361
2548
  changeSong(null);
2362
- selectGenrePlaylist(genrePlaylist);
2549
+ selectGenrePlaylist(genrePlaylist2);
2363
2550
  },
2364
2551
  expanded: controlledSongExpanded ?? (songUuid !== null && expandedSongUuid === songUuid),
2365
2552
  onExpandedChange: (on) => {
@@ -2368,8 +2555,8 @@ function GenreTreeView({
2368
2555
  },
2369
2556
  onClose: () => changeSong(null)
2370
2557
  });
2371
- const builtInSongPanel = overlayPanel || hasSelectedGenreNode ? void 0 : songPanel;
2372
- const builtInArtistPanel = overlayPanel || hasSelectedGenreNode || builtInSongPanel ? void 0 : artistPanel;
2558
+ const builtInSongPanel = overlayPanel || selectedGenreUuid !== null ? void 0 : songPanel;
2559
+ const builtInArtistPanel = overlayPanel || selectedGenreUuid !== null || builtInSongPanel ? void 0 : artistPanel;
2373
2560
  const builtInPanel = builtInSongPanel ?? builtInArtistPanel;
2374
2561
  const [uncontrolledCollapsed, setUncontrolledCollapsed] = useState9(false);
2375
2562
  const [uncontrolledExpanded, setUncontrolledExpanded] = useState9(false);
@@ -2385,10 +2572,10 @@ function GenreTreeView({
2385
2572
  };
2386
2573
  useEffect5(() => {
2387
2574
  setUncontrolledCollapsed(false);
2388
- }, [selectedNodeId, artistUuid, songUuid]);
2575
+ }, [selectedGenreUuid, artistUuid, songUuid]);
2389
2576
  const [isWheelRevealed, setIsWheelRevealed] = useState9(false);
2390
2577
  const usesHandoff = viewMode === "wheel" || viewMode === "pop-core";
2391
- const isTreeReady = !isLoading && (!usesHandoff || isWheelRevealed);
2578
+ const isTreeReady = !isLoading && !isError && (!usesHandoff || isWheelRevealed);
2392
2579
  const [resolveStyle, setResolveStyle] = useState9(null);
2393
2580
  const handleNodeStyleResolverChange = useCallback6(
2394
2581
  (resolver) => setResolveStyle(() => resolver),
@@ -2398,140 +2585,99 @@ function GenreTreeView({
2398
2585
  const obscuredInset = useObscuredInset(panelRef);
2399
2586
  const hostPanel = { obscuredInset, onNodeStyleResolverChange: handleNodeStyleResolverChange };
2400
2587
  const nodeMetric = isArtistFiltered || !canSizeByViews ? "songs" : deferredSizeMetric;
2401
- const treeNodes = useMemo7(() => toTreeNodes(visibleGenrePlaylists, nodeMetric), [visibleGenrePlaylists, nodeMetric]);
2588
+ const treeNodes = useMemo8(() => toTreeNodes(visibleGenrePlaylists, nodeMetric), [visibleGenrePlaylists, nodeMetric]);
2402
2589
  const relations = useGenreNodeRelations(
2403
2590
  treeNodes,
2404
2591
  !overlayPanel && hasSelectedGenreNode ? selectedNodeId : null,
2405
2592
  resolveStyle
2406
2593
  );
2407
- const [selectionHistory, setSelectionHistory] = useState9({
2408
- current: selectedNodeId,
2409
- previous: null
2410
- });
2411
- if (selectionHistory.current !== selectedNodeId) {
2412
- setSelectionHistory({
2413
- current: selectedNodeId,
2414
- previous: selectionHistory.current ?? selectionHistory.previous
2415
- });
2594
+ const [selectionHistory, setSelectionHistory] = useState9({ current: selectedGenreUuid, previous: null });
2595
+ if (selectionHistory.current !== selectedGenreUuid) {
2596
+ setSelectionHistory({ current: selectedGenreUuid, previous: selectionHistory.current ?? selectionHistory.previous });
2416
2597
  }
2417
- const previousPlaylist = selectionHistory.previous !== null && selectionHistory.previous !== selectedNodeId ? genrePlaylistByNodeId.get(selectionHistory.previous) : void 0;
2418
- const handlePrevious = previousPlaylist && visibleGenrePlaylists.includes(previousPlaylist) ? () => handleGenreSearchSelect(previousPlaylist) : void 0;
2598
+ const handlePrevious = selectionHistory.previous ? () => selectGenre(selectionHistory.previous) : void 0;
2419
2599
  const { showPopup, hidePopup } = usePopup5();
2600
+ const reopenPanel = () => {
2601
+ setUncontrolledCollapsed(false);
2602
+ onCollapsedChange?.(false);
2603
+ };
2420
2604
  const handleSelectedNodeChange = (node) => {
2605
+ reopenPanel();
2421
2606
  if (node !== null && songUuid !== null) changeSong(null);
2422
2607
  selectNode(node);
2423
2608
  };
2424
2609
  const handleGenreSearchClear = clearSelection;
2425
- const handleGenreSearchSelect = (genrePlaylist) => {
2610
+ const handleGenreSearchSelect = (genrePlaylist2) => {
2611
+ reopenPanel();
2426
2612
  if (songUuid !== null) changeSong(null);
2427
- selectGenrePlaylist(genrePlaylist);
2613
+ selectGenrePlaylist(genrePlaylist2);
2428
2614
  };
2429
2615
  const selectedUuid = selectedGenrePlaylist?.uuid;
2430
- const selectedChildren = selectedUuid ? visibleGenrePlaylists.filter((genrePlaylist) => genrePlaylist.parent?.uuid === selectedUuid) : [];
2616
+ const selectedChildren = selectedUuid ? visibleGenrePlaylists.filter((genrePlaylist2) => genrePlaylist2.parent?.uuid === selectedUuid) : [];
2431
2617
  const canZoomIn = viewMode === "pop-core" && focus.onFocusGenreChange !== void 0 && selectedUuid !== void 0 && focus.focusGenreUuid !== selectedUuid && selectedChildren.length > 0 && selectedChildren.filter((child) => child.criteria?.side === "pop").length <= 1;
2432
2618
  const handleGenreSearchZoomIn = () => {
2433
2619
  if (selectedUuid) focus.onFocusGenreChange?.(selectedUuid, "zoom-in");
2434
2620
  };
2435
2621
  const zoomInRef = useRef3(void 0);
2436
2622
  zoomInRef.current = canZoomIn ? handleGenreSearchZoomIn : void 0;
2437
- const selectGenreFromPanel = (nodeId) => {
2438
- const genrePlaylist = genrePlaylistByNodeId.get(nodeId);
2439
- if (genrePlaylist) handleGenreSearchSelect(genrePlaylist);
2440
- };
2441
- const renderGenreBody = (relation) => {
2442
- const { node } = relation;
2443
- const family = /* @__PURE__ */ jsx33(
2444
- GenreNodeFamily,
2445
- {
2446
- node,
2447
- fill: relation.fill,
2448
- textColor: relation.textColor,
2449
- parentNode: relation.parentNode,
2450
- childNodes: relation.childNodes,
2451
- onSelectNode: selectGenreFromPanel
2452
- }
2453
- );
2454
- const ancestors = /* @__PURE__ */ jsx33(GenreNodeAncestors, { ancestorNodes: relation.ancestorNodes, onSelectNode: selectGenreFromPanel });
2455
- const genrePlaylist = genrePlaylistByNodeId.get(node.id);
2456
- const criteria = genrePlaylist?.criteria;
2457
- if (!genrePlaylist || !criteria) return { primary: /* @__PURE__ */ jsxs25(Fragment13, { children: [
2458
- family,
2459
- ancestors
2460
- ] }) };
2461
- const genreUuid = criteria.uuid;
2462
- const parentPlaylist = visibleGenrePlaylists.find(({ uuid }) => uuid === genrePlaylist.parent?.uuid);
2463
- const canToggleSide = !readOnly && parentPlaylist !== void 0 && parentPlaylist.parent === null;
2464
- const side = criteria.side ?? "core";
2465
- const shareTarget = {
2466
- kind: "genre",
2467
- genrePlaylistUuid: genrePlaylist.uuid,
2468
- name: criteria.name,
2469
- scope,
2470
- artistUuid
2471
- };
2472
- const shareUrl = getShareUrlRef.current?.(shareTarget) ?? void 0;
2473
- const popSibling = canToggleSide && side === "core" ? visibleGenrePlaylists.find(
2474
- (sibling) => sibling.parent?.uuid === parentPlaylist.uuid && sibling.uuid !== genrePlaylist.uuid && sibling.criteria?.side === "pop"
2475
- ) : void 0;
2476
- return {
2477
- primary: /* @__PURE__ */ jsxs25(Fragment13, { children: [
2478
- /* @__PURE__ */ jsx33(
2479
- GenreInfoPanelBody,
2623
+ const genrePlaylist = selectedGenrePlaylist;
2624
+ const criteria = genrePlaylist?.criteria;
2625
+ const parentPlaylist = visibleGenrePlaylists.find(({ uuid }) => uuid === genrePlaylist?.parent?.uuid);
2626
+ const canToggleSide = !readOnly && criteria !== void 0 && criteria !== null && parentPlaylist !== void 0 && parentPlaylist.parent === null;
2627
+ const side = criteria?.side ?? "core";
2628
+ const popSibling = canToggleSide && side === "core" ? visibleGenrePlaylists.find((sibling) => sibling.parent?.uuid === parentPlaylist.uuid && sibling.uuid !== genrePlaylist?.uuid && sibling.criteria?.side === "pop") : void 0;
2629
+ const shareTarget = selectedGenreUuid ? { kind: "genre", genreUuid: selectedGenreUuid, name: criteria?.name ?? "Genre", scope, artistUuid } : void 0;
2630
+ const shareUrl = shareTarget ? getShareUrlRef.current?.(shareTarget) ?? void 0 : void 0;
2631
+ const genreContent = useGenreDetailContent({
2632
+ genreUuid: selectedGenreUuid,
2633
+ scope,
2634
+ getBackendBaseUrl,
2635
+ onSelectGenre: (uuid) => {
2636
+ reopenPanel();
2637
+ selectGenre(uuid);
2638
+ },
2639
+ expanded: isExpanded,
2640
+ playback,
2641
+ genrePlaylist: selectedGenrePlaylist,
2642
+ band: {
2643
+ ...relations ? { node: relations.node } : {},
2644
+ shareTarget: shareUrl ? shareTarget : void 0,
2645
+ shareUrl,
2646
+ onZoomIn: zoomInRef.current,
2647
+ onAddChild: readOnly || !criteria ? void 0 : () => handleGenreCreationAction(criteria),
2648
+ onRename: readOnly || !criteria ? void 0 : () => handleGenreRenameAction(criteria),
2649
+ onChangeParent: readOnly || !genrePlaylist || !criteria ? void 0 : () => showPopup(
2650
+ /* @__PURE__ */ jsx34(
2651
+ ChangeParentPopup,
2480
2652
  {
2481
- family: /* @__PURE__ */ jsxs25(Fragment13, { children: [
2482
- family,
2483
- ancestors
2484
- ] }),
2485
- genreUuid,
2486
- genrePlaylist,
2487
- catalogue: genrePlaylist.catalogue,
2488
- wikidataId: criteria.wikidataId,
2653
+ genrePlaylist: { ...genrePlaylist, criteria },
2654
+ genrePlaylists: visibleGenrePlaylists,
2489
2655
  scope,
2490
2656
  getBackendBaseUrl,
2491
- playback,
2492
- band: {
2493
- node,
2494
- shareTarget: shareUrl ? shareTarget : void 0,
2495
- shareUrl,
2496
- onZoomIn: zoomInRef.current,
2497
- onAddChild: readOnly ? void 0 : () => handleGenreCreationAction(criteria),
2498
- onRename: readOnly ? void 0 : () => handleGenreRenameAction(criteria),
2499
- onChangeParent: readOnly ? void 0 : () => showPopup(
2500
- /* @__PURE__ */ jsx33(
2501
- ChangeParentPopup,
2502
- {
2503
- genrePlaylist: { ...genrePlaylist, criteria },
2504
- genrePlaylists: visibleGenrePlaylists,
2505
- scope,
2506
- getBackendBaseUrl,
2507
- onClose: hidePopup,
2508
- onParentChanged
2509
- }
2510
- )
2511
- ),
2512
- side,
2513
- onToggleSide: canToggleSide ? () => updateGenre({ uuid: genreUuid, data: { side: side === "pop" ? "core" : "pop" } }) : void 0,
2514
- toggleSideDisabledReason: popSibling ? `${popSibling.name} is already the pop side of ${parentPlaylist?.name}` : void 0,
2515
- additionalActions
2516
- },
2517
- extras: {
2518
- criteriaOverviewSchema,
2519
- renderGenreDetailExtras,
2520
- renderArtistLink,
2521
- renderSongLink: resolvedRenderSongLink,
2522
- artist: filterArtist,
2523
- renderSummary,
2524
- renderEssentialTracks,
2525
- renderSongLeading,
2526
- renderSongActions,
2527
- renderTracksToolbar
2528
- }
2657
+ onClose: hidePopup,
2658
+ onParentChanged
2529
2659
  }
2530
- ),
2531
- renderDangerZone?.(node)
2532
- ] })
2533
- };
2534
- };
2660
+ )
2661
+ ),
2662
+ side,
2663
+ onToggleSide: canToggleSide && criteria ? () => updateGenre({ uuid: criteria.uuid, data: { side: side === "pop" ? "core" : "pop" } }) : void 0,
2664
+ toggleSideDisabledReason: popSibling ? `${popSibling.name} is already the pop side of ${parentPlaylist?.name}` : void 0,
2665
+ additionalActions
2666
+ },
2667
+ extras: {
2668
+ criteriaOverviewSchema,
2669
+ renderGenreDetailExtras,
2670
+ renderArtistLink,
2671
+ renderSongLink: resolvedRenderSongLink,
2672
+ artist: filterArtist,
2673
+ renderSummary,
2674
+ renderEssentialTracks,
2675
+ renderSongLeading,
2676
+ renderSongActions,
2677
+ renderTracksToolbar
2678
+ },
2679
+ trailing: relations ? renderDangerZone?.(relations.node) : void 0
2680
+ });
2535
2681
  const isRotationControlled = controlledAllowWheelRotation !== void 0;
2536
2682
  const allowWheelRotation = controlledAllowWheelRotation ?? internalAllowWheelRotation;
2537
2683
  let activePanel = null;
@@ -2545,16 +2691,16 @@ function GenreTreeView({
2545
2691
  onClose: overlayPanel.onClose,
2546
2692
  compact: false
2547
2693
  };
2548
- } else if (relations) {
2549
- const { primary } = renderGenreBody(relations);
2694
+ } else if (selectedGenreUuid) {
2550
2695
  activePanel = {
2551
- title: relations.node.name,
2552
- content: primary,
2696
+ title: genreContent.title,
2697
+ content: genreContent.content,
2698
+ aside: genreContent.aside,
2553
2699
  expanded: isExpanded,
2554
2700
  onExpandedChange: changeExpanded,
2555
2701
  onClose: clearSelection,
2556
- compact: true,
2557
- headerAccent: { background: relations.fill, color: relations.textColor }
2702
+ compact: false,
2703
+ headerAccent: relations ? { background: relations.fill, color: relations.textColor } : genreContent.headerAccent
2558
2704
  };
2559
2705
  } else if (builtInPanel) {
2560
2706
  activePanel = {
@@ -2567,36 +2713,36 @@ function GenreTreeView({
2567
2713
  compact: true
2568
2714
  };
2569
2715
  }
2570
- const isPanelShown = isTreeReady && activePanel !== null;
2716
+ const isPanelShown = activePanel !== null && (selectedGenreUuid !== null || isTreeReady);
2571
2717
  const detailTab = placement === "regions" && activePanel?.onExpandedChange ? {
2572
2718
  edge: activePanel.expanded ? { label: "Shrink panel", onClick: () => activePanel.onExpandedChange?.(false) } : isCollapsed ? null : { label: "Hide panel", onClick: () => changeCollapsed(true) },
2573
2719
  center: activePanel.expanded ? null : isCollapsed ? { label: "Show panel", onClick: () => changeCollapsed(false) } : activePanel.onExpandedChange ? { label: "Extend panel", onClick: () => activePanel.onExpandedChange?.(true) } : null
2574
2720
  } : void 0;
2575
- const expandButton = placement === "default" && activePanel?.onExpandedChange ? /* @__PURE__ */ jsx33(
2576
- IconButton,
2721
+ const expandButton = placement === "default" && activePanel?.onExpandedChange ? /* @__PURE__ */ jsx34(
2722
+ IconButton2,
2577
2723
  {
2578
2724
  "aria-label": activePanel.expanded ? "Shrink panel" : "Extend panel",
2579
2725
  style: activePanel.headerAccent && { color: "inherit" },
2580
2726
  onClick: () => activePanel.onExpandedChange?.(!activePanel.expanded),
2581
- children: activePanel.expanded ? /* @__PURE__ */ jsx33(Minimize2, { "aria-hidden": "true", className: "h-4 w-4" }) : /* @__PURE__ */ jsx33(Maximize2, { "aria-hidden": "true", className: "h-4 w-4" })
2727
+ children: activePanel.expanded ? /* @__PURE__ */ jsx34(Minimize22, { "aria-hidden": "true", className: "h-4 w-4" }) : /* @__PURE__ */ jsx34(Maximize22, { "aria-hidden": "true", className: "h-4 w-4" })
2582
2728
  }
2583
2729
  ) : null;
2584
2730
  const renderArtistFilter = () => {
2585
2731
  if (isLoading || !filterArtist) return null;
2586
- return /* @__PURE__ */ jsxs25("span", { className: "inline-flex items-center overflow-hidden rounded-full bg-[var(--color-neutral-100)] text-sm", children: [
2587
- /* @__PURE__ */ jsx33(
2732
+ return /* @__PURE__ */ jsxs26("span", { className: "inline-flex items-center overflow-hidden rounded-full bg-[var(--color-neutral-100)] text-sm", children: [
2733
+ /* @__PURE__ */ jsx34(
2588
2734
  "button",
2589
2735
  {
2590
2736
  type: "button",
2591
2737
  className: "py-1 pl-3 pr-2 [@media(pointer:coarse)]:min-h-[var(--layout-touch-target)] hover:underline",
2592
2738
  "aria-label": `Show artist ${filterArtist.name}`,
2593
2739
  onClick: () => {
2594
- if (selectedNodeId !== null) clearSelection();
2740
+ if (selectedGenreUuid !== null) clearSelection();
2595
2741
  },
2596
2742
  children: filterArtist.name
2597
2743
  }
2598
2744
  ),
2599
- /* @__PURE__ */ jsx33(
2745
+ /* @__PURE__ */ jsx34(
2600
2746
  "button",
2601
2747
  {
2602
2748
  type: "button",
@@ -2608,8 +2754,8 @@ function GenreTreeView({
2608
2754
  )
2609
2755
  ] });
2610
2756
  };
2611
- const actions = /* @__PURE__ */ jsxs25(Fragment13, { children: [
2612
- !isLoading && /* @__PURE__ */ jsx33(
2757
+ const actions = /* @__PURE__ */ jsxs26(Fragment14, { children: [
2758
+ !isLoading && !isError && /* @__PURE__ */ jsx34(
2613
2759
  GenreTreeSearch,
2614
2760
  {
2615
2761
  artistBaseUrl,
@@ -2627,7 +2773,12 @@ function GenreTreeView({
2627
2773
  SearchGroups,
2628
2774
  genrePlaylists: visibleGenrePlaylists,
2629
2775
  onSelect: handleGenreSearchSelect,
2630
- selectedName: selectedGenrePlaylist?.name ?? (typeof builtInPanel?.title === "string" ? builtInPanel.title : null),
2776
+ onSelectGenre: (uuid) => {
2777
+ reopenPanel();
2778
+ if (songUuid) changeSong(null);
2779
+ selectGenre(uuid);
2780
+ },
2781
+ selectedName: (selectedGenreUuid ? genreContent.title : null) ?? (typeof builtInPanel?.title === "string" ? builtInPanel.title : null),
2631
2782
  onClear: builtInSongPanel ? () => changeSong(null) : builtInArtistPanel ? () => changeArtist(null) : handleGenreSearchClear,
2632
2783
  onPrevious: handlePrevious,
2633
2784
  onQueryChange: onSearchQueryChange,
@@ -2635,9 +2786,9 @@ function GenreTreeView({
2635
2786
  extraGroups: extraSearchGroups
2636
2787
  }
2637
2788
  ),
2638
- !isLoading && filterArtist && /* @__PURE__ */ jsx33("div", { className: "hidden w-full justify-center max-md:flex", children: renderArtistFilter() }),
2639
- /* @__PURE__ */ jsxs25("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: [
2640
- !isLoading && !isControlled && /* @__PURE__ */ jsx33(
2789
+ !isLoading && !isError && filterArtist && /* @__PURE__ */ jsx34("div", { className: "hidden w-full justify-center max-md:flex", children: renderArtistFilter() }),
2790
+ /* @__PURE__ */ jsxs26("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: [
2791
+ !isLoading && !isError && !isControlled && /* @__PURE__ */ jsx34(
2641
2792
  SegmentedControl2,
2642
2793
  {
2643
2794
  "aria-label": "Tree view mode",
@@ -2651,13 +2802,13 @@ function GenreTreeView({
2651
2802
  ]
2652
2803
  }
2653
2804
  ),
2654
- !isLoading && showDisplayOptions && /* @__PURE__ */ jsx33(
2805
+ !isLoading && !isError && showDisplayOptions && /* @__PURE__ */ jsx34(
2655
2806
  "div",
2656
2807
  {
2657
2808
  className: "flex items-center gap-1",
2658
2809
  role: "group",
2659
2810
  "aria-label": "Tree display options",
2660
- children: !isRotationControlled && viewMode !== "stacked" && viewMode !== "outline" && /* @__PURE__ */ jsx33(
2811
+ children: !isRotationControlled && viewMode !== "stacked" && viewMode !== "outline" && /* @__PURE__ */ jsx34(
2661
2812
  Button,
2662
2813
  {
2663
2814
  variant: allowWheelRotation ? "default" : "outline",
@@ -2668,7 +2819,7 @@ function GenreTreeView({
2668
2819
  )
2669
2820
  }
2670
2821
  ),
2671
- !isLoading && !readOnly && showAddRoot && /* @__PURE__ */ jsx33(
2822
+ !isLoading && !isError && !readOnly && showAddRoot && /* @__PURE__ */ jsx34(
2672
2823
  IconTextButton,
2673
2824
  {
2674
2825
  icon: Plus2,
@@ -2678,34 +2829,38 @@ function GenreTreeView({
2678
2829
  )
2679
2830
  ] })
2680
2831
  ] });
2681
- return /* @__PURE__ */ jsxs25(
2832
+ return /* @__PURE__ */ jsxs26(
2682
2833
  "div",
2683
2834
  {
2684
2835
  "data-left-panel": isPanelShown && !isCollapsed ? void 0 : "hidden",
2685
2836
  className: ["genre-tree-view relative flex flex-col h-full", placement === "regions" && REGIONS_CLASS, className2].filter(Boolean).join(" "),
2686
2837
  children: [
2687
- /* @__PURE__ */ jsx33(
2838
+ /* @__PURE__ */ jsx34(
2688
2839
  "div",
2689
2840
  {
2690
2841
  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"}`,
2691
2842
  children: actions
2692
2843
  }
2693
2844
  ),
2694
- !isLoading && filterArtist && /* @__PURE__ */ jsx33(
2845
+ !isLoading && !isError && filterArtist && /* @__PURE__ */ jsx34(
2695
2846
  "div",
2696
2847
  {
2697
2848
  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"}`,
2698
2849
  children: renderArtistFilter()
2699
2850
  }
2700
2851
  ),
2701
- /* @__PURE__ */ jsx33("div", { className: "content-container flex-1 min-h-0 flex flex-row gap-4", children: /* @__PURE__ */ jsxs25("div", { className: "tree-view-container relative flex-1 min-w-0 flex flex-col h-full", children: [
2702
- !isLoading && viewMode === "outline" && floatingControlsLeading && /* @__PURE__ */ jsx33("div", { className: "absolute bottom-3 right-3 z-10", children: floatingControlsLeading }),
2703
- isLoading ? /* @__PURE__ */ jsx33(GenreTreeViewSkeleton, { viewMode }) : viewMode === "wheel" ? (
2852
+ /* @__PURE__ */ jsx34("div", { className: "content-container flex-1 min-h-0 flex flex-row gap-4", children: /* @__PURE__ */ jsxs26("div", { className: "tree-view-container relative flex-1 min-w-0 flex flex-col h-full", children: [
2853
+ !isLoading && !isError && viewMode === "outline" && floatingControlsLeading && /* @__PURE__ */ jsx34("div", { className: "absolute bottom-3 right-3 z-10", children: floatingControlsLeading }),
2854
+ isLoading ? /* @__PURE__ */ jsx34(GenreTreeViewSkeleton, { viewMode }) : isError ? /* @__PURE__ */ jsxs26("p", { role: "alert", className: "m-auto text-center", children: [
2855
+ "Couldn't load the genre tree.",
2856
+ " ",
2857
+ /* @__PURE__ */ jsx34("button", { type: "button", className: "underline", onClick: refetch, children: "Retry" })
2858
+ ] }) : viewMode === "wheel" ? (
2704
2859
  // The info panel's top edge aligns with the tree's; the search bar floats `--layout-panel-inset` below it,
2705
2860
  // and the header inset (the bar's height plus that inset) extends the header's fill behind the bar,
2706
2861
  // Google Maps style; the 8px bottom padding mirrors the inset above the bar, and the hidden title's height is zeroed.
2707
2862
  // The panel's own Zoom in button is hidden: the search bar carries it.
2708
- /* @__PURE__ */ jsx33("div", { className: "tree-container search-overlay relative flex-1 w-full min-h-0 ", children: /* @__PURE__ */ jsx33(GenreTreeWheelHandoff, { skeleton: /* @__PURE__ */ jsx33(GenreTreeWheelSkeleton, {}), onRevealedChange: setIsWheelRevealed, children: /* @__PURE__ */ jsx33(
2863
+ /* @__PURE__ */ jsx34("div", { className: "tree-container search-overlay relative flex-1 w-full min-h-0 ", children: /* @__PURE__ */ jsx34(GenreTreeWheelHandoff, { skeleton: /* @__PURE__ */ jsx34(GenreTreeWheelSkeleton, {}), onRevealedChange: setIsWheelRevealed, children: /* @__PURE__ */ jsx34(
2709
2864
  GenrePlaylistTreeWheel,
2710
2865
  {
2711
2866
  scope,
@@ -2722,9 +2877,9 @@ function GenreTreeView({
2722
2877
  allowWheelRotation,
2723
2878
  sizeMetric: isArtistFiltered || !canSizeByViews ? "songs" : deferredSizeMetric,
2724
2879
  busy,
2725
- floatingControlsLeading: floatingControlsLeading || canSizeByViews ? /* @__PURE__ */ jsxs25("div", { className: "flex items-center gap-2", children: [
2880
+ floatingControlsLeading: floatingControlsLeading || canSizeByViews ? /* @__PURE__ */ jsxs26("div", { className: "flex items-center gap-2", children: [
2726
2881
  floatingControlsLeading,
2727
- canSizeByViews && /* @__PURE__ */ jsx33(SizeMetricToggle, { value: sizeMetric, onChange: changeSizeMetric })
2882
+ canSizeByViews && /* @__PURE__ */ jsx34(SizeMetricToggle, { value: sizeMetric, onChange: changeSizeMetric })
2728
2883
  ] }) : void 0,
2729
2884
  defaultViewport,
2730
2885
  viewportRestore,
@@ -2733,7 +2888,7 @@ function GenreTreeView({
2733
2888
  ) }) })
2734
2889
  ) : viewMode === "pop-core" ? (
2735
2890
  // Same as the wheel: the header inset extends its fill behind the floating search bar, Google Maps style.
2736
- /* @__PURE__ */ jsx33("div", { className: "tree-container search-overlay relative flex-1 w-full min-h-0 ", children: /* @__PURE__ */ jsx33(GenreTreeWheelHandoff, { skeleton: /* @__PURE__ */ jsx33(GenreTreeWheelSkeleton, {}), onRevealedChange: setIsWheelRevealed, children: /* @__PURE__ */ jsx33(
2891
+ /* @__PURE__ */ jsx34("div", { className: "tree-container search-overlay relative flex-1 w-full min-h-0 ", children: /* @__PURE__ */ jsx34(GenreTreeWheelHandoff, { skeleton: /* @__PURE__ */ jsx34(GenreTreeWheelSkeleton, {}), onRevealedChange: setIsWheelRevealed, children: /* @__PURE__ */ jsx34(
2737
2892
  GenrePlaylistTreeWheelRadialPopCore,
2738
2893
  {
2739
2894
  scope,
@@ -2750,9 +2905,9 @@ function GenreTreeView({
2750
2905
  allowWheelRotation,
2751
2906
  sizeMetric: isArtistFiltered || !canSizeByViews ? "songs" : deferredSizeMetric,
2752
2907
  busy,
2753
- floatingControlsLeading: floatingControlsLeading || canSizeByViews ? /* @__PURE__ */ jsxs25("div", { className: "flex items-center gap-2", children: [
2908
+ floatingControlsLeading: floatingControlsLeading || canSizeByViews ? /* @__PURE__ */ jsxs26("div", { className: "flex items-center gap-2", children: [
2754
2909
  floatingControlsLeading,
2755
- canSizeByViews && /* @__PURE__ */ jsx33(SizeMetricToggle, { value: sizeMetric, onChange: changeSizeMetric })
2910
+ canSizeByViews && /* @__PURE__ */ jsx34(SizeMetricToggle, { value: sizeMetric, onChange: changeSizeMetric })
2756
2911
  ] }) : void 0,
2757
2912
  defaultViewport,
2758
2913
  viewportRestore,
@@ -2760,7 +2915,7 @@ function GenreTreeView({
2760
2915
  ...focus
2761
2916
  }
2762
2917
  ) }) })
2763
- ) : viewMode === "outline" ? /* @__PURE__ */ jsx33("div", { className: "tree-container flex-1 min-h-0 w-full overflow-y-auto relative", children: /* @__PURE__ */ jsx33(
2918
+ ) : viewMode === "outline" ? /* @__PURE__ */ jsx34("div", { className: "tree-container flex-1 min-h-0 w-full overflow-y-auto relative", children: /* @__PURE__ */ jsx34(
2764
2919
  GenrePlaylistTreeWheelRadialPopCore,
2765
2920
  {
2766
2921
  outline: true,
@@ -2776,13 +2931,13 @@ function GenreTreeView({
2776
2931
  selectedNodeId,
2777
2932
  readOnly
2778
2933
  }
2779
- ) }) : /* @__PURE__ */ jsx33("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(
2934
+ ) }) : /* @__PURE__ */ jsx34("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(
2780
2935
  ([uuid, genrePlaylistTreePerRoot]) => {
2781
- return /* @__PURE__ */ jsx33(
2936
+ return /* @__PURE__ */ jsx34(
2782
2937
  "div",
2783
2938
  {
2784
2939
  className: "tree-per-root-container relative shrink-0 h-[28rem] mt-2 mr-16 p-2 bg-[var(--color-neutral-100)] rounded-lg",
2785
- children: /* @__PURE__ */ jsx33("div", { className: "graph-container relative z-10 h-full", children: /* @__PURE__ */ jsx33(
2940
+ children: /* @__PURE__ */ jsx34("div", { className: "graph-container relative z-10 h-full", children: /* @__PURE__ */ jsx34(
2786
2941
  GenrePlaylistTreePerRoot,
2787
2942
  {
2788
2943
  scope,
@@ -2808,8 +2963,8 @@ function GenreTreeView({
2808
2963
  );
2809
2964
  }
2810
2965
  ) }),
2811
- /* @__PURE__ */ jsx33(
2812
- DetailPanel,
2966
+ /* @__PURE__ */ jsx34(
2967
+ DetailPanel2,
2813
2968
  {
2814
2969
  ref: panelRef,
2815
2970
  side: "left",
@@ -2819,13 +2974,13 @@ function GenreTreeView({
2819
2974
  headerAccent: activePanel?.headerAccent,
2820
2975
  headerActions: expandButton,
2821
2976
  onClose: activePanel?.onClose ?? noop,
2822
- collapsed: isCollapsed || !isTreeReady || !activePanel,
2977
+ collapsed: isCollapsed || !isPanelShown,
2823
2978
  expanded: activePanel?.expanded,
2824
2979
  onExpandedChange: activePanel?.onExpandedChange,
2825
2980
  aside: activePanel?.aside,
2826
2981
  edgeTab: detailTab,
2827
- headerClassName: activePanel?.compact ? "pt-[calc(var(--layout-search-h)+var(--layout-panel-inset)+0.5rem)]" : void 0,
2828
- children: activePanel?.aside && !activePanel.expanded ? /* @__PURE__ */ jsxs25(Fragment13, { children: [
2982
+ headerClassName: "pt-[calc(var(--layout-search-h)+var(--layout-panel-inset)+0.5rem)]",
2983
+ children: activePanel?.aside && !activePanel.expanded ? /* @__PURE__ */ jsxs26(Fragment14, { children: [
2829
2984
  activePanel.content,
2830
2985
  activePanel.aside
2831
2986
  ] }) : activePanel?.content
@@ -2837,145 +2992,6 @@ function GenreTreeView({
2837
2992
  );
2838
2993
  }
2839
2994
 
2840
- // src/genre-tree/GenreDetailPanel.tsx
2841
- import { useMemo as useMemo8 } from "react";
2842
- import {
2843
- ACCENT_TEXT_COLOR,
2844
- GenreNodeAncestors as GenreNodeAncestors2,
2845
- GenreNodeFamily as GenreNodeFamily2,
2846
- getGenreTreeColor as getGenreTreeColor2,
2847
- TEXT_COLOR,
2848
- tintSurface
2849
- } from "@behindthemusictree/genre-tree-view";
2850
- import { DetailPanel as DetailPanel2, IconButton as IconButton2, Skeleton as Skeleton7 } from "@behindthemusictree/ui";
2851
- import { Maximize2 as Maximize22, Minimize2 as Minimize22 } from "lucide-react";
2852
- import {
2853
- useFetchGenreDetail,
2854
- genreMusicCount as genreMusicCount2,
2855
- GenrePlaylistSummarySchema
2856
- } from "@behindthemusictree/app-kit/genre-tree";
2857
- import { Fragment as Fragment14, jsx as jsx34, jsxs as jsxs26 } from "react/jsx-runtime";
2858
- var RELATIVE_ITEM_COUNT = 1;
2859
- function toNode(genre, parentId) {
2860
- return {
2861
- id: genre.uuid,
2862
- parentId,
2863
- name: genre.name,
2864
- itemCount: RELATIVE_ITEM_COUNT,
2865
- side: genre.side ?? void 0,
2866
- wikidataId: genre.wikidataId
2867
- };
2868
- }
2869
- function GenreDetailPanel({
2870
- genreUuid,
2871
- scope,
2872
- getBackendBaseUrl,
2873
- onClose,
2874
- onSelectGenre,
2875
- onZoomIn,
2876
- shareUrl,
2877
- shareTarget,
2878
- collapsed,
2879
- expanded,
2880
- onExpandedChange,
2881
- edgeTab,
2882
- extras
2883
- }) {
2884
- const { data: detail, isPending, isError } = useFetchGenreDetail(genreUuid, scope, getBackendBaseUrl);
2885
- const playback = useGenrePlaylistPlayback({ scope, genrePlaylists: [] });
2886
- const view = useMemo8(() => {
2887
- if (!detail) return null;
2888
- const rootColor = getGenreTreeColor2(detail.root.wikidataId);
2889
- const relative = (genre, parentId) => ({
2890
- node: toNode(genre, parentId),
2891
- fill: tintSurface(rootColor),
2892
- textColor: TEXT_COLOR
2893
- });
2894
- const ancestors2 = [...detail.ascendants].sort((a, b) => b.degree - a.degree);
2895
- return {
2896
- node: {
2897
- id: detail.uuid,
2898
- parentId: detail.parent?.uuid ?? null,
2899
- name: detail.name,
2900
- itemCount: scope === "reference" ? genreMusicCount2(GenrePlaylistSummarySchema.parse(detail.criteriaPlaylist)) : detail.tracksCount,
2901
- wikidataId: detail.wikidataId
2902
- },
2903
- fill: rootColor,
2904
- parentNode: detail.parent ? relative(detail.parent, null) : null,
2905
- childNodes: detail.children.map((child) => relative(child, detail.uuid)),
2906
- ancestorNodes: ancestors2.map(
2907
- ({ ascendant }, index) => relative(ascendant, index === 0 ? null : ancestors2[index - 1].ascendant.uuid)
2908
- )
2909
- };
2910
- }, [detail, scope]);
2911
- if (isError) {
2912
- return /* @__PURE__ */ jsx34("div", { role: "alert", className: "p-4 text-sm text-[var(--color-text)]", children: "This genre could not be loaded." });
2913
- }
2914
- if (isPending || !detail || !view) {
2915
- return /* @__PURE__ */ jsxs26("div", { className: "flex flex-col gap-3 p-4", "aria-busy": "true", "data-testid": "genre-detail-panel-skeleton", children: [
2916
- /* @__PURE__ */ jsx34(Skeleton7, { className: "h-6 w-2/3" }),
2917
- /* @__PURE__ */ jsx34(Skeleton7, { className: "h-4 w-full" }),
2918
- /* @__PURE__ */ jsx34(Skeleton7, { className: "h-4 w-3/4" })
2919
- ] });
2920
- }
2921
- const selectGenre = (nodeId) => onSelectGenre(nodeId);
2922
- const family = /* @__PURE__ */ jsx34(
2923
- GenreNodeFamily2,
2924
- {
2925
- node: view.node,
2926
- fill: view.fill,
2927
- textColor: ACCENT_TEXT_COLOR,
2928
- parentNode: view.parentNode,
2929
- childNodes: view.childNodes,
2930
- onSelectNode: selectGenre
2931
- }
2932
- );
2933
- const ancestors = /* @__PURE__ */ jsx34(GenreNodeAncestors2, { ancestorNodes: view.ancestorNodes, onSelectNode: selectGenre });
2934
- return /* @__PURE__ */ jsxs26(
2935
- DetailPanel2,
2936
- {
2937
- side: "left",
2938
- title: view.node.name,
2939
- headerAccent: { background: view.fill, color: ACCENT_TEXT_COLOR },
2940
- headerActions: onExpandedChange ? /* @__PURE__ */ jsx34(
2941
- IconButton2,
2942
- {
2943
- "aria-label": expanded ? "Shrink panel" : "Extend panel",
2944
- style: { color: "inherit" },
2945
- onClick: () => onExpandedChange(!expanded),
2946
- children: expanded ? /* @__PURE__ */ jsx34(Minimize22, { "aria-hidden": "true", className: "h-4 w-4" }) : /* @__PURE__ */ jsx34(Maximize22, { "aria-hidden": "true", className: "h-4 w-4" })
2947
- }
2948
- ) : void 0,
2949
- onClose,
2950
- collapsed,
2951
- expanded,
2952
- onExpandedChange,
2953
- edgeTab,
2954
- children: [
2955
- /* @__PURE__ */ jsx34(
2956
- GenreInfoPanelBody,
2957
- {
2958
- family: /* @__PURE__ */ jsxs26(Fragment14, { children: [
2959
- family,
2960
- ancestors
2961
- ] }),
2962
- genreUuid: detail.uuid,
2963
- genrePlaylist: scope === "reference" ? GenrePlaylistSummarySchema.parse(detail.criteriaPlaylist) : { ...detail.criteriaPlaylist, tracksCount: detail.tracksCount },
2964
- catalogue: detail.catalogue,
2965
- wikidataId: detail.wikidataId,
2966
- scope,
2967
- getBackendBaseUrl,
2968
- playback,
2969
- band: { node: view.node, onZoomIn, shareUrl, shareTarget: shareUrl ? shareTarget : void 0 },
2970
- extras
2971
- }
2972
- ),
2973
- !expanded && ancestors
2974
- ]
2975
- }
2976
- );
2977
- }
2978
-
2979
2995
  // src/genre-tree/index.ts
2980
2996
  import {
2981
2997
  GenreTreeSkeleton,
@@ -3433,10 +3449,10 @@ export {
3433
3449
  GenreSearch,
3434
3450
  GenreNodeActionBand,
3435
3451
  GenreInfoPanelBody,
3452
+ GenreDetailPanel,
3436
3453
  TrackPositionPlayPause,
3437
3454
  TrackItem,
3438
3455
  GenreTreeView,
3439
- GenreDetailPanel,
3440
3456
  TrackListSidebar,
3441
3457
  PlayerPanel,
3442
3458
  Rating,
@@ -3445,4 +3461,4 @@ export {
3445
3461
  GenreTreeViewSkeleton2 as GenreTreeViewSkeleton,
3446
3462
  GenreTreeOutlineSkeleton
3447
3463
  };
3448
- //# sourceMappingURL=chunk-JP7PID7L.js.map
3464
+ //# sourceMappingURL=chunk-RTXLUHJY.js.map