@behindthemusictree/app-kit-ui 30.0.0 → 32.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.cjs CHANGED
@@ -36,7 +36,6 @@ __export(src_exports, {
36
36
  GenreTreeWheelSkeleton: () => import_genre_tree_view6.GenreTreeWheelSkeleton,
37
37
  InternalErrorPopup: () => InternalErrorPopup,
38
38
  NetworkErrorPopup: () => NetworkErrorPopup,
39
- PanelEdgeTab: () => PanelEdgeTab,
40
39
  PlayerPanel: () => PlayerPanel,
41
40
  PopupButtons: () => PopupButtons,
42
41
  PopupTitle: () => PopupTitle,
@@ -47,8 +46,7 @@ __export(src_exports, {
47
46
  TrackListSidebar: () => TrackListSidebar,
48
47
  TrackPositionPlayPause: () => TrackPositionPlayPause,
49
48
  TrackUploadPopup: () => TrackUploadPopup,
50
- useGenrePlaylistPlayback: () => useGenrePlaylistPlayback,
51
- useLeftDetailPanelLayout: () => useLeftDetailPanelLayout
49
+ useGenrePlaylistPlayback: () => useGenrePlaylistPlayback
52
50
  });
53
51
  module.exports = __toCommonJS(src_exports);
54
52
 
@@ -580,8 +578,9 @@ function AuthCallbackHandler({ renderStatus, ...options }) {
580
578
  }
581
579
 
582
580
  // src/genre-tree/GenreTreeView.tsx
583
- var import_react17 = require("react");
581
+ var import_react16 = require("react");
584
582
  var import_react_query3 = require("@tanstack/react-query");
583
+ var import_lucide_react15 = require("lucide-react");
585
584
  var import_lucide_react16 = require("lucide-react");
586
585
  var import_ui18 = require("@behindthemusictree/ui");
587
586
  var import_genre_tree_view4 = require("@behindthemusictree/genre-tree-view");
@@ -589,6 +588,32 @@ var import_genre_tree18 = require("@behindthemusictree/app-kit/genre-tree");
589
588
  var import_genre_tree19 = require("@behindthemusictree/app-kit/genre-tree");
590
589
  var import_popup6 = require("@behindthemusictree/app-kit/popup");
591
590
 
591
+ // src/genre-tree/playlist-tree/size-metric.ts
592
+ var hasViewsCount = (genrePlaylists) => genrePlaylists.every((genrePlaylist) => genrePlaylist.viewsCount !== void 0);
593
+ var genrePlaylistSize = (genrePlaylist, metric) => {
594
+ if (metric === "songs") return genrePlaylist.tracksCount;
595
+ if (genrePlaylist.viewsCount === void 0) {
596
+ throw new Error(`Genre playlist ${genrePlaylist.uuid} has no viewsCount to size by views`);
597
+ }
598
+ return genrePlaylist.viewsCount;
599
+ };
600
+
601
+ // src/genre-tree/playlist-tree/toTreeNodes.ts
602
+ function toTreeNodes(genrePlaylists, sizeMetric) {
603
+ return genrePlaylists.map((genrePlaylist) => ({
604
+ id: genrePlaylist.uuid,
605
+ parentId: genrePlaylist.parent?.uuid ?? null,
606
+ name: genrePlaylist.name,
607
+ itemCount: genrePlaylistSize(genrePlaylist, sizeMetric),
608
+ actionable: Boolean(genrePlaylist.criteria),
609
+ detached: !genrePlaylist.criteria,
610
+ side: genrePlaylist.criteria?.side ?? void 0,
611
+ wikidataId: genrePlaylist.criteria?.wikidataId ?? null,
612
+ umbrella: genrePlaylist.isUmbrella,
613
+ scene: genrePlaylist.scene ?? void 0
614
+ }));
615
+ }
616
+
592
617
  // src/genre-tree/playlist-tree/TreePerRoot.tsx
593
618
  var import_react4 = require("react");
594
619
  var import_genre_tree_view = require("@behindthemusictree/genre-tree-view");
@@ -625,7 +650,6 @@ function useGenrePlaylistPlayback({
625
650
  }
626
651
 
627
652
  // src/genre-tree/playlist-tree/TreePerRoot.tsx
628
- var import_track_noun2 = require("@behindthemusictree/app-kit/track-noun");
629
653
  var import_jsx_runtime12 = require("react/jsx-runtime");
630
654
  function GenrePlaylistTreePerRoot({
631
655
  scope,
@@ -637,24 +661,19 @@ function GenrePlaylistTreePerRoot({
637
661
  additionalActions,
638
662
  onNodeClick,
639
663
  onNodeHover,
640
- renderInfoPanelBody,
641
664
  selectedNodeId,
642
665
  onSelectedNodeChange,
643
- hideInfoPanelClose,
644
- hideInfoPanelTitle,
645
666
  readOnly = false,
646
- showToolbar,
647
667
  wheelZoom,
648
668
  defaultViewport,
649
669
  viewportRestore,
650
670
  onViewportChange,
651
671
  playback,
652
- ...infoPanelCuration
672
+ ...hostPanel
653
673
  }) {
654
674
  const { isPlaying, setIsPlaying } = (0, import_player.usePlayer)();
655
675
  const defaultPlayback = useGenrePlaylistPlayback({ scope, genrePlaylists: genrePlaylistTreePerRoot });
656
676
  const { playingNodeId, play } = playback ?? defaultPlayback;
657
- const trackNoun = (0, import_track_noun2.useTrackNoun)();
658
677
  const { showPopup } = (0, import_popup2.usePopup)();
659
678
  const nodes = (0, import_react4.useMemo)(
660
679
  () => genrePlaylistTreePerRoot.map((genrePlaylist) => ({
@@ -719,7 +738,6 @@ function GenrePlaylistTreePerRoot({
719
738
  {
720
739
  className: className2,
721
740
  nodes,
722
- itemCountLabel: `${(0, import_track_noun2.capitalize)(trackNoun.singular)} count`,
723
741
  rootColor: (0, import_genre_tree_view.getGenreTreeColor)(rootUuid),
724
742
  playingNodeId,
725
743
  playState: isPlaying ? "playing" : "paused",
@@ -730,18 +748,13 @@ function GenrePlaylistTreePerRoot({
730
748
  additionalActions,
731
749
  onNodeClick,
732
750
  onNodeHover,
733
- renderInfoPanelBody,
734
751
  selectedNodeId,
735
752
  onSelectedNodeChange,
736
- hideInfoPanelClose,
737
- hideInfoPanelTitle,
738
- showToolbar,
739
753
  wheelZoom,
740
754
  defaultViewport,
741
755
  viewportRestore,
742
756
  onViewportChange,
743
- ...infoPanelCuration,
744
- readOnly
757
+ ...hostPanel
745
758
  }
746
759
  );
747
760
  }
@@ -752,19 +765,6 @@ var import_genre_tree_view2 = require("@behindthemusictree/genre-tree-view");
752
765
  var import_player2 = require("@behindthemusictree/app-kit/player");
753
766
  var import_popup3 = require("@behindthemusictree/app-kit/popup");
754
767
  var import_transport3 = require("@behindthemusictree/app-kit/transport");
755
-
756
- // src/genre-tree/playlist-tree/size-metric.ts
757
- var hasViewsCount = (genrePlaylists) => genrePlaylists.every((genrePlaylist) => genrePlaylist.viewsCount !== void 0);
758
- var genrePlaylistSize = (genrePlaylist, metric) => {
759
- if (metric === "songs") return genrePlaylist.tracksCount;
760
- if (genrePlaylist.viewsCount === void 0) {
761
- throw new Error(`Genre playlist ${genrePlaylist.uuid} has no viewsCount to size by views`);
762
- }
763
- return genrePlaylist.viewsCount;
764
- };
765
-
766
- // src/genre-tree/playlist-tree/TreeWheel.tsx
767
- var import_track_noun3 = require("@behindthemusictree/app-kit/track-noun");
768
768
  var import_jsx_runtime13 = require("react/jsx-runtime");
769
769
  function GenrePlaylistTreeWheel({
770
770
  scope,
@@ -775,14 +775,10 @@ function GenrePlaylistTreeWheel({
775
775
  additionalActions,
776
776
  onNodeClick,
777
777
  onNodeHover,
778
- renderInfoPanelBody,
779
778
  selectedNodeId,
780
779
  onSelectedNodeChange,
781
- hideInfoPanelClose,
782
- hideInfoPanelTitle,
783
780
  readOnly = false,
784
781
  allowWheelRotation,
785
- showToolbar,
786
782
  sizeMetric = "songs",
787
783
  floatingControlsLeading,
788
784
  busy,
@@ -790,12 +786,11 @@ function GenrePlaylistTreeWheel({
790
786
  viewportRestore,
791
787
  onViewportChange,
792
788
  playback,
793
- ...infoPanelCuration
789
+ ...hostPanel
794
790
  }) {
795
791
  const { isPlaying, setIsPlaying } = (0, import_player2.usePlayer)();
796
792
  const defaultPlayback = useGenrePlaylistPlayback({ scope, genrePlaylists });
797
793
  const { playingNodeId, play } = playback ?? defaultPlayback;
798
- const trackNoun = (0, import_track_noun3.useTrackNoun)();
799
794
  const { showPopup } = (0, import_popup3.usePopup)();
800
795
  const nodes = (0, import_react5.useMemo)(
801
796
  () => genrePlaylists.map((genrePlaylist) => ({
@@ -859,7 +854,6 @@ function GenrePlaylistTreeWheel({
859
854
  {
860
855
  className: className2,
861
856
  nodes,
862
- itemCountLabel: sizeMetric === "views" ? "Views" : `${(0, import_track_noun3.capitalize)(trackNoun.singular)} count`,
863
857
  playingNodeId,
864
858
  playState: isPlaying ? "playing" : "paused",
865
859
  onPlayPause: handlePlayPause,
@@ -869,20 +863,15 @@ function GenrePlaylistTreeWheel({
869
863
  additionalActions,
870
864
  onNodeClick,
871
865
  onNodeHover,
872
- renderInfoPanelBody,
873
866
  selectedNodeId,
874
867
  onSelectedNodeChange,
875
- hideInfoPanelClose,
876
- hideInfoPanelTitle,
877
868
  allowWheelRotation,
878
- showToolbar,
879
869
  floatingControlsLeading,
880
870
  busy,
881
871
  defaultViewport,
882
872
  viewportRestore,
883
873
  onViewportChange,
884
- ...infoPanelCuration,
885
- readOnly
874
+ ...hostPanel
886
875
  }
887
876
  );
888
877
  }
@@ -891,7 +880,6 @@ function GenrePlaylistTreeWheel({
891
880
  var import_react6 = require("react");
892
881
  var import_genre_tree_view3 = require("@behindthemusictree/genre-tree-view");
893
882
  var import_player3 = require("@behindthemusictree/app-kit/player");
894
- var import_track_noun4 = require("@behindthemusictree/app-kit/track-noun");
895
883
  var import_jsx_runtime14 = require("react/jsx-runtime");
896
884
  function GenrePlaylistTreeWheelRadialPopCore(props) {
897
885
  const {
@@ -903,14 +891,10 @@ function GenrePlaylistTreeWheelRadialPopCore(props) {
903
891
  additionalActions,
904
892
  onNodeClick,
905
893
  onNodeHover,
906
- renderInfoPanelBody,
907
894
  selectedNodeId,
908
895
  onSelectedNodeChange,
909
- hideInfoPanelClose,
910
- hideInfoPanelTitle,
911
896
  readOnly = false,
912
897
  allowWheelRotation,
913
- showToolbar,
914
898
  sizeMetric = "songs",
915
899
  floatingControlsLeading,
916
900
  busy,
@@ -921,33 +905,14 @@ function GenrePlaylistTreeWheelRadialPopCore(props) {
921
905
  playback
922
906
  } = props;
923
907
  const focus = props;
924
- const infoPanelCuration = {
925
- renderDangerZone: props.renderDangerZone,
926
- expanded: props.expanded,
927
- onExpandedChange: props.onExpandedChange,
928
- collapsed: props.collapsed,
929
- hideExpandButton: props.hideExpandButton,
930
- overlayPanel: props.overlayPanel
908
+ const hostPanel = {
909
+ obscuredInset: props.obscuredInset,
910
+ onNodeStyleResolverChange: props.onNodeStyleResolverChange
931
911
  };
932
912
  const { isPlaying, setIsPlaying } = (0, import_player3.usePlayer)();
933
913
  const defaultPlayback = useGenrePlaylistPlayback({ scope, genrePlaylists });
934
914
  const { playingNodeId, play } = playback ?? defaultPlayback;
935
- const trackNoun = (0, import_track_noun4.useTrackNoun)();
936
- const nodes = (0, import_react6.useMemo)(
937
- () => genrePlaylists.map((genrePlaylist) => ({
938
- id: genrePlaylist.uuid,
939
- parentId: genrePlaylist.parent?.uuid ?? null,
940
- name: genrePlaylist.name,
941
- itemCount: genrePlaylistSize(genrePlaylist, sizeMetric),
942
- actionable: Boolean(genrePlaylist.criteria),
943
- detached: !genrePlaylist.criteria,
944
- side: genrePlaylist.criteria?.side ?? void 0,
945
- wikidataId: genrePlaylist.criteria?.wikidataId ?? null,
946
- umbrella: genrePlaylist.isUmbrella,
947
- scene: genrePlaylist.scene ?? void 0
948
- })),
949
- [genrePlaylists, sizeMetric]
950
- );
915
+ const nodes = (0, import_react6.useMemo)(() => toTreeNodes(genrePlaylists, sizeMetric), [genrePlaylists, sizeMetric]);
951
916
  const handlePlayPause = (0, import_react6.useCallback)(
952
917
  (nodeId) => {
953
918
  const genrePlaylist = genrePlaylists.find((g) => g.uuid === nodeId);
@@ -988,7 +953,6 @@ function GenrePlaylistTreeWheelRadialPopCore(props) {
988
953
  const treeProps = {
989
954
  className: className2,
990
955
  nodes,
991
- itemCountLabel: sizeMetric === "views" ? "Views" : `${(0, import_track_noun4.capitalize)(trackNoun.singular)} count`,
992
956
  playingNodeId,
993
957
  playState: isPlaying ? "playing" : "paused",
994
958
  onPlayPause: handlePlayPause,
@@ -998,14 +962,9 @@ function GenrePlaylistTreeWheelRadialPopCore(props) {
998
962
  additionalActions,
999
963
  onNodeClick,
1000
964
  onNodeHover,
1001
- renderInfoPanelBody,
1002
965
  selectedNodeId,
1003
966
  onSelectedNodeChange,
1004
- hideInfoPanelClose,
1005
- hideInfoPanelTitle,
1006
- showToolbar,
1007
- ...infoPanelCuration,
1008
- readOnly
967
+ ...hostPanel
1009
968
  };
1010
969
  return outline ? /* @__PURE__ */ (0, import_jsx_runtime14.jsx)(import_genre_tree_view3.GenreTreeOutline, { ...treeProps }) : /* @__PURE__ */ (0, import_jsx_runtime14.jsx)(
1011
970
  import_genre_tree_view3.GenreTreeWheelRadialPopCore,
@@ -1125,7 +1084,7 @@ function GenreSearch({
1125
1084
  type: "button",
1126
1085
  "aria-label": "Previous",
1127
1086
  title: "Previous",
1128
- className: "genre-search-previous absolute left-2 top-1/2 z-10 hidden items-center justify-center p-1.5 [@media(pointer:coarse)]:min-h-[var(--layout-touch-target)] [@media(pointer:coarse)]:min-w-[var(--layout-touch-target)] text-[var(--color-text-muted)] rounded-full -translate-y-1/2 hover:bg-[var(--color-surface-muted)] hover:text-[var(--color-text)]",
1087
+ className: "genre-search-previous absolute left-2 top-1/2 z-10 flex items-center justify-center p-1.5 [@media(pointer:coarse)]:min-h-[var(--layout-touch-target)] [@media(pointer:coarse)]:min-w-[var(--layout-touch-target)] text-[var(--color-text-muted)] rounded-full -translate-y-1/2 hover:bg-[var(--color-surface-muted)] hover:text-[var(--color-text)]",
1129
1088
  onClick: onPrevious,
1130
1089
  children: /* @__PURE__ */ (0, import_jsx_runtime16.jsx)(import_lucide_react7.ArrowLeft, { "aria-hidden": "true", className: "w-4 h-4" })
1131
1090
  }
@@ -1147,7 +1106,7 @@ function GenreSearch({
1147
1106
  onPick: handlePick,
1148
1107
  status,
1149
1108
  loading: loading ? "Searching\u2026" : void 0,
1150
- inputClassName: "h-[var(--layout-search-h,3rem)] pl-6 pr-20 text-base transition-[padding] duration-200 ease-out motion-reduce:transition-none bg-[var(--color-surface)] border-[var(--color-border)] rounded-full shadow-md",
1109
+ inputClassName: `h-[var(--layout-search-h,3rem)] ${onPrevious ? "pl-11" : "pl-6"} pr-20 text-base transition-[padding] duration-200 ease-out motion-reduce:transition-none bg-[var(--color-surface)] border-[var(--color-border)] rounded-full shadow-md`,
1151
1110
  className: "genre-search-results z-40 rounded-xl shadow-md",
1152
1111
  listClassName: "max-h-[min(20rem,50dvh)]",
1153
1112
  optionClassName: "[@media(pointer:coarse)]:min-h-[var(--layout-touch-target)]"
@@ -1584,7 +1543,7 @@ function MusicBrainzRecordingLink({
1584
1543
 
1585
1544
  // src/genre-tree/GenrePlaylistTracks.tsx
1586
1545
  var import_genre_tree8 = require("@behindthemusictree/app-kit/genre-tree");
1587
- var import_track_noun5 = require("@behindthemusictree/app-kit/track-noun");
1546
+ var import_track_noun2 = require("@behindthemusictree/app-kit/track-noun");
1588
1547
  var import_jsx_runtime24 = require("react/jsx-runtime");
1589
1548
  var CATALOGUE_PAGE_SIZE = 100;
1590
1549
  var SongPageSchema = (0, import_transport5.PaginatedResponseSchema)(import_genre_tree6.SongDetailedSchema).extend({
@@ -1618,7 +1577,7 @@ function GenrePlaylistTracks({
1618
1577
  const { session, sessionRestored } = (0, import_auth2.useSession)();
1619
1578
  const { playerTrackObject, playState, handlePlayPauseAction } = (0, import_player4.usePlayer)();
1620
1579
  const { showPopup } = (0, import_popup5.usePopup)();
1621
- const trackNoun = (0, import_track_noun5.useTrackNoun)();
1580
+ const trackNoun = (0, import_track_noun2.useTrackNoun)();
1622
1581
  const { uuid } = genrePlaylist;
1623
1582
  const { fetch } = (0, import_transport4.useFetchWrapper)(getBackendBaseUrl);
1624
1583
  const { data, isPending, isError, refetch, hasNextPage, isFetchingNextPage, fetchNextPage } = (0, import_react_query.useInfiniteQuery)({
@@ -1639,9 +1598,8 @@ function GenrePlaylistTracks({
1639
1598
  getNextPageParam: (lastPage) => lastPage.nextPage ?? void 0,
1640
1599
  enabled: scope !== "me" || sessionRestored && !!session?.accessToken
1641
1600
  });
1642
- const sentinelRef = (0, import_react11.useRef)(null);
1601
+ const [sentinel, setSentinel] = (0, import_react11.useState)(null);
1643
1602
  (0, import_react11.useEffect)(() => {
1644
- const sentinel = sentinelRef.current;
1645
1603
  if (!hasNextPage || isFetchingNextPage || !sentinel) return;
1646
1604
  const observer = new IntersectionObserver((entries) => {
1647
1605
  if (entries.some((entry) => entry.isIntersecting)) {
@@ -1650,7 +1608,7 @@ function GenrePlaylistTracks({
1650
1608
  });
1651
1609
  observer.observe(sentinel);
1652
1610
  return () => observer.disconnect();
1653
- }, [hasNextPage, isFetchingNextPage, fetchNextPage]);
1611
+ }, [hasNextPage, isFetchingNextPage, fetchNextPage, sentinel]);
1654
1612
  const pages = data?.pages ?? [];
1655
1613
  const tracks = pages.flatMap((page) => page.tracks);
1656
1614
  const total = pages[0]?.total ?? void 0;
@@ -1661,11 +1619,7 @@ function GenrePlaylistTracks({
1661
1619
  showPopup(/* @__PURE__ */ (0, import_jsx_runtime24.jsx)(InternalErrorPopup, { errorCode: import_transport7.ErrorCode.CLIENT_UNKNOWN }));
1662
1620
  });
1663
1621
  };
1664
- return /* @__PURE__ */ (0, import_jsx_runtime24.jsxs)("div", { className: "gtv-info-panel-children", children: [
1665
- /* @__PURE__ */ (0, import_jsx_runtime24.jsxs)("span", { className: "gtv-info-panel-children-title", children: [
1666
- (0, import_track_noun5.capitalize)(trackNoun.plural),
1667
- total !== void 0 && ` (${total})`
1668
- ] }),
1622
+ return /* @__PURE__ */ (0, import_jsx_runtime24.jsx)(import_ui7.PanelSection, { title: `${(0, import_track_noun2.capitalize)(trackNoun.plural)}${total !== void 0 ? ` (${total})` : ""}`, children: /* @__PURE__ */ (0, import_jsx_runtime24.jsxs)("div", { children: [
1669
1623
  renderTracksToolbar?.(),
1670
1624
  isPending ? /* @__PURE__ */ (0, import_jsx_runtime24.jsx)(import_ui7.Skeleton, { className: "h-4 w-3/4", "aria-busy": "true" }) : isError && tracks.length === 0 ? /* @__PURE__ */ (0, import_jsx_runtime24.jsxs)("p", { role: "alert", children: [
1671
1625
  "Couldn't load ",
@@ -1728,13 +1682,13 @@ function GenrePlaylistTracks({
1728
1682
  track.uuid
1729
1683
  );
1730
1684
  }),
1731
- hasNextPage && /* @__PURE__ */ (0, import_jsx_runtime24.jsx)("li", { ref: sentinelRef, "aria-hidden": "true", className: "h-4" })
1685
+ hasNextPage && /* @__PURE__ */ (0, import_jsx_runtime24.jsx)("li", { ref: setSentinel, "aria-hidden": "true", className: "h-4" })
1732
1686
  ] })
1733
- ] });
1687
+ ] }) });
1734
1688
  }
1735
1689
 
1736
1690
  // src/genre-tree/GenreDetailExtras.tsx
1737
- var import_track_noun6 = require("@behindthemusictree/app-kit/track-noun");
1691
+ var import_track_noun3 = require("@behindthemusictree/app-kit/track-noun");
1738
1692
  var import_si2 = require("react-icons/si");
1739
1693
  var import_jsx_runtime25 = require("react/jsx-runtime");
1740
1694
  function GenreDetailExtras({
@@ -1752,7 +1706,7 @@ function GenreDetailExtras({
1752
1706
  family,
1753
1707
  ...trackSlots
1754
1708
  }) {
1755
- const trackNoun = (0, import_track_noun6.useTrackNoun)();
1709
+ const trackNoun = (0, import_track_noun3.useTrackNoun)();
1756
1710
  const { data: overview, isPending } = (0, import_genre_tree9.useFetchGenreOverview)(
1757
1711
  genreUuid,
1758
1712
  scope,
@@ -1761,10 +1715,7 @@ function GenreDetailExtras({
1761
1715
  );
1762
1716
  if (isPending) {
1763
1717
  return /* @__PURE__ */ (0, import_jsx_runtime25.jsxs)(import_jsx_runtime25.Fragment, { children: [
1764
- /* @__PURE__ */ (0, import_jsx_runtime25.jsxs)("div", { className: "gtv-info-panel-children", "aria-busy": "true", "data-testid": "genre-detail-extras-skeleton", children: [
1765
- /* @__PURE__ */ (0, import_jsx_runtime25.jsx)("span", { className: "gtv-info-panel-children-title", children: "Summary" }),
1766
- /* @__PURE__ */ (0, import_jsx_runtime25.jsx)(import_ui8.Skeleton, { className: "h-4 w-3/4" })
1767
- ] }),
1718
+ /* @__PURE__ */ (0, import_jsx_runtime25.jsx)(import_ui8.PanelSection, { title: "Summary", children: /* @__PURE__ */ (0, import_jsx_runtime25.jsx)("div", { "aria-busy": "true", "data-testid": "genre-detail-extras-skeleton", children: /* @__PURE__ */ (0, import_jsx_runtime25.jsx)(import_ui8.Skeleton, { className: "h-4 w-3/4" }) }) }),
1768
1719
  family
1769
1720
  ] });
1770
1721
  }
@@ -1772,50 +1723,38 @@ function GenreDetailExtras({
1772
1723
  const { summary, summarySourceUrl, essentialSongs } = overview;
1773
1724
  const wikidataUrl = wikidataId ? `https://www.wikidata.org/wiki/${encodeURIComponent(wikidataId)}` : null;
1774
1725
  return /* @__PURE__ */ (0, import_jsx_runtime25.jsxs)(import_jsx_runtime25.Fragment, { children: [
1775
- (summarySourceUrl || wikidataUrl) && /* @__PURE__ */ (0, import_jsx_runtime25.jsxs)("div", { className: "gtv-info-panel-children text-[0.8125rem] leading-[1.45] text-[var(--color-text)]", children: [
1776
- /* @__PURE__ */ (0, import_jsx_runtime25.jsx)("span", { className: "gtv-info-panel-children-title", children: "Links" }),
1777
- /* @__PURE__ */ (0, import_jsx_runtime25.jsxs)("div", { className: "mt-1 flex flex-wrap gap-x-3 gap-y-1 text-xs", children: [
1778
- summarySourceUrl && /* @__PURE__ */ (0, import_jsx_runtime25.jsxs)(
1779
- "a",
1780
- {
1781
- href: summarySourceUrl,
1782
- target: "_blank",
1783
- rel: "noopener noreferrer",
1784
- className: "inline-flex items-center gap-1.5 text-[var(--color-text-muted)] hover:text-[var(--color-text)]",
1785
- children: [
1786
- /* @__PURE__ */ (0, import_jsx_runtime25.jsx)(import_si2.SiWikipedia, { "aria-hidden": "true", className: "h-3.5 w-3.5" }),
1787
- "Wikipedia"
1788
- ]
1789
- }
1790
- ),
1791
- wikidataUrl && /* @__PURE__ */ (0, import_jsx_runtime25.jsxs)(
1792
- "a",
1793
- {
1794
- href: wikidataUrl,
1795
- target: "_blank",
1796
- rel: "noopener noreferrer",
1797
- className: "inline-flex items-center gap-1.5 text-[var(--color-text-muted)] hover:text-[var(--color-text)]",
1798
- children: [
1799
- /* @__PURE__ */ (0, import_jsx_runtime25.jsx)(import_si2.SiWikidata, { "aria-hidden": "true", className: "h-3.5 w-3.5" }),
1800
- "Wikidata"
1801
- ]
1802
- }
1803
- )
1804
- ] })
1805
- ] }),
1806
- renderSummary ? renderSummary(overview) : /* @__PURE__ */ (0, import_jsx_runtime25.jsxs)("div", { className: "gtv-info-panel-children text-[0.8125rem] leading-[1.45] text-[var(--color-text)]", children: [
1807
- /* @__PURE__ */ (0, import_jsx_runtime25.jsx)("span", { className: "gtv-info-panel-children-title", children: "Summary" }),
1808
- /* @__PURE__ */ (0, import_jsx_runtime25.jsx)(ClampedText, { className: "mt-1", children: summary ?? "\u2014" }, genreUuid)
1809
- ] }),
1726
+ (summarySourceUrl || wikidataUrl) && /* @__PURE__ */ (0, import_jsx_runtime25.jsx)(import_ui8.PanelSection, { title: "Links", children: /* @__PURE__ */ (0, import_jsx_runtime25.jsxs)("div", { className: "flex flex-wrap gap-x-3 gap-y-1 text-xs text-[var(--color-text)]", children: [
1727
+ summarySourceUrl && /* @__PURE__ */ (0, import_jsx_runtime25.jsxs)(
1728
+ "a",
1729
+ {
1730
+ href: summarySourceUrl,
1731
+ target: "_blank",
1732
+ rel: "noopener noreferrer",
1733
+ className: "inline-flex items-center gap-1.5 text-[var(--color-text-muted)] hover:text-[var(--color-text)]",
1734
+ children: [
1735
+ /* @__PURE__ */ (0, import_jsx_runtime25.jsx)(import_si2.SiWikipedia, { "aria-hidden": "true", className: "h-3.5 w-3.5" }),
1736
+ "Wikipedia"
1737
+ ]
1738
+ }
1739
+ ),
1740
+ wikidataUrl && /* @__PURE__ */ (0, import_jsx_runtime25.jsxs)(
1741
+ "a",
1742
+ {
1743
+ href: wikidataUrl,
1744
+ target: "_blank",
1745
+ rel: "noopener noreferrer",
1746
+ className: "inline-flex items-center gap-1.5 text-[var(--color-text-muted)] hover:text-[var(--color-text)]",
1747
+ children: [
1748
+ /* @__PURE__ */ (0, import_jsx_runtime25.jsx)(import_si2.SiWikidata, { "aria-hidden": "true", className: "h-3.5 w-3.5" }),
1749
+ "Wikidata"
1750
+ ]
1751
+ }
1752
+ )
1753
+ ] }) }),
1754
+ /* @__PURE__ */ (0, import_jsx_runtime25.jsx)(import_ui8.PanelSection, { title: "Summary", children: renderSummary ? renderSummary(overview) : /* @__PURE__ */ (0, import_jsx_runtime25.jsx)(ClampedText, { className: "text-[0.8125rem] leading-[1.45] text-[var(--color-text)]", children: summary ?? "\u2014" }, genreUuid) }),
1810
1755
  family,
1811
1756
  renderGenreDetailExtras?.(overview),
1812
- renderEssentialTracks ? renderEssentialTracks(overview) : essentialSongs && /* @__PURE__ */ (0, import_jsx_runtime25.jsxs)("div", { className: "gtv-info-panel-children text-[0.8125rem] leading-[1.45] text-[var(--color-text)]", children: [
1813
- /* @__PURE__ */ (0, import_jsx_runtime25.jsxs)("span", { className: "gtv-info-panel-children-title", children: [
1814
- "Essential ",
1815
- trackNoun.plural
1816
- ] }),
1817
- essentialSongs.length > 0 ? /* @__PURE__ */ (0, import_jsx_runtime25.jsx)("ul", { className: "mt-1 divide-y divide-[var(--color-border)]", children: essentialSongs.map((track) => /* @__PURE__ */ (0, import_jsx_runtime25.jsx)("li", { className: "py-1", children: track.title }, track.uuid)) }) : /* @__PURE__ */ (0, import_jsx_runtime25.jsx)("p", { className: "mt-1", children: "\u2014" })
1818
- ] }),
1757
+ (renderEssentialTracks || essentialSongs) && /* @__PURE__ */ (0, import_jsx_runtime25.jsx)(import_ui8.PanelSection, { title: `Essential ${trackNoun.plural}`, children: renderEssentialTracks ? renderEssentialTracks(overview) : essentialSongs && essentialSongs.length > 0 ? /* @__PURE__ */ (0, import_jsx_runtime25.jsx)("ul", { className: "text-[0.8125rem] leading-[1.45] text-[var(--color-text)]", children: essentialSongs.map((track) => /* @__PURE__ */ (0, import_jsx_runtime25.jsx)("li", { className: "py-1", children: track.title }, track.uuid)) }) : /* @__PURE__ */ (0, import_jsx_runtime25.jsx)("p", { className: "text-[0.8125rem] leading-[1.45] text-[var(--color-text)]", children: "\u2014" }) }),
1819
1758
  /* @__PURE__ */ (0, import_jsx_runtime25.jsx)(
1820
1759
  GenrePlaylistTracks,
1821
1760
  {
@@ -1886,66 +1825,10 @@ function GenreInfoPanelBody({
1886
1825
  ] });
1887
1826
  }
1888
1827
 
1889
- // src/genre-tree/PanelEdgeTab.tsx
1890
- var import_lucide_react12 = require("lucide-react");
1891
- var import_tailwind_merge = require("tailwind-merge");
1892
- var import_jsx_runtime27 = require("react/jsx-runtime");
1893
- var buttonClassName = "flex h-12 w-full items-center justify-center p-0 text-[var(--color-text-muted)] hover:bg-[var(--color-surface-muted)] hover:text-[var(--color-text)] focus-visible:outline focus-visible:outline-2 focus-visible:-outline-offset-2 focus-visible:outline-[var(--color-primary)]";
1894
- function PanelEdgeTab({ side, edge, center, controls, className: className2 }) {
1895
- if (!edge && !center) return null;
1896
- const isLeft = side === "left";
1897
- const EdgeIcon = isLeft ? import_lucide_react12.ChevronLeft : import_lucide_react12.ChevronRight;
1898
- const CenterIcon = isLeft ? import_lucide_react12.ChevronRight : import_lucide_react12.ChevronLeft;
1899
- const renderButton = (action, icon) => /* @__PURE__ */ (0, import_jsx_runtime27.jsx)(
1900
- "button",
1901
- {
1902
- type: "button",
1903
- "aria-label": action.label,
1904
- title: action.label,
1905
- "aria-controls": controls,
1906
- "aria-expanded": action.expanded,
1907
- className: buttonClassName,
1908
- onClick: action.onClick,
1909
- children: icon
1910
- }
1911
- );
1912
- const edgeButton = edge && renderButton(edge, /* @__PURE__ */ (0, import_jsx_runtime27.jsx)(EdgeIcon, { "aria-hidden": "true", className: "h-5 w-5" }));
1913
- const centerButton = center && renderButton(center, /* @__PURE__ */ (0, import_jsx_runtime27.jsx)(CenterIcon, { "aria-hidden": "true", className: "h-5 w-5" }));
1914
- const buttons = isLeft ? [edgeButton, centerButton] : [centerButton, edgeButton];
1915
- return /* @__PURE__ */ (0, import_jsx_runtime27.jsxs)(
1916
- "div",
1917
- {
1918
- className: (0, import_tailwind_merge.twMerge)(
1919
- "panel-edge-tab flex w-[var(--layout-edge-tab-w)] flex-col overflow-hidden bg-[var(--color-surface)] shadow-[0_1px_2px_rgba(60,64,67,0.3),0_2px_6px_2px_rgba(60,64,67,0.15)] [clip-path:inset(-0.5rem_-0.5rem_-0.5rem_0)]",
1920
- isLeft ? "rounded-r-lg" : "rounded-l-lg [clip-path:inset(-0.5rem_0_-0.5rem_-0.5rem)]",
1921
- className2
1922
- ),
1923
- children: [
1924
- buttons[0],
1925
- buttons[0] && buttons[1] && /* @__PURE__ */ (0, import_jsx_runtime27.jsx)("div", { role: "presentation", className: "h-px shrink-0 bg-[var(--color-border)]" }),
1926
- buttons[1]
1927
- ]
1928
- }
1929
- );
1930
- }
1931
-
1932
- // src/genre-tree/useLeftDetailPanelLayout.ts
1933
- var import_react13 = require("react");
1934
- function useLeftDetailPanelLayout(isPanelShown, enabled = true) {
1935
- (0, import_react13.useEffect)(() => {
1936
- if (!enabled || isPanelShown) return void 0;
1937
- const root = document.documentElement;
1938
- root.style.setProperty("--layout-left-w", "0px");
1939
- return () => {
1940
- root.style.removeProperty("--layout-left-w");
1941
- };
1942
- }, [enabled, isPanelShown]);
1943
- }
1944
-
1945
1828
  // src/genre-tree/ChangeParentPopup.tsx
1946
- var import_react14 = require("react");
1829
+ var import_react13 = require("react");
1947
1830
  var import_genre_tree10 = require("@behindthemusictree/app-kit/genre-tree");
1948
- var import_jsx_runtime28 = require("react/jsx-runtime");
1831
+ var import_jsx_runtime27 = require("react/jsx-runtime");
1949
1832
  function ChangeParentPopup({
1950
1833
  genrePlaylist,
1951
1834
  genrePlaylists,
@@ -1955,7 +1838,7 @@ function ChangeParentPopup({
1955
1838
  onParentChanged
1956
1839
  }) {
1957
1840
  const { mutate, isPending, formErrors } = (0, import_genre_tree10.useUpdateGenre)(scope, getBackendBaseUrl);
1958
- const candidates = (0, import_react14.useMemo)(
1841
+ const candidates = (0, import_react13.useMemo)(
1959
1842
  () => (0, import_genre_tree10.getReparentCandidates)(genrePlaylists, genrePlaylist.uuid),
1960
1843
  [genrePlaylists, genrePlaylist.uuid]
1961
1844
  );
@@ -1973,19 +1856,19 @@ function ChangeParentPopup({
1973
1856
  }
1974
1857
  );
1975
1858
  };
1976
- (0, import_react14.useEffect)(() => {
1859
+ (0, import_react13.useEffect)(() => {
1977
1860
  const handleKeyDown = (event) => {
1978
1861
  if (event.key === "Escape") onClose();
1979
1862
  };
1980
1863
  window.addEventListener("keydown", handleKeyDown);
1981
1864
  return () => window.removeEventListener("keydown", handleKeyDown);
1982
1865
  }, [onClose]);
1983
- return /* @__PURE__ */ (0, import_jsx_runtime28.jsxs)(BasePopup_default, { title: `Change parent of ${genrePlaylist.name}`, onClose, showCancelButton: true, fitContent: true, children: [
1984
- /* @__PURE__ */ (0, import_jsx_runtime28.jsxs)("p", { className: "mb-3 text-sm text-[var(--color-text-muted)]", children: [
1866
+ return /* @__PURE__ */ (0, import_jsx_runtime27.jsxs)(BasePopup_default, { title: `Change parent of ${genrePlaylist.name}`, onClose, showCancelButton: true, fitContent: true, children: [
1867
+ /* @__PURE__ */ (0, import_jsx_runtime27.jsxs)("p", { className: "mb-3 text-sm text-[var(--color-text-muted)]", children: [
1985
1868
  "Current parent: ",
1986
1869
  genrePlaylist.parent?.name ?? "none"
1987
1870
  ] }),
1988
- /* @__PURE__ */ (0, import_jsx_runtime28.jsx)("div", { "aria-busy": isPending, className: isPending ? "pointer-events-none opacity-60" : void 0, children: /* @__PURE__ */ (0, import_jsx_runtime28.jsx)(
1871
+ /* @__PURE__ */ (0, import_jsx_runtime27.jsx)("div", { "aria-busy": isPending, className: isPending ? "pointer-events-none opacity-60" : void 0, children: /* @__PURE__ */ (0, import_jsx_runtime27.jsx)(
1989
1872
  GenreSearch,
1990
1873
  {
1991
1874
  genrePlaylists: candidates,
@@ -1993,7 +1876,7 @@ function ChangeParentPopup({
1993
1876
  placeholder: "Search a new parent\u2026"
1994
1877
  }
1995
1878
  ) }),
1996
- formErrors.length > 0 && /* @__PURE__ */ (0, import_jsx_runtime28.jsx)("p", { role: "alert", className: "mt-3 text-sm text-[var(--color-red-500)]", children: formErrors.map(({ message }) => message).join(" ") })
1879
+ formErrors.length > 0 && /* @__PURE__ */ (0, import_jsx_runtime27.jsx)("p", { role: "alert", className: "mt-3 text-sm text-[var(--color-red-500)]", children: formErrors.map(({ message }) => message).join(" ") })
1997
1880
  ] });
1998
1881
  }
1999
1882
 
@@ -2005,12 +1888,12 @@ var import_genre_tree15 = require("@behindthemusictree/app-kit/genre-tree");
2005
1888
  var import_ui10 = require("@behindthemusictree/ui");
2006
1889
 
2007
1890
  // src/genre-tree/artist/ArtistLinks.tsx
2008
- var import_lucide_react13 = require("lucide-react");
1891
+ var import_lucide_react12 = require("lucide-react");
2009
1892
  var import_si3 = require("react-icons/si");
2010
1893
  var import_ui9 = require("@behindthemusictree/ui");
2011
- var import_jsx_runtime29 = require("react/jsx-runtime");
1894
+ var import_jsx_runtime28 = require("react/jsx-runtime");
2012
1895
  var LINK_KINDS = {
2013
- official: { label: "Official website", Icon: import_lucide_react13.Globe },
1896
+ official: { label: "Official website", Icon: import_lucide_react12.Globe },
2014
1897
  wikipedia: { label: "Wikipedia", Icon: import_si3.SiWikipedia },
2015
1898
  wikidata: { label: "Wikidata", Icon: import_si3.SiWikidata },
2016
1899
  spotify: { label: "Spotify", Icon: import_si3.SiSpotify },
@@ -2018,7 +1901,7 @@ var LINK_KINDS = {
2018
1901
  bandcamp: { label: "Bandcamp", Icon: import_si3.SiBandcamp },
2019
1902
  soundcloud: { label: "SoundCloud", Icon: import_si3.SiSoundcloud },
2020
1903
  discogs: { label: "Discogs", Icon: import_si3.SiDiscogs },
2021
- social: { label: "Social", Icon: import_lucide_react13.Share2 }
1904
+ social: { label: "Social", Icon: import_lucide_react12.Share2 }
2022
1905
  };
2023
1906
  function socialLabel(url) {
2024
1907
  try {
@@ -2045,7 +1928,7 @@ function artistLinks(artist) {
2045
1928
  return links;
2046
1929
  }
2047
1930
  function ArtistLinks({ links }) {
2048
- return /* @__PURE__ */ (0, import_jsx_runtime29.jsx)("ul", { className: "flex flex-wrap gap-1.5", children: links.map(({ key, url, Icon, label }) => /* @__PURE__ */ (0, import_jsx_runtime29.jsx)("li", { children: /* @__PURE__ */ (0, import_jsx_runtime29.jsx)(import_ui9.Chip, { href: url, icon: /* @__PURE__ */ (0, import_jsx_runtime29.jsx)(Icon, { className: "h-3.5 w-3.5 shrink-0", "aria-hidden": "true" }), children: label }) }, key)) });
1931
+ return /* @__PURE__ */ (0, import_jsx_runtime28.jsx)("ul", { className: "flex flex-wrap gap-1.5", children: links.map(({ key, url, Icon, label }) => /* @__PURE__ */ (0, import_jsx_runtime28.jsx)("li", { children: /* @__PURE__ */ (0, import_jsx_runtime28.jsx)(import_ui9.Chip, { href: url, icon: /* @__PURE__ */ (0, import_jsx_runtime28.jsx)(Icon, { className: "h-3.5 w-3.5 shrink-0", "aria-hidden": "true" }), children: label }) }, key)) });
2049
1932
  }
2050
1933
 
2051
1934
  // src/genre-tree/artist/formatYears.ts
@@ -2056,7 +1939,7 @@ function formatYears(beginYear, endYear, ended) {
2056
1939
  }
2057
1940
 
2058
1941
  // src/genre-tree/artist/ArtistHeader.tsx
2059
- var import_jsx_runtime30 = require("react/jsx-runtime");
1942
+ var import_jsx_runtime29 = require("react/jsx-runtime");
2060
1943
  var countryNames = new Intl.DisplayNames(["en"], { type: "region" });
2061
1944
  function ArtistHeader({ artist }) {
2062
1945
  const fields = [];
@@ -2066,35 +1949,35 @@ function ArtistHeader({ artist }) {
2066
1949
  const years = formatYears(artist.beginYear, artist.endYear, artist.ended);
2067
1950
  if (years) fields.push({ label: "Active", value: years });
2068
1951
  const links = artistLinks(artist);
2069
- if (links.length > 0) fields.push({ label: "Links", value: /* @__PURE__ */ (0, import_jsx_runtime30.jsx)(ArtistLinks, { links }) });
1952
+ if (links.length > 0) fields.push({ label: "Links", value: /* @__PURE__ */ (0, import_jsx_runtime29.jsx)(ArtistLinks, { links }) });
2070
1953
  if (fields.length === 0) return null;
2071
- return /* @__PURE__ */ (0, import_jsx_runtime30.jsx)(import_ui10.FieldList, { items: fields });
1954
+ return /* @__PURE__ */ (0, import_jsx_runtime29.jsx)(import_ui10.FieldList, { items: fields });
2072
1955
  }
2073
1956
 
2074
1957
  // src/genre-tree/artist/ArtistPhoto.tsx
2075
- var import_jsx_runtime31 = require("react/jsx-runtime");
1958
+ var import_jsx_runtime30 = require("react/jsx-runtime");
2076
1959
  function ExternalLink({ href, children }) {
2077
- return /* @__PURE__ */ (0, import_jsx_runtime31.jsx)("a", { href, target: "_blank", rel: "noopener noreferrer", className: "underline-offset-2 hover:underline", children });
1960
+ return /* @__PURE__ */ (0, import_jsx_runtime30.jsx)("a", { href, target: "_blank", rel: "noopener noreferrer", className: "underline-offset-2 hover:underline", children });
2078
1961
  }
2079
1962
  function ArtistPhoto({ image, name }) {
2080
- return /* @__PURE__ */ (0, import_jsx_runtime31.jsxs)("figure", { className: "flex flex-col gap-1.5 px-6 py-2.5", children: [
2081
- /* @__PURE__ */ (0, import_jsx_runtime31.jsx)("img", { src: image.url, alt: name, className: "h-64 w-full object-contain bg-[var(--color-surface-muted)] rounded" }),
2082
- /* @__PURE__ */ (0, import_jsx_runtime31.jsxs)("figcaption", { className: "text-xs text-[var(--color-text-muted)]", children: [
1963
+ return /* @__PURE__ */ (0, import_jsx_runtime30.jsxs)("figure", { className: "flex flex-col gap-1.5 px-6 py-2.5", children: [
1964
+ /* @__PURE__ */ (0, import_jsx_runtime30.jsx)("img", { src: image.url, alt: name, className: "h-64 w-full object-contain bg-[var(--color-surface-muted)] rounded" }),
1965
+ /* @__PURE__ */ (0, import_jsx_runtime30.jsxs)("figcaption", { className: "text-xs text-[var(--color-text-muted)]", children: [
2083
1966
  "Photo: ",
2084
- /* @__PURE__ */ (0, import_jsx_runtime31.jsx)(ExternalLink, { href: image.pageUrl, children: image.author ?? "Wikimedia Commons" }),
1967
+ /* @__PURE__ */ (0, import_jsx_runtime30.jsx)(ExternalLink, { href: image.pageUrl, children: image.author ?? "Wikimedia Commons" }),
2085
1968
  ",",
2086
1969
  " ",
2087
- image.licenseUrl ? /* @__PURE__ */ (0, import_jsx_runtime31.jsx)(ExternalLink, { href: image.licenseUrl, children: image.license }) : image.license
1970
+ image.licenseUrl ? /* @__PURE__ */ (0, import_jsx_runtime30.jsx)(ExternalLink, { href: image.licenseUrl, children: image.license }) : image.license
2088
1971
  ] })
2089
1972
  ] });
2090
1973
  }
2091
1974
 
2092
1975
  // src/genre-tree/artist/ArtistMembers.tsx
2093
1976
  var import_ui11 = require("@behindthemusictree/ui");
2094
- var import_jsx_runtime32 = require("react/jsx-runtime");
1977
+ var import_jsx_runtime31 = require("react/jsx-runtime");
2095
1978
  function MembershipList({ title, memberships, renderArtistLink }) {
2096
1979
  if (memberships.length === 0) return null;
2097
- return /* @__PURE__ */ (0, import_jsx_runtime32.jsx)(import_ui11.PanelGroup, { title, children: /* @__PURE__ */ (0, import_jsx_runtime32.jsx)("ul", { children: memberships.map((membership, index) => {
1980
+ return /* @__PURE__ */ (0, import_jsx_runtime31.jsx)(import_ui11.PanelGroup, { title, children: /* @__PURE__ */ (0, import_jsx_runtime31.jsx)("ul", { children: memberships.map((membership, index) => {
2098
1981
  const details = [
2099
1982
  formatYears(membership.beginYear, membership.endYear, membership.ended),
2100
1983
  ...membership.roles
@@ -2102,9 +1985,9 @@ function MembershipList({ title, memberships, renderArtistLink }) {
2102
1985
  const detailsText = details.filter(Boolean).join(" \xB7 ");
2103
1986
  return (
2104
1987
  // A member can have several periods in the same group.
2105
- /* @__PURE__ */ (0, import_jsx_runtime32.jsxs)("li", { className: "truncate py-0.5", title: detailsText || void 0, children: [
2106
- /* @__PURE__ */ (0, import_jsx_runtime32.jsx)("span", { className: "font-medium text-[var(--color-text)]", children: renderArtistLink(membership.artist) }),
2107
- detailsText && /* @__PURE__ */ (0, import_jsx_runtime32.jsxs)("span", { className: "text-[var(--color-text-muted)]", children: [
1988
+ /* @__PURE__ */ (0, import_jsx_runtime31.jsxs)("li", { className: "truncate py-0.5", title: detailsText || void 0, children: [
1989
+ /* @__PURE__ */ (0, import_jsx_runtime31.jsx)("span", { className: "font-medium text-[var(--color-text)]", children: renderArtistLink(membership.artist) }),
1990
+ detailsText && /* @__PURE__ */ (0, import_jsx_runtime31.jsxs)("span", { className: "text-[var(--color-text-muted)]", children: [
2108
1991
  " \u2014 ",
2109
1992
  detailsText
2110
1993
  ] })
@@ -2115,9 +1998,9 @@ function MembershipList({ title, memberships, renderArtistLink }) {
2115
1998
  function ArtistMembers({ title, memberships, renderArtistLink }) {
2116
1999
  if (memberships.length === 0) return null;
2117
2000
  const isCurrent = (membership) => !membership.ended && membership.endYear === null;
2118
- return /* @__PURE__ */ (0, import_jsx_runtime32.jsxs)(import_ui11.PanelSection, { title, children: [
2119
- /* @__PURE__ */ (0, import_jsx_runtime32.jsx)(MembershipList, { title: "Current", memberships: memberships.filter(isCurrent), renderArtistLink }),
2120
- /* @__PURE__ */ (0, import_jsx_runtime32.jsx)(
2001
+ return /* @__PURE__ */ (0, import_jsx_runtime31.jsxs)(import_ui11.PanelSection, { title, children: [
2002
+ /* @__PURE__ */ (0, import_jsx_runtime31.jsx)(MembershipList, { title: "Current", memberships: memberships.filter(isCurrent), renderArtistLink }),
2003
+ /* @__PURE__ */ (0, import_jsx_runtime31.jsx)(
2121
2004
  MembershipList,
2122
2005
  {
2123
2006
  title: "Past",
@@ -2130,7 +2013,7 @@ function ArtistMembers({ title, memberships, renderArtistLink }) {
2130
2013
 
2131
2014
  // src/genre-tree/artist/ArtistDiscography.tsx
2132
2015
  var import_ui12 = require("@behindthemusictree/ui");
2133
- var import_jsx_runtime33 = require("react/jsx-runtime");
2016
+ var import_jsx_runtime32 = require("react/jsx-runtime");
2134
2017
  var GROUPS = ["Albums", "EPs", "Singles", "Live", "Compilations", "Other"];
2135
2018
  var PRIMARY_GROUPS = { album: "Albums", ep: "EPs", single: "Singles" };
2136
2019
  function discographyGroup({ primaryType, secondaryTypes }) {
@@ -2150,20 +2033,20 @@ function byYear(a, b) {
2150
2033
  function ArtistDiscography({ albums }) {
2151
2034
  if (albums.length === 0) return null;
2152
2035
  const sorted = [...albums].sort(byYear);
2153
- return /* @__PURE__ */ (0, import_jsx_runtime33.jsx)(import_ui12.PanelSection, { title: "Discography", children: GROUPS.map((group) => {
2036
+ return /* @__PURE__ */ (0, import_jsx_runtime32.jsx)(import_ui12.PanelSection, { title: "Discography", children: GROUPS.map((group) => {
2154
2037
  const groupAlbums = sorted.filter((album) => discographyGroup(album) === group);
2155
2038
  if (groupAlbums.length === 0) return null;
2156
- return /* @__PURE__ */ (0, import_jsx_runtime33.jsx)(import_ui12.PanelGroup, { title: group, children: /* @__PURE__ */ (0, import_jsx_runtime33.jsx)("ul", { children: groupAlbums.map((album) => /* @__PURE__ */ (0, import_jsx_runtime33.jsxs)("li", { className: "grid grid-cols-[2.5rem_minmax(0,1fr)] gap-2 py-0.5", children: [
2157
- /* @__PURE__ */ (0, import_jsx_runtime33.jsx)("span", { className: "tabular-nums text-[var(--color-text-muted)]", children: album.year ?? "\u2014" }),
2158
- /* @__PURE__ */ (0, import_jsx_runtime33.jsx)("span", { className: "truncate text-[var(--color-text)]", title: album.name, children: album.name })
2039
+ return /* @__PURE__ */ (0, import_jsx_runtime32.jsx)(import_ui12.PanelGroup, { title: group, children: /* @__PURE__ */ (0, import_jsx_runtime32.jsx)("ul", { children: groupAlbums.map((album) => /* @__PURE__ */ (0, import_jsx_runtime32.jsxs)("li", { className: "grid grid-cols-[2.5rem_minmax(0,1fr)] gap-2 py-0.5", children: [
2040
+ /* @__PURE__ */ (0, import_jsx_runtime32.jsx)("span", { className: "tabular-nums text-[var(--color-text-muted)]", children: album.year ?? "\u2014" }),
2041
+ /* @__PURE__ */ (0, import_jsx_runtime32.jsx)("span", { className: "truncate text-[var(--color-text)]", title: album.name, children: album.name })
2159
2042
  ] }, album.uuid)) }) }, group);
2160
2043
  }) });
2161
2044
  }
2162
2045
 
2163
2046
  // src/genre-tree/artist/ArtistSongs.tsx
2164
- var import_react15 = require("react");
2047
+ var import_react14 = require("react");
2165
2048
  var import_react_query2 = require("@tanstack/react-query");
2166
- var import_lucide_react14 = require("lucide-react");
2049
+ var import_lucide_react13 = require("lucide-react");
2167
2050
  var import_ui13 = require("@behindthemusictree/ui");
2168
2051
  var import_player10 = require("@behindthemusictree/app-kit/player");
2169
2052
  var import_genre_tree14 = require("@behindthemusictree/app-kit/genre-tree");
@@ -2177,7 +2060,7 @@ var import_genre_tree11 = require("@behindthemusictree/app-kit/genre-tree");
2177
2060
  var import_fa5 = require("react-icons/fa");
2178
2061
  var import_player7 = require("@behindthemusictree/app-kit/player");
2179
2062
  var import_player8 = require("@behindthemusictree/app-kit/player");
2180
- var import_jsx_runtime34 = require("react/jsx-runtime");
2063
+ var import_jsx_runtime33 = require("react/jsx-runtime");
2181
2064
  function TrackPositionPlayPause({
2182
2065
  position,
2183
2066
  uuid,
@@ -2191,14 +2074,14 @@ function TrackPositionPlayPause({
2191
2074
  const isPlaying = isThisTrack && isConfirmedPlayable && playState === import_player8.PlayStates.PLAYING;
2192
2075
  const isPaused = isThisTrack && isConfirmedPlayable && playState === import_player8.PlayStates.PAUSED;
2193
2076
  const isCurrentTrackActive = isPlaying || isPaused;
2194
- return /* @__PURE__ */ (0, import_jsx_runtime34.jsxs)(
2077
+ return /* @__PURE__ */ (0, import_jsx_runtime33.jsxs)(
2195
2078
  "div",
2196
2079
  {
2197
2080
  className: "track-position-play-pause relative flex items-center justify-center gap-2 text-lg w-16 cursor-pointer",
2198
2081
  style: { minWidth: "4rem", maxWidth: "4rem" },
2199
2082
  onClick: handlePlayPauseClick,
2200
2083
  children: [
2201
- /* @__PURE__ */ (0, import_jsx_runtime34.jsx)("div", { className: "track-position-play-pause__status group-hover:hidden flex items-center justify-center", children: isAwaitingPlayback ? /* @__PURE__ */ (0, import_jsx_runtime34.jsx)("span", { className: "track-position-play-pause__spinner", role: "status", "aria-label": "Loading track" }) : isCurrentTrackActive ? /* @__PURE__ */ (0, import_jsx_runtime34.jsxs)(
2084
+ /* @__PURE__ */ (0, import_jsx_runtime33.jsx)("div", { className: "track-position-play-pause__status group-hover:hidden flex items-center justify-center", children: isAwaitingPlayback ? /* @__PURE__ */ (0, import_jsx_runtime33.jsx)("span", { className: "track-position-play-pause__spinner", role: "status", "aria-label": "Loading track" }) : isCurrentTrackActive ? /* @__PURE__ */ (0, import_jsx_runtime33.jsxs)(
2202
2085
  "span",
2203
2086
  {
2204
2087
  className: "track-position-play-pause__bars",
@@ -2206,18 +2089,18 @@ function TrackPositionPlayPause({
2206
2089
  role: "img",
2207
2090
  "aria-label": isPlaying ? "Playing" : "Paused",
2208
2091
  children: [
2209
- /* @__PURE__ */ (0, import_jsx_runtime34.jsx)("span", { className: "track-position-play-pause__bar" }),
2210
- /* @__PURE__ */ (0, import_jsx_runtime34.jsx)("span", { className: "track-position-play-pause__bar" }),
2211
- /* @__PURE__ */ (0, import_jsx_runtime34.jsx)("span", { className: "track-position-play-pause__bar" })
2092
+ /* @__PURE__ */ (0, import_jsx_runtime33.jsx)("span", { className: "track-position-play-pause__bar" }),
2093
+ /* @__PURE__ */ (0, import_jsx_runtime33.jsx)("span", { className: "track-position-play-pause__bar" }),
2094
+ /* @__PURE__ */ (0, import_jsx_runtime33.jsx)("span", { className: "track-position-play-pause__bar" })
2212
2095
  ]
2213
2096
  }
2214
2097
  ) : position }),
2215
- /* @__PURE__ */ (0, import_jsx_runtime34.jsx)(
2098
+ /* @__PURE__ */ (0, import_jsx_runtime33.jsx)(
2216
2099
  "div",
2217
2100
  {
2218
2101
  className: `hidden group-hover:flex items-center justify-center ${isCurrentTrackActive ? "" : "absolute inset-0"}`,
2219
2102
  "aria-hidden": "true",
2220
- children: isAwaitingPlayback ? /* @__PURE__ */ (0, import_jsx_runtime34.jsx)("span", { className: "track-position-play-pause__spinner" }) : isPlaying ? /* @__PURE__ */ (0, import_jsx_runtime34.jsx)(import_fa5.FaPause, {}) : /* @__PURE__ */ (0, import_jsx_runtime34.jsx)(import_fa5.FaPlay, {})
2103
+ children: isAwaitingPlayback ? /* @__PURE__ */ (0, import_jsx_runtime33.jsx)("span", { className: "track-position-play-pause__spinner" }) : isPlaying ? /* @__PURE__ */ (0, import_jsx_runtime33.jsx)(import_fa5.FaPause, {}) : /* @__PURE__ */ (0, import_jsx_runtime33.jsx)(import_fa5.FaPlay, {})
2221
2104
  }
2222
2105
  )
2223
2106
  ]
@@ -2228,7 +2111,7 @@ function TrackPositionPlayPause({
2228
2111
  // src/genre-tree/track-list-sidebar/TrackItem.tsx
2229
2112
  var import_genre_tree12 = require("@behindthemusictree/app-kit/genre-tree");
2230
2113
  var import_genre_tree13 = require("@behindthemusictree/app-kit/genre-tree");
2231
- var import_jsx_runtime35 = require("react/jsx-runtime");
2114
+ var import_jsx_runtime34 = require("react/jsx-runtime");
2232
2115
  function TrackItem({
2233
2116
  track,
2234
2117
  position,
@@ -2252,12 +2135,12 @@ function TrackItem({
2252
2135
  loadTrackForPlayer(track.uuid);
2253
2136
  }
2254
2137
  };
2255
- return /* @__PURE__ */ (0, import_jsx_runtime35.jsxs)(
2138
+ return /* @__PURE__ */ (0, import_jsx_runtime34.jsxs)(
2256
2139
  "div",
2257
2140
  {
2258
2141
  className: `track-item flex h-14 text-[var(--color-text)] ${isUnplayable ? "opacity-50" : "hover:bg-[var(--color-surface-muted)] group"}`,
2259
2142
  children: [
2260
- isUnplayable ? /* @__PURE__ */ (0, import_jsx_runtime35.jsx)(
2143
+ isUnplayable ? /* @__PURE__ */ (0, import_jsx_runtime34.jsx)(
2261
2144
  "div",
2262
2145
  {
2263
2146
  className: "flex items-center justify-center text-lg w-16",
@@ -2265,7 +2148,7 @@ function TrackItem({
2265
2148
  "aria-disabled": "true",
2266
2149
  children: position
2267
2150
  }
2268
- ) : /* @__PURE__ */ (0, import_jsx_runtime35.jsx)(
2151
+ ) : /* @__PURE__ */ (0, import_jsx_runtime34.jsx)(
2269
2152
  TrackPositionPlayPause,
2270
2153
  {
2271
2154
  position,
@@ -2274,12 +2157,12 @@ function TrackItem({
2274
2157
  isSelected: selectedTrack?.uuid === track.uuid
2275
2158
  }
2276
2159
  ),
2277
- /* @__PURE__ */ (0, import_jsx_runtime35.jsxs)("div", { className: "title-artist-container flex flex-col items-start justify-center min-w-0 flex-1", children: [
2278
- /* @__PURE__ */ (0, import_jsx_runtime35.jsx)("div", { className: "flex max-w-full items-center gap-2", children: /* @__PURE__ */ (0, import_jsx_runtime35.jsx)("div", { className: "title text-lg font-bold text-[var(--color-text)] max-w-full truncate", children: renderSongLink ? renderSongLink(track) : (0, import_genre_tree12.isSong)(track) ? /* @__PURE__ */ (0, import_jsx_runtime35.jsx)(MusicBrainzRecordingLink, { song: track, className: "hover:underline focus-visible:underline", children: track.title }) : track.title }) }),
2279
- track.artists && track.artists.length > 0 ? /* @__PURE__ */ (0, import_jsx_runtime35.jsx)("div", { className: "artist text-base max-w-full truncate text-[var(--color-text-muted)]", children: /* @__PURE__ */ (0, import_jsx_runtime35.jsx)(import_genre_tree13.ArtistNames, { artists: track.artists, renderArtistLink }) }) : ""
2160
+ /* @__PURE__ */ (0, import_jsx_runtime34.jsxs)("div", { className: "title-artist-container flex flex-col items-start justify-center min-w-0 flex-1", children: [
2161
+ /* @__PURE__ */ (0, import_jsx_runtime34.jsx)("div", { className: "flex max-w-full items-center gap-2", children: /* @__PURE__ */ (0, import_jsx_runtime34.jsx)("div", { className: "title text-lg font-bold text-[var(--color-text)] max-w-full truncate", children: renderSongLink ? renderSongLink(track) : (0, import_genre_tree12.isSong)(track) ? /* @__PURE__ */ (0, import_jsx_runtime34.jsx)(MusicBrainzRecordingLink, { song: track, className: "hover:underline focus-visible:underline", children: track.title }) : track.title }) }),
2162
+ track.artists && track.artists.length > 0 ? /* @__PURE__ */ (0, import_jsx_runtime34.jsx)("div", { className: "artist text-base max-w-full truncate text-[var(--color-text-muted)]", children: /* @__PURE__ */ (0, import_jsx_runtime34.jsx)(import_genre_tree13.ArtistNames, { artists: track.artists, renderArtistLink }) }) : ""
2280
2163
  ] }),
2281
- /* @__PURE__ */ (0, import_jsx_runtime35.jsx)("div", { className: "genre-name-container flex items-center justify-start w-[21.6%] min-w-0 shrink-0", children: track.genre ? /* @__PURE__ */ (0, import_jsx_runtime35.jsx)("div", { className: "genre-name max-w-full whitespace-normal break-words p-1 text-xs font-bold", children: track.genre.name }) : "" }),
2282
- renderDuration && /* @__PURE__ */ (0, import_jsx_runtime35.jsx)(
2164
+ /* @__PURE__ */ (0, import_jsx_runtime34.jsx)("div", { className: "genre-name-container flex items-center justify-start w-[21.6%] min-w-0 shrink-0", children: track.genre ? /* @__PURE__ */ (0, import_jsx_runtime34.jsx)("div", { className: "genre-name max-w-full whitespace-normal break-words p-1 text-xs font-bold", children: track.genre.name }) : "" }),
2165
+ renderDuration && /* @__PURE__ */ (0, import_jsx_runtime34.jsx)(
2283
2166
  "div",
2284
2167
  {
2285
2168
  className: "duration flex text-base w-16 items-center justify-center",
@@ -2287,22 +2170,22 @@ function TrackItem({
2287
2170
  children: renderDuration(track)
2288
2171
  }
2289
2172
  ),
2290
- renderActions && /* @__PURE__ */ (0, import_jsx_runtime35.jsx)("div", { className: "edit flex text-base w-6 items-center justify-center mr-2 cursor-pointer", children: renderActions(track) })
2173
+ renderActions && /* @__PURE__ */ (0, import_jsx_runtime34.jsx)("div", { className: "edit flex text-base w-6 items-center justify-center mr-2 cursor-pointer", children: renderActions(track) })
2291
2174
  ]
2292
2175
  }
2293
2176
  );
2294
2177
  }
2295
2178
 
2296
2179
  // src/genre-tree/artist/ArtistSongs.tsx
2297
- var import_jsx_runtime36 = require("react/jsx-runtime");
2180
+ var import_jsx_runtime35 = require("react/jsx-runtime");
2298
2181
  var SongPageSchema2 = (0, import_transport8.PaginatedResponseSchema)(import_genre_tree14.SongDetailedSchema);
2299
2182
  var SORTS = [
2300
2183
  { ordering: "title", label: "Title" },
2301
2184
  { ordering: "-albumYear", label: "Year" }
2302
2185
  ];
2303
2186
  function ArtistSongs({ artist, getBackendBaseUrl, renderArtistLink, renderSongLink }) {
2304
- const [page, setPage] = (0, import_react15.useState)(1);
2305
- const [ordering, setOrdering] = (0, import_react15.useState)("title");
2187
+ const [page, setPage] = (0, import_react14.useState)(1);
2188
+ const [ordering, setOrdering] = (0, import_react14.useState)("title");
2306
2189
  const { fetch } = (0, import_transport8.useFetchWrapper)(getBackendBaseUrl);
2307
2190
  const { playNewTrackListFromArtist } = (0, import_genre_tree14.useTrackList)();
2308
2191
  const { playerTrackObject } = (0, import_player10.usePlayer)();
@@ -2327,21 +2210,21 @@ function ArtistSongs({ artist, getBackendBaseUrl, renderArtistLink, renderSongLi
2327
2210
  event.stopPropagation();
2328
2211
  void play(song.uuid);
2329
2212
  };
2330
- return /* @__PURE__ */ (0, import_jsx_runtime36.jsx)(import_ui13.PanelSection, { title: "Songs", children: isPending ? /* @__PURE__ */ (0, import_jsx_runtime36.jsx)(import_ui13.Skeleton, { className: "h-14", "aria-busy": "true" }) : isError ? /* @__PURE__ */ (0, import_jsx_runtime36.jsx)("p", { className: "text-red-600", children: "Could not load songs." }) : data.results.length === 0 ? /* @__PURE__ */ (0, import_jsx_runtime36.jsx)("p", { className: "text-[var(--color-text-muted)]", children: "No songs yet." }) : /* @__PURE__ */ (0, import_jsx_runtime36.jsxs)(import_jsx_runtime36.Fragment, { children: [
2331
- /* @__PURE__ */ (0, import_jsx_runtime36.jsxs)("div", { className: "flex flex-wrap items-center justify-between gap-2 mb-2", children: [
2332
- /* @__PURE__ */ (0, import_jsx_runtime36.jsxs)(
2213
+ return /* @__PURE__ */ (0, import_jsx_runtime35.jsx)(import_ui13.PanelSection, { title: "Songs", children: isPending ? /* @__PURE__ */ (0, import_jsx_runtime35.jsx)(import_ui13.Skeleton, { className: "h-14", "aria-busy": "true" }) : isError ? /* @__PURE__ */ (0, import_jsx_runtime35.jsx)("p", { className: "text-red-600", children: "Could not load songs." }) : data.results.length === 0 ? /* @__PURE__ */ (0, import_jsx_runtime35.jsx)("p", { className: "text-[var(--color-text-muted)]", children: "No songs yet." }) : /* @__PURE__ */ (0, import_jsx_runtime35.jsxs)(import_jsx_runtime35.Fragment, { children: [
2214
+ /* @__PURE__ */ (0, import_jsx_runtime35.jsxs)("div", { className: "flex flex-wrap items-center justify-between gap-2 mb-2", children: [
2215
+ /* @__PURE__ */ (0, import_jsx_runtime35.jsxs)(
2333
2216
  "button",
2334
2217
  {
2335
2218
  type: "button",
2336
2219
  onClick: () => void play(),
2337
2220
  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",
2338
2221
  children: [
2339
- /* @__PURE__ */ (0, import_jsx_runtime36.jsx)(import_lucide_react14.Play, { className: "h-3.5 w-3.5", "aria-hidden": "true" }),
2222
+ /* @__PURE__ */ (0, import_jsx_runtime35.jsx)(import_lucide_react13.Play, { className: "h-3.5 w-3.5", "aria-hidden": "true" }),
2340
2223
  "Play all"
2341
2224
  ]
2342
2225
  }
2343
2226
  ),
2344
- /* @__PURE__ */ (0, import_jsx_runtime36.jsx)("div", { role: "group", "aria-label": "Sort songs by", className: "flex gap-1 text-xs", children: SORTS.map((sort) => /* @__PURE__ */ (0, import_jsx_runtime36.jsx)(
2227
+ /* @__PURE__ */ (0, import_jsx_runtime35.jsx)("div", { role: "group", "aria-label": "Sort songs by", className: "flex gap-1 text-xs", children: SORTS.map((sort) => /* @__PURE__ */ (0, import_jsx_runtime35.jsx)(
2345
2228
  "button",
2346
2229
  {
2347
2230
  type: "button",
@@ -2356,7 +2239,7 @@ function ArtistSongs({ artist, getBackendBaseUrl, renderArtistLink, renderSongLi
2356
2239
  sort.ordering
2357
2240
  )) })
2358
2241
  ] }),
2359
- /* @__PURE__ */ (0, import_jsx_runtime36.jsx)("ul", { className: "overflow-hidden mb-2 bg-zinc-950 rounded-lg", children: data.results.map((song, index) => /* @__PURE__ */ (0, import_jsx_runtime36.jsx)("li", { onClickCapture: (event) => handleRowClickCapture(event, song), children: /* @__PURE__ */ (0, import_jsx_runtime36.jsx)(
2242
+ /* @__PURE__ */ (0, import_jsx_runtime35.jsx)("ul", { className: "overflow-hidden mb-2 bg-zinc-950 rounded-lg", children: data.results.map((song, index) => /* @__PURE__ */ (0, import_jsx_runtime35.jsx)("li", { onClickCapture: (event) => handleRowClickCapture(event, song), children: /* @__PURE__ */ (0, import_jsx_runtime35.jsx)(
2360
2243
  TrackItem,
2361
2244
  {
2362
2245
  track: song,
@@ -2365,12 +2248,12 @@ function ArtistSongs({ artist, getBackendBaseUrl, renderArtistLink, renderSongLi
2365
2248
  renderSongLink
2366
2249
  }
2367
2250
  ) }, song.uuid)) }),
2368
- /* @__PURE__ */ (0, import_jsx_runtime36.jsx)(import_ui13.Pagination, { currentPage: page, totalPages: data.totalPages, onPageChange: setPage })
2251
+ /* @__PURE__ */ (0, import_jsx_runtime35.jsx)(import_ui13.Pagination, { currentPage: page, totalPages: data.totalPages, onPageChange: setPage })
2369
2252
  ] }) });
2370
2253
  }
2371
2254
 
2372
2255
  // src/genre-tree/artist/useArtistPanel.tsx
2373
- var import_jsx_runtime37 = require("react/jsx-runtime");
2256
+ var import_jsx_runtime36 = require("react/jsx-runtime");
2374
2257
  function useArtistPanel({
2375
2258
  uuid,
2376
2259
  scope,
@@ -2391,26 +2274,26 @@ function useArtistPanel({
2391
2274
  return {
2392
2275
  ...panel,
2393
2276
  title: "Artist",
2394
- content: /* @__PURE__ */ (0, import_jsx_runtime37.jsxs)("div", { "aria-busy": "true", className: "flex flex-col gap-2.5 px-6 py-2.5", children: [
2395
- /* @__PURE__ */ (0, import_jsx_runtime37.jsx)(import_ui14.Skeleton, { className: "h-3 w-16" }),
2396
- /* @__PURE__ */ (0, import_jsx_runtime37.jsx)(import_ui14.Skeleton, { className: "h-4 w-32" }),
2397
- /* @__PURE__ */ (0, import_jsx_runtime37.jsx)(import_ui14.Skeleton, { className: "h-3 w-16" }),
2398
- /* @__PURE__ */ (0, import_jsx_runtime37.jsx)(import_ui14.Skeleton, { className: "h-4 w-24" }),
2399
- /* @__PURE__ */ (0, import_jsx_runtime37.jsx)(import_ui14.Skeleton, { className: "h-16 mt-2.5" }),
2400
- /* @__PURE__ */ (0, import_jsx_runtime37.jsx)(import_ui14.Skeleton, { className: "h-16" })
2277
+ content: /* @__PURE__ */ (0, import_jsx_runtime36.jsxs)("div", { "aria-busy": "true", className: "flex flex-col gap-2.5 px-6 py-2.5", children: [
2278
+ /* @__PURE__ */ (0, import_jsx_runtime36.jsx)(import_ui14.Skeleton, { className: "h-3 w-16" }),
2279
+ /* @__PURE__ */ (0, import_jsx_runtime36.jsx)(import_ui14.Skeleton, { className: "h-4 w-32" }),
2280
+ /* @__PURE__ */ (0, import_jsx_runtime36.jsx)(import_ui14.Skeleton, { className: "h-3 w-16" }),
2281
+ /* @__PURE__ */ (0, import_jsx_runtime36.jsx)(import_ui14.Skeleton, { className: "h-4 w-24" }),
2282
+ /* @__PURE__ */ (0, import_jsx_runtime36.jsx)(import_ui14.Skeleton, { className: "h-16 mt-2.5" }),
2283
+ /* @__PURE__ */ (0, import_jsx_runtime36.jsx)(import_ui14.Skeleton, { className: "h-16" })
2401
2284
  ] })
2402
2285
  };
2403
2286
  case "not-found":
2404
2287
  return {
2405
2288
  ...panel,
2406
2289
  title: "Artist not found",
2407
- content: /* @__PURE__ */ (0, import_jsx_runtime37.jsx)("p", { className: "px-6 py-2.5 text-[var(--color-text-muted)]", children: "This artist does not exist." })
2290
+ content: /* @__PURE__ */ (0, import_jsx_runtime36.jsx)("p", { className: "px-6 py-2.5 text-[var(--color-text-muted)]", children: "This artist does not exist." })
2408
2291
  };
2409
2292
  case "error":
2410
2293
  return {
2411
2294
  ...panel,
2412
2295
  title: "Artist",
2413
- content: /* @__PURE__ */ (0, import_jsx_runtime37.jsx)("p", { className: "px-6 py-2.5 text-red-600", children: "Could not load this artist." })
2296
+ content: /* @__PURE__ */ (0, import_jsx_runtime36.jsx)("p", { className: "px-6 py-2.5 text-red-600", children: "Could not load this artist." })
2414
2297
  };
2415
2298
  case "success": {
2416
2299
  const { artist } = result;
@@ -2419,13 +2302,13 @@ function useArtistPanel({
2419
2302
  return {
2420
2303
  ...panel,
2421
2304
  title: artist.name,
2422
- content: /* @__PURE__ */ (0, import_jsx_runtime37.jsxs)(import_jsx_runtime37.Fragment, { children: [
2423
- shareUrl && /* @__PURE__ */ (0, import_jsx_runtime37.jsx)("div", { className: "genre-action-band border-b border-[var(--color-border)]", children: /* @__PURE__ */ (0, import_jsx_runtime37.jsx)("div", { className: "flex flex-wrap gap-1 px-3.5 py-3", children: shareTarget && /* @__PURE__ */ (0, import_jsx_runtime37.jsx)(ShareActionButton, { target: shareTarget, url: shareUrl }) }) }),
2424
- artist.image && /* @__PURE__ */ (0, import_jsx_runtime37.jsx)(ArtistPhoto, { image: artist.image, name: artist.name }),
2425
- /* @__PURE__ */ (0, import_jsx_runtime37.jsx)(ArtistHeader, { artist }),
2426
- /* @__PURE__ */ (0, import_jsx_runtime37.jsx)(ArtistMembers, { title: "Members", memberships: artist.members, renderArtistLink }),
2427
- /* @__PURE__ */ (0, import_jsx_runtime37.jsx)(ArtistMembers, { title: "Member of", memberships: artist.memberOf, renderArtistLink }),
2428
- artist.tracksCount > 0 && /* @__PURE__ */ (0, import_jsx_runtime37.jsx)(
2305
+ content: /* @__PURE__ */ (0, import_jsx_runtime36.jsxs)(import_jsx_runtime36.Fragment, { children: [
2306
+ shareUrl && /* @__PURE__ */ (0, import_jsx_runtime36.jsx)("div", { className: "genre-action-band border-b border-[var(--color-border)]", children: /* @__PURE__ */ (0, import_jsx_runtime36.jsx)("div", { className: "flex flex-wrap gap-1 px-3.5 py-3", children: shareTarget && /* @__PURE__ */ (0, import_jsx_runtime36.jsx)(ShareActionButton, { target: shareTarget, url: shareUrl }) }) }),
2307
+ artist.image && /* @__PURE__ */ (0, import_jsx_runtime36.jsx)(ArtistPhoto, { image: artist.image, name: artist.name }),
2308
+ /* @__PURE__ */ (0, import_jsx_runtime36.jsx)(ArtistHeader, { artist }),
2309
+ /* @__PURE__ */ (0, import_jsx_runtime36.jsx)(ArtistMembers, { title: "Members", memberships: artist.members, renderArtistLink }),
2310
+ /* @__PURE__ */ (0, import_jsx_runtime36.jsx)(ArtistMembers, { title: "Member of", memberships: artist.memberOf, renderArtistLink }),
2311
+ artist.tracksCount > 0 && /* @__PURE__ */ (0, import_jsx_runtime36.jsx)(
2429
2312
  ArtistSongs,
2430
2313
  {
2431
2314
  artist: { uuid: artist.uuid, name: artist.name },
@@ -2435,7 +2318,7 @@ function useArtistPanel({
2435
2318
  }
2436
2319
  )
2437
2320
  ] }),
2438
- extraContent: artist.albums.length > 0 ? /* @__PURE__ */ (0, import_jsx_runtime37.jsx)(ArtistDiscography, { albums: artist.albums }) : void 0
2321
+ extraContent: artist.albums.length > 0 ? /* @__PURE__ */ (0, import_jsx_runtime36.jsx)(ArtistDiscography, { albums: artist.albums }) : void 0
2439
2322
  };
2440
2323
  }
2441
2324
  }
@@ -2446,45 +2329,45 @@ var import_ui16 = require("@behindthemusictree/ui");
2446
2329
  var import_genre_tree17 = require("@behindthemusictree/app-kit/genre-tree");
2447
2330
 
2448
2331
  // src/genre-tree/song/SongPanelContent.tsx
2449
- var import_react16 = require("react");
2450
- var import_lucide_react15 = require("lucide-react");
2332
+ var import_react15 = require("react");
2333
+ var import_lucide_react14 = require("lucide-react");
2451
2334
  var import_ui15 = require("@behindthemusictree/ui");
2452
2335
  var import_player11 = require("@behindthemusictree/app-kit/player");
2453
2336
  var import_genre_tree16 = require("@behindthemusictree/app-kit/genre-tree");
2454
- var import_jsx_runtime38 = require("react/jsx-runtime");
2337
+ var import_jsx_runtime37 = require("react/jsx-runtime");
2455
2338
  var numberFormat = new Intl.NumberFormat("en");
2456
2339
  var dateFormat = new Intl.DateTimeFormat("en", { dateStyle: "medium" });
2457
2340
  var externalLink = "text-[var(--color-primary)] hover:underline focus-visible:underline";
2458
2341
  function SongHistory({ uuid, getBackendBaseUrl }) {
2459
- const [open, setOpen] = (0, import_react16.useState)(false);
2342
+ const [open, setOpen] = (0, import_react15.useState)(false);
2460
2343
  const history = (0, import_genre_tree16.useSongHistory)(uuid, getBackendBaseUrl, open);
2461
2344
  let body = null;
2462
- if (history.status === "loading") body = /* @__PURE__ */ (0, import_jsx_runtime38.jsx)(import_ui15.Skeleton, { className: "h-10", "aria-busy": "true" });
2463
- else if (history.status === "error") body = /* @__PURE__ */ (0, import_jsx_runtime38.jsx)("p", { className: "text-red-600", children: "Could not load the history." });
2345
+ if (history.status === "loading") body = /* @__PURE__ */ (0, import_jsx_runtime37.jsx)(import_ui15.Skeleton, { className: "h-10", "aria-busy": "true" });
2346
+ else if (history.status === "error") body = /* @__PURE__ */ (0, import_jsx_runtime37.jsx)("p", { className: "text-red-600", children: "Could not load the history." });
2464
2347
  else if (history.status === "success")
2465
- body = history.entries.length === 0 ? /* @__PURE__ */ (0, import_jsx_runtime38.jsx)("p", { className: "text-[var(--color-text-muted)]", children: "No changes yet." }) : /* @__PURE__ */ (0, import_jsx_runtime38.jsx)("ul", { className: "divide-y divide-[var(--color-border)]", children: history.entries.map((entry) => /* @__PURE__ */ (0, import_jsx_runtime38.jsxs)("li", { className: "py-1.5 text-sm", children: [
2466
- /* @__PURE__ */ (0, import_jsx_runtime38.jsx)("span", { className: "font-medium", children: entry.action }),
2467
- entry.oldValue !== null || entry.newValue !== null ? /* @__PURE__ */ (0, import_jsx_runtime38.jsxs)("span", { className: "text-[var(--color-text-muted)]", children: [
2348
+ body = history.entries.length === 0 ? /* @__PURE__ */ (0, import_jsx_runtime37.jsx)("p", { className: "text-[var(--color-text-muted)]", children: "No changes yet." }) : /* @__PURE__ */ (0, import_jsx_runtime37.jsx)("ul", { className: "divide-y divide-[var(--color-border)]", children: history.entries.map((entry) => /* @__PURE__ */ (0, import_jsx_runtime37.jsxs)("li", { className: "py-1.5 text-sm", children: [
2349
+ /* @__PURE__ */ (0, import_jsx_runtime37.jsx)("span", { className: "font-medium", children: entry.action }),
2350
+ entry.oldValue !== null || entry.newValue !== null ? /* @__PURE__ */ (0, import_jsx_runtime37.jsxs)("span", { className: "text-[var(--color-text-muted)]", children: [
2468
2351
  " ",
2469
2352
  "\u2014 ",
2470
2353
  entry.oldValue ?? "none",
2471
2354
  " \u2192 ",
2472
2355
  entry.newValue ?? "none"
2473
2356
  ] }) : null,
2474
- /* @__PURE__ */ (0, import_jsx_runtime38.jsxs)("span", { className: "block text-xs text-[var(--color-text-muted)]", children: [
2357
+ /* @__PURE__ */ (0, import_jsx_runtime37.jsxs)("span", { className: "block text-xs text-[var(--color-text-muted)]", children: [
2475
2358
  entry.actorPseudo ?? "Anonymous",
2476
2359
  " \xB7 ",
2477
2360
  dateFormat.format(new Date(entry.createdOn))
2478
2361
  ] })
2479
2362
  ] }, entry.uuid)) });
2480
- return /* @__PURE__ */ (0, import_jsx_runtime38.jsxs)(
2363
+ return /* @__PURE__ */ (0, import_jsx_runtime37.jsxs)(
2481
2364
  "details",
2482
2365
  {
2483
2366
  onToggle: (event) => setOpen(event.currentTarget.open),
2484
2367
  className: "px-6 pb-2.5 mt-2.5 border-t border-[var(--color-border)]",
2485
2368
  children: [
2486
- /* @__PURE__ */ (0, import_jsx_runtime38.jsx)("summary", { className: "mt-2.5 cursor-pointer text-[0.6875rem] font-semibold uppercase text-[var(--color-text-muted)]", children: "History" }),
2487
- /* @__PURE__ */ (0, import_jsx_runtime38.jsx)("div", { className: "mt-1.5", children: body })
2369
+ /* @__PURE__ */ (0, import_jsx_runtime37.jsx)("summary", { className: "mt-2.5 cursor-pointer text-[0.6875rem] font-semibold uppercase text-[var(--color-text-muted)]", children: "History" }),
2370
+ /* @__PURE__ */ (0, import_jsx_runtime37.jsx)("div", { className: "mt-1.5", children: body })
2488
2371
  ]
2489
2372
  }
2490
2373
  );
@@ -2510,13 +2393,13 @@ function SongPanelContent({
2510
2393
  const path = genrePath.length > 0 ? genrePath : null;
2511
2394
  fields.push({
2512
2395
  label: "Genre",
2513
- value: path ? /* @__PURE__ */ (0, import_jsx_runtime38.jsx)("span", { className: "flex flex-wrap items-center gap-x-1", children: path.map((genrePlaylist, index) => /* @__PURE__ */ (0, import_jsx_runtime38.jsxs)("span", { children: [
2514
- index > 0 && /* @__PURE__ */ (0, import_jsx_runtime38.jsx)("span", { "aria-hidden": "true", children: "\u203A " }),
2515
- /* @__PURE__ */ (0, import_jsx_runtime38.jsx)("button", { type: "button", onClick: () => onSelectGenre(genrePlaylist), className: "hover:underline", children: genrePlaylist.name })
2396
+ value: path ? /* @__PURE__ */ (0, import_jsx_runtime37.jsx)("span", { className: "flex flex-wrap items-center gap-x-1", children: path.map((genrePlaylist, index) => /* @__PURE__ */ (0, import_jsx_runtime37.jsxs)("span", { children: [
2397
+ index > 0 && /* @__PURE__ */ (0, import_jsx_runtime37.jsx)("span", { "aria-hidden": "true", children: "\u203A " }),
2398
+ /* @__PURE__ */ (0, import_jsx_runtime37.jsx)("button", { type: "button", onClick: () => onSelectGenre(genrePlaylist), className: "hover:underline", children: genrePlaylist.name })
2516
2399
  ] }, genrePlaylist.uuid)) }) : song.genre.name
2517
2400
  });
2518
2401
  } else {
2519
- fields.push({ label: "Genre", value: /* @__PURE__ */ (0, import_jsx_runtime38.jsx)("span", { className: "text-[var(--color-text-muted)]", children: "No genre" }) });
2402
+ fields.push({ label: "Genre", value: /* @__PURE__ */ (0, import_jsx_runtime37.jsx)("span", { className: "text-[var(--color-text-muted)]", children: "No genre" }) });
2520
2403
  }
2521
2404
  if (song.firstReleaseYear) fields.push({ label: "First released", value: song.firstReleaseYear });
2522
2405
  const viewCount = song.youtubeVideo?.viewCount;
@@ -2526,11 +2409,11 @@ function SongPanelContent({
2526
2409
  const links = [];
2527
2410
  if (musicBrainzUrl)
2528
2411
  links.push(
2529
- /* @__PURE__ */ (0, import_jsx_runtime38.jsx)("a", { href: musicBrainzUrl, target: "_blank", rel: "noopener noreferrer", className: externalLink, children: "MusicBrainz" }, "mb")
2412
+ /* @__PURE__ */ (0, import_jsx_runtime37.jsx)("a", { href: musicBrainzUrl, target: "_blank", rel: "noopener noreferrer", className: externalLink, children: "MusicBrainz" }, "mb")
2530
2413
  );
2531
2414
  if (song.youtubeVideo)
2532
2415
  links.push(
2533
- /* @__PURE__ */ (0, import_jsx_runtime38.jsx)(
2416
+ /* @__PURE__ */ (0, import_jsx_runtime37.jsx)(
2534
2417
  "a",
2535
2418
  {
2536
2419
  href: `https://www.youtube.com/watch?v=${encodeURIComponent(song.youtubeVideo.id)}`,
@@ -2545,33 +2428,33 @@ function SongPanelContent({
2545
2428
  if (links.length > 0)
2546
2429
  fields.push({
2547
2430
  label: "Links",
2548
- value: /* @__PURE__ */ (0, import_jsx_runtime38.jsx)("span", { className: "flex gap-3", children: links })
2431
+ value: /* @__PURE__ */ (0, import_jsx_runtime37.jsx)("span", { className: "flex gap-3", children: links })
2549
2432
  });
2550
- return /* @__PURE__ */ (0, import_jsx_runtime38.jsxs)(import_jsx_runtime38.Fragment, { children: [
2551
- /* @__PURE__ */ (0, import_jsx_runtime38.jsx)("div", { className: "genre-action-band border-b border-[var(--color-border)]", children: /* @__PURE__ */ (0, import_jsx_runtime38.jsxs)("div", { className: "flex flex-wrap gap-1 px-3.5 py-3", children: [
2552
- unplayableReason ? /* @__PURE__ */ (0, import_jsx_runtime38.jsx)("p", { className: "flex items-center px-2.5 text-sm text-[var(--color-text-muted)]", children: unplayableReason }) : /* @__PURE__ */ (0, import_jsx_runtime38.jsxs)(
2433
+ return /* @__PURE__ */ (0, import_jsx_runtime37.jsxs)(import_jsx_runtime37.Fragment, { children: [
2434
+ /* @__PURE__ */ (0, import_jsx_runtime37.jsx)("div", { className: "genre-action-band border-b border-[var(--color-border)]", children: /* @__PURE__ */ (0, import_jsx_runtime37.jsxs)("div", { className: "flex flex-wrap gap-1 px-3.5 py-3", children: [
2435
+ unplayableReason ? /* @__PURE__ */ (0, import_jsx_runtime37.jsx)("p", { className: "flex items-center px-2.5 text-sm text-[var(--color-text-muted)]", children: unplayableReason }) : /* @__PURE__ */ (0, import_jsx_runtime37.jsxs)(
2553
2436
  "button",
2554
2437
  {
2555
2438
  type: "button",
2556
2439
  onClick: () => isCurrent ? handlePlayPauseAction() : playNewTrackListFromTrackUuid(song, scope),
2557
2440
  className: "group flex flex-col items-center gap-1 w-16 text-xs text-center text-[var(--color-text)] focus-visible:outline-none",
2558
2441
  children: [
2559
- /* @__PURE__ */ (0, import_jsx_runtime38.jsx)("span", { className: "flex items-center justify-center w-10 h-10 rounded-full group-focus-visible:ring-2 group-focus-visible:ring-[var(--color-focus-ring)] bg-[var(--color-surface-muted)] transition-colors group-hover:bg-[var(--color-border)]", children: isPlaying ? /* @__PURE__ */ (0, import_jsx_runtime38.jsx)(import_lucide_react15.Pause, { "aria-hidden": "true", className: "w-5 h-5" }) : /* @__PURE__ */ (0, import_jsx_runtime38.jsx)(import_lucide_react15.Play, { "aria-hidden": "true", className: "w-5 h-5" }) }),
2442
+ /* @__PURE__ */ (0, import_jsx_runtime37.jsx)("span", { className: "flex items-center justify-center w-10 h-10 rounded-full group-focus-visible:ring-2 group-focus-visible:ring-[var(--color-focus-ring)] bg-[var(--color-surface-muted)] transition-colors group-hover:bg-[var(--color-border)]", children: isPlaying ? /* @__PURE__ */ (0, import_jsx_runtime37.jsx)(import_lucide_react14.Pause, { "aria-hidden": "true", className: "w-5 h-5" }) : /* @__PURE__ */ (0, import_jsx_runtime37.jsx)(import_lucide_react14.Play, { "aria-hidden": "true", className: "w-5 h-5" }) }),
2560
2443
  isPlaying ? "Pause" : "Play"
2561
2444
  ]
2562
2445
  }
2563
2446
  ),
2564
- shareUrl && /* @__PURE__ */ (0, import_jsx_runtime38.jsx)(ShareActionButton, { target: shareTarget, url: shareUrl })
2447
+ shareUrl && /* @__PURE__ */ (0, import_jsx_runtime37.jsx)(ShareActionButton, { target: shareTarget, url: shareUrl })
2565
2448
  ] }) }),
2566
- song.artists && song.artists.length > 0 && /* @__PURE__ */ (0, import_jsx_runtime38.jsx)("p", { className: "px-6 pt-2.5 text-[var(--color-text-muted)]", children: /* @__PURE__ */ (0, import_jsx_runtime38.jsx)(import_genre_tree16.ArtistNames, { artists: song.artists, renderArtistLink }) }),
2567
- /* @__PURE__ */ (0, import_jsx_runtime38.jsx)(import_ui15.FieldList, { items: fields }),
2568
- song.essentialGenres && song.essentialGenres.length > 0 && /* @__PURE__ */ (0, import_jsx_runtime38.jsx)(import_ui15.PanelSection, { title: "Essential in", children: /* @__PURE__ */ (0, import_jsx_runtime38.jsx)("ul", { children: song.essentialGenres.map((genre) => /* @__PURE__ */ (0, import_jsx_runtime38.jsx)("li", { className: "py-0.5", children: genre.name }, genre.uuid)) }) }),
2569
- /* @__PURE__ */ (0, import_jsx_runtime38.jsx)(SongHistory, { uuid: song.uuid, getBackendBaseUrl })
2449
+ song.artists && song.artists.length > 0 && /* @__PURE__ */ (0, import_jsx_runtime37.jsx)("p", { className: "px-6 pt-2.5 text-[var(--color-text-muted)]", children: /* @__PURE__ */ (0, import_jsx_runtime37.jsx)(import_genre_tree16.ArtistNames, { artists: song.artists, renderArtistLink }) }),
2450
+ /* @__PURE__ */ (0, import_jsx_runtime37.jsx)(import_ui15.FieldList, { items: fields }),
2451
+ song.essentialGenres && song.essentialGenres.length > 0 && /* @__PURE__ */ (0, import_jsx_runtime37.jsx)(import_ui15.PanelSection, { title: "Essential in", children: /* @__PURE__ */ (0, import_jsx_runtime37.jsx)("ul", { children: song.essentialGenres.map((genre) => /* @__PURE__ */ (0, import_jsx_runtime37.jsx)("li", { className: "py-0.5", children: genre.name }, genre.uuid)) }) }),
2452
+ /* @__PURE__ */ (0, import_jsx_runtime37.jsx)(SongHistory, { uuid: song.uuid, getBackendBaseUrl })
2570
2453
  ] });
2571
2454
  }
2572
2455
 
2573
2456
  // src/genre-tree/song/useSongPanel.tsx
2574
- var import_jsx_runtime39 = require("react/jsx-runtime");
2457
+ var import_jsx_runtime38 = require("react/jsx-runtime");
2575
2458
  function useSongPanel({
2576
2459
  uuid,
2577
2460
  scope,
@@ -2593,24 +2476,24 @@ function useSongPanel({
2593
2476
  return {
2594
2477
  ...panel,
2595
2478
  title: "Song",
2596
- content: /* @__PURE__ */ (0, import_jsx_runtime39.jsxs)("div", { "aria-busy": "true", className: "flex flex-col gap-2.5 px-6 py-2.5", children: [
2597
- /* @__PURE__ */ (0, import_jsx_runtime39.jsx)(import_ui16.Skeleton, { className: "h-3 w-16" }),
2598
- /* @__PURE__ */ (0, import_jsx_runtime39.jsx)(import_ui16.Skeleton, { className: "h-4 w-32" }),
2599
- /* @__PURE__ */ (0, import_jsx_runtime39.jsx)(import_ui16.Skeleton, { className: "h-3 w-16" }),
2600
- /* @__PURE__ */ (0, import_jsx_runtime39.jsx)(import_ui16.Skeleton, { className: "h-4 w-24" })
2479
+ content: /* @__PURE__ */ (0, import_jsx_runtime38.jsxs)("div", { "aria-busy": "true", className: "flex flex-col gap-2.5 px-6 py-2.5", children: [
2480
+ /* @__PURE__ */ (0, import_jsx_runtime38.jsx)(import_ui16.Skeleton, { className: "h-3 w-16" }),
2481
+ /* @__PURE__ */ (0, import_jsx_runtime38.jsx)(import_ui16.Skeleton, { className: "h-4 w-32" }),
2482
+ /* @__PURE__ */ (0, import_jsx_runtime38.jsx)(import_ui16.Skeleton, { className: "h-3 w-16" }),
2483
+ /* @__PURE__ */ (0, import_jsx_runtime38.jsx)(import_ui16.Skeleton, { className: "h-4 w-24" })
2601
2484
  ] })
2602
2485
  };
2603
2486
  case "not-found":
2604
2487
  return {
2605
2488
  ...panel,
2606
2489
  title: "Song not found",
2607
- content: /* @__PURE__ */ (0, import_jsx_runtime39.jsx)("p", { className: "px-6 py-2.5 text-[var(--color-text-muted)]", children: "This song does not exist." })
2490
+ content: /* @__PURE__ */ (0, import_jsx_runtime38.jsx)("p", { className: "px-6 py-2.5 text-[var(--color-text-muted)]", children: "This song does not exist." })
2608
2491
  };
2609
2492
  case "error":
2610
2493
  return {
2611
2494
  ...panel,
2612
2495
  title: "Song",
2613
- content: /* @__PURE__ */ (0, import_jsx_runtime39.jsx)("p", { className: "px-6 py-2.5 text-red-600", children: "Could not load this song." })
2496
+ content: /* @__PURE__ */ (0, import_jsx_runtime38.jsx)("p", { className: "px-6 py-2.5 text-red-600", children: "Could not load this song." })
2614
2497
  };
2615
2498
  case "success": {
2616
2499
  const { song } = result;
@@ -2618,7 +2501,7 @@ function useSongPanel({
2618
2501
  return {
2619
2502
  ...panel,
2620
2503
  title: song.title,
2621
- content: /* @__PURE__ */ (0, import_jsx_runtime39.jsx)(
2504
+ content: /* @__PURE__ */ (0, import_jsx_runtime38.jsx)(
2622
2505
  SongPanelContent,
2623
2506
  {
2624
2507
  song,
@@ -2638,12 +2521,12 @@ function useSongPanel({
2638
2521
  // src/genre-tree/SizeMetricToggle.tsx
2639
2522
  var import_md2 = require("react-icons/md");
2640
2523
  var import_ui17 = require("@behindthemusictree/ui");
2641
- var import_jsx_runtime40 = require("react/jsx-runtime");
2524
+ var import_jsx_runtime39 = require("react/jsx-runtime");
2642
2525
  function SizeMetricToggle({
2643
2526
  value,
2644
2527
  onChange
2645
2528
  }) {
2646
- return /* @__PURE__ */ (0, import_jsx_runtime40.jsx)(
2529
+ return /* @__PURE__ */ (0, import_jsx_runtime39.jsx)(
2647
2530
  import_ui17.SegmentedControl,
2648
2531
  {
2649
2532
  variant: "floating",
@@ -2651,26 +2534,19 @@ function SizeMetricToggle({
2651
2534
  value,
2652
2535
  onChange,
2653
2536
  options: [
2654
- { value: "songs", label: /* @__PURE__ */ (0, import_jsx_runtime40.jsx)(import_md2.MdMusicNote, { size: 18, "aria-hidden": true }), "aria-label": "Size by songs", title: "Node size by song count" },
2655
- { value: "views", label: /* @__PURE__ */ (0, import_jsx_runtime40.jsx)(import_md2.MdVisibility, { size: 18, "aria-hidden": true }), "aria-label": "Size by views", title: "Node size by song views" }
2537
+ { value: "songs", label: /* @__PURE__ */ (0, import_jsx_runtime39.jsx)(import_md2.MdMusicNote, { size: 18, "aria-hidden": true }), "aria-label": "Size by songs", title: "Node size by song count" },
2538
+ { value: "views", label: /* @__PURE__ */ (0, import_jsx_runtime39.jsx)(import_md2.MdVisibility, { size: 18, "aria-hidden": true }), "aria-label": "Size by views", title: "Node size by song views" }
2656
2539
  ]
2657
2540
  }
2658
2541
  );
2659
2542
  }
2660
2543
 
2661
2544
  // src/genre-tree/GenreTreeView.tsx
2662
- var import_jsx_runtime41 = require("react/jsx-runtime");
2663
- var PREVIOUS_BUTTON_CSS = `
2664
- .genre-tree-view .search-overlay .gtv-info-panel-previous { display: none; }
2665
- .genre-tree-view:has(.search-overlay .gtv-info-panel-previous) .genre-search input { padding-left: 2.75rem; }
2666
- .genre-tree-view:has(.search-overlay .gtv-info-panel-previous) .genre-search-previous { display: flex; }
2667
- @keyframes genre-tree-view-previous-in { from { opacity: 0; transform: translate(-0.5rem, -50%); } to { opacity: 1; transform: translate(0, -50%); } }
2668
- @media (prefers-reduced-motion: no-preference) {
2669
- .genre-search-previous { animation: genre-tree-view-previous-in 200ms ease-out; }
2670
- }
2671
- `;
2545
+ var import_jsx_runtime40 = require("react/jsx-runtime");
2672
2546
  var NO_POP_ROOT_TITLE = "This genre tree has no 'Mainstream Pop' root yet";
2673
- var REGIONS_CLASS = "[--gtv-center-left:var(--layout-center-left)] [--gtv-center-right:var(--layout-center-right)] [--gtv-center-top:var(--layout-band-top)] [&_.gtv-info-panel--overlay]:[--gtv-info-panel-header-inset:0px]";
2547
+ var REGIONS_CLASS = "[--gtv-center-left:var(--layout-center-left)] [--gtv-center-right:var(--layout-center-right)] [--gtv-center-top:var(--layout-band-top)]";
2548
+ var noop = () => {
2549
+ };
2674
2550
  function GenreTreeView({
2675
2551
  scope,
2676
2552
  handleGenreCreationAction,
@@ -2724,21 +2600,21 @@ function GenreTreeView({
2724
2600
  placement = "default",
2725
2601
  ...focus
2726
2602
  }) {
2727
- const getShareUrlRef = (0, import_react17.useRef)(getShareUrl);
2603
+ const getShareUrlRef = (0, import_react16.useRef)(getShareUrl);
2728
2604
  getShareUrlRef.current = getShareUrl;
2729
- const [internalArtistUuid, setInternalArtistUuid] = (0, import_react17.useState)(null);
2605
+ const [internalArtistUuid, setInternalArtistUuid] = (0, import_react16.useState)(null);
2730
2606
  const isArtistControlled = controlledArtistUuid !== void 0;
2731
2607
  const artistUuid = isArtistControlled ? controlledArtistUuid : internalArtistUuid;
2732
- const [internalSongUuid, setInternalSongUuid] = (0, import_react17.useState)(null);
2608
+ const [internalSongUuid, setInternalSongUuid] = (0, import_react16.useState)(null);
2733
2609
  const isSongControlled = controlledSongUuid !== void 0;
2734
2610
  const songUuid = isSongControlled ? controlledSongUuid : internalSongUuid;
2735
2611
  const changeSong = (uuid) => {
2736
2612
  if (!isSongControlled) setInternalSongUuid(uuid);
2737
2613
  onSongUuidChange?.(uuid);
2738
2614
  };
2739
- const [internalSizeMetric, setInternalSizeMetric] = (0, import_react17.useState)("songs");
2615
+ const [internalSizeMetric, setInternalSizeMetric] = (0, import_react16.useState)("songs");
2740
2616
  const sizeMetric = controlledSizeMetric ?? internalSizeMetric;
2741
- const deferredSizeMetric = (0, import_react17.useDeferredValue)(sizeMetric);
2617
+ const deferredSizeMetric = (0, import_react16.useDeferredValue)(sizeMetric);
2742
2618
  const changeSizeMetric = (metric) => {
2743
2619
  if (controlledSizeMetric === void 0) setInternalSizeMetric(metric);
2744
2620
  onSizeMetricChange?.(metric);
@@ -2775,7 +2651,7 @@ function GenreTreeView({
2775
2651
  artistUuid
2776
2652
  });
2777
2653
  const restoreRevision = viewportRestore?.revision;
2778
- const [stackedRestoreOwner, setStackedRestoreOwner] = (0, import_react17.useState)({ revision: restoreRevision, rootUuid: selectedGenrePlaylist?.root.uuid });
2654
+ const [stackedRestoreOwner, setStackedRestoreOwner] = (0, import_react16.useState)({ revision: restoreRevision, rootUuid: selectedGenrePlaylist?.root.uuid });
2779
2655
  let restoreRootUuid = stackedRestoreOwner.rootUuid;
2780
2656
  if (stackedRestoreOwner.revision !== restoreRevision) {
2781
2657
  restoreRootUuid = selectedGenrePlaylist?.root.uuid;
@@ -2791,7 +2667,7 @@ function GenreTreeView({
2791
2667
  const canSizeByViews = !isArtistFiltered && hasViewsCount(visibleGenrePlaylists);
2792
2668
  const artistResult = (0, import_genre_tree19.useArtist)(artistUuid, artistSearchBaseUrl ?? getBackendBaseUrl);
2793
2669
  const filterArtistName = artistResult.status === "success" ? artistResult.artist.name : null;
2794
- const filterArtist = (0, import_react17.useMemo)(
2670
+ const filterArtist = (0, import_react16.useMemo)(
2795
2671
  () => artistUuid !== null && filterArtistName !== null ? { uuid: artistUuid, name: filterArtistName } : null,
2796
2672
  [artistUuid, filterArtistName]
2797
2673
  );
@@ -2806,14 +2682,14 @@ function GenreTreeView({
2806
2682
  if (selectedNodeId !== null) clearSelection();
2807
2683
  changeSong(uuid);
2808
2684
  };
2809
- const openSongRef = (0, import_react17.useRef)(openSong);
2685
+ const openSongRef = (0, import_react16.useRef)(openSong);
2810
2686
  openSongRef.current = openSong;
2811
- const resolvedRenderSongLink = (0, import_react17.useCallback)(
2812
- (song) => renderSongLink ? renderSongLink(song) : /* @__PURE__ */ (0, import_jsx_runtime41.jsx)("button", { type: "button", onClick: () => openSongRef.current(song.uuid), className: "hover:underline", children: song.title }),
2687
+ const resolvedRenderSongLink = (0, import_react16.useCallback)(
2688
+ (song) => renderSongLink ? renderSongLink(song) : /* @__PURE__ */ (0, import_jsx_runtime40.jsx)("button", { type: "button", onClick: () => openSongRef.current(song.uuid), className: "hover:underline", children: song.title }),
2813
2689
  [renderSongLink]
2814
2690
  );
2815
- const [expandedSongUuid, setExpandedSongUuid] = (0, import_react17.useState)(null);
2816
- const [expandedArtistUuid, setExpandedArtistUuid] = (0, import_react17.useState)(null);
2691
+ const [expandedSongUuid, setExpandedSongUuid] = (0, import_react16.useState)(null);
2692
+ const [expandedArtistUuid, setExpandedArtistUuid] = (0, import_react16.useState)(null);
2817
2693
  const artistBaseUrl = artistSearchBaseUrl ?? getBackendBaseUrl;
2818
2694
  const songBaseUrl = getBackendBaseUrl;
2819
2695
  const artistPanel = useArtistPanel({
@@ -2821,7 +2697,7 @@ function GenreTreeView({
2821
2697
  scope,
2822
2698
  getBackendBaseUrl: artistBaseUrl,
2823
2699
  getShareUrl: getShareUrlRef.current,
2824
- renderArtistLink: renderArtistLink ?? ((artist) => artist.isSpecialPurpose ? artist.name : /* @__PURE__ */ (0, import_jsx_runtime41.jsx)("button", { type: "button", onClick: () => openArtist(artist.uuid), className: "hover:underline", children: artist.name })),
2700
+ renderArtistLink: renderArtistLink ?? ((artist) => artist.isSpecialPurpose ? artist.name : /* @__PURE__ */ (0, import_jsx_runtime40.jsx)("button", { type: "button", onClick: () => openArtist(artist.uuid), className: "hover:underline", children: artist.name })),
2825
2701
  expanded: controlledArtistExpanded ?? (artistUuid !== null && expandedArtistUuid === artistUuid),
2826
2702
  onExpandedChange: (on) => {
2827
2703
  if (controlledArtistExpanded === void 0) setExpandedArtistUuid(on ? artistUuid : null);
@@ -2830,7 +2706,7 @@ function GenreTreeView({
2830
2706
  renderSongLink: resolvedRenderSongLink,
2831
2707
  onClose: () => changeArtist(null)
2832
2708
  });
2833
- const resolvedRenderArtistLink = renderArtistLink ?? ((artist) => artist.isSpecialPurpose ? artist.name : /* @__PURE__ */ (0, import_jsx_runtime41.jsx)("button", { type: "button", onClick: () => openArtist(artist.uuid), className: "hover:underline", children: artist.name }));
2709
+ const resolvedRenderArtistLink = renderArtistLink ?? ((artist) => artist.isSpecialPurpose ? artist.name : /* @__PURE__ */ (0, import_jsx_runtime40.jsx)("button", { type: "button", onClick: () => openArtist(artist.uuid), className: "hover:underline", children: artist.name }));
2834
2710
  const getGenrePath = (genre) => {
2835
2711
  const path = [];
2836
2712
  let current = visibleGenrePlaylists.find((playlist) => playlist.criteria?.uuid === genre.uuid);
@@ -2862,9 +2738,8 @@ function GenreTreeView({
2862
2738
  const builtInSongPanel = overlayPanel || hasSelectedGenreNode ? void 0 : songPanel;
2863
2739
  const builtInArtistPanel = overlayPanel || hasSelectedGenreNode || builtInSongPanel ? void 0 : artistPanel;
2864
2740
  const builtInPanel = builtInSongPanel ?? builtInArtistPanel;
2865
- const activeOverlayPanel = overlayPanel ?? (builtInPanel && { ...builtInPanel, hideTitle: true, hideClose: true });
2866
- const [uncontrolledCollapsed, setUncontrolledCollapsed] = (0, import_react17.useState)(false);
2867
- const [uncontrolledExpanded, setUncontrolledExpanded] = (0, import_react17.useState)(false);
2741
+ const [uncontrolledCollapsed, setUncontrolledCollapsed] = (0, import_react16.useState)(false);
2742
+ const [uncontrolledExpanded, setUncontrolledExpanded] = (0, import_react16.useState)(false);
2868
2743
  const isCollapsed = collapsed ?? uncontrolledCollapsed;
2869
2744
  const isExpanded = expanded ?? uncontrolledExpanded;
2870
2745
  const changeCollapsed = (next) => {
@@ -2875,55 +2750,56 @@ function GenreTreeView({
2875
2750
  setUncontrolledExpanded(next);
2876
2751
  onExpandedChange?.(next);
2877
2752
  };
2878
- (0, import_react17.useEffect)(() => {
2753
+ (0, import_react16.useEffect)(() => {
2879
2754
  setUncontrolledCollapsed(false);
2880
2755
  }, [selectedNodeId, artistUuid, songUuid]);
2881
- const isDetailHidden = isCollapsed || !hasSelectedGenreNode && !activeOverlayPanel;
2882
- useLeftDetailPanelLayout(!isDetailHidden, placement === "regions");
2883
- const detailOverlay = placement === "regions" && activeOverlayPanel?.onExpandedChange ? activeOverlayPanel : void 0;
2884
- const detailExpanded = detailOverlay ? !!detailOverlay.expanded : isExpanded;
2885
- const onDetailExpandedChange = detailOverlay ? detailOverlay.onExpandedChange : changeExpanded;
2886
- const infoPanelCuration = {
2887
- renderDangerZone,
2888
- expanded: placement === "regions" ? isExpanded : expanded,
2889
- onExpandedChange: placement === "regions" ? changeExpanded : onExpandedChange,
2890
- collapsed: placement === "regions" ? isCollapsed : void 0,
2891
- hideExpandButton: placement === "regions" ? true : void 0,
2892
- overlayPanel: detailOverlay ? { ...detailOverlay, collapsed: isCollapsed, hideExpandButton: true } : activeOverlayPanel
2893
- };
2894
- const [isWheelRevealed, setIsWheelRevealed] = (0, import_react17.useState)(false);
2756
+ const [isWheelRevealed, setIsWheelRevealed] = (0, import_react16.useState)(false);
2895
2757
  const usesHandoff = viewMode === "wheel" || viewMode === "pop-core";
2896
2758
  const isTreeReady = !isLoading && (!usesHandoff || isWheelRevealed);
2897
- const rootRef = (0, import_react17.useRef)(null);
2898
- const handlePrevious = () => {
2899
- rootRef.current?.querySelector(".search-overlay .gtv-info-panel-previous")?.click();
2900
- };
2901
- (0, import_react17.useEffect)(() => {
2902
- if (!isTreeReady || viewMode !== "pop-core" || !hasSelectedGenreNode || !selectedGenrePlaylist) return;
2903
- const root = rootRef.current;
2904
- const centerChip = root?.querySelector(".gtv-wheel-center-node .gtv-wheel-chip");
2905
- if (!centerChip || centerChip.getAttribute("aria-label") !== selectedGenrePlaylist.name) return;
2906
- const panelTitle = root?.querySelector(".gtv-info-panel-title");
2907
- if (panelTitle?.textContent?.trim().startsWith(selectedGenrePlaylist.name)) return;
2908
- centerChip.click();
2909
- }, [hasSelectedGenreNode, isTreeReady, selectedGenrePlaylist, viewMode]);
2759
+ const [resolveStyle, setResolveStyle] = (0, import_react16.useState)(null);
2760
+ const handleNodeStyleResolverChange = (0, import_react16.useCallback)(
2761
+ (resolver) => setResolveStyle(() => resolver),
2762
+ []
2763
+ );
2764
+ const panelRef = (0, import_react16.useRef)(null);
2765
+ const obscuredInset = (0, import_ui18.useObscuredInset)(panelRef);
2766
+ const hostPanel = { obscuredInset, onNodeStyleResolverChange: handleNodeStyleResolverChange };
2767
+ const nodeMetric = isArtistFiltered || !canSizeByViews ? "songs" : deferredSizeMetric;
2768
+ const treeNodes = (0, import_react16.useMemo)(() => toTreeNodes(visibleGenrePlaylists, nodeMetric), [visibleGenrePlaylists, nodeMetric]);
2769
+ const relations = (0, import_genre_tree_view4.useGenreNodeRelations)(
2770
+ treeNodes,
2771
+ !overlayPanel && hasSelectedGenreNode ? selectedNodeId : null,
2772
+ resolveStyle
2773
+ );
2774
+ const [selectionHistory, setSelectionHistory] = (0, import_react16.useState)({
2775
+ current: selectedNodeId,
2776
+ previous: null
2777
+ });
2778
+ if (selectionHistory.current !== selectedNodeId) {
2779
+ setSelectionHistory({
2780
+ current: selectedNodeId,
2781
+ previous: selectionHistory.current ?? selectionHistory.previous
2782
+ });
2783
+ }
2784
+ const previousPlaylist = selectionHistory.previous !== null && selectionHistory.previous !== selectedNodeId ? genrePlaylistByNodeId.get(selectionHistory.previous) : void 0;
2785
+ const handlePrevious = previousPlaylist && visibleGenrePlaylists.includes(previousPlaylist) ? () => handleGenreSearchSelect(previousPlaylist) : void 0;
2910
2786
  const { showPopup, hidePopup } = (0, import_popup6.usePopup)();
2911
- const [searchText, setSearchText] = (0, import_react17.useState)("");
2787
+ const [searchText, setSearchText] = (0, import_react16.useState)("");
2912
2788
  const artistSearchResult = (0, import_genre_tree18.useArtistSearch)(artistBaseUrl, artistSearch ? searchText : "");
2913
2789
  const artistGroup = artistSearch ? {
2914
2790
  key: "artists",
2915
2791
  label: "Artists",
2916
2792
  items: artistSearchResult.artists.map((artist) => ({
2917
2793
  key: artist.uuid,
2918
- content: /* @__PURE__ */ (0, import_jsx_runtime41.jsxs)(import_jsx_runtime41.Fragment, { children: [
2919
- /* @__PURE__ */ (0, import_jsx_runtime41.jsxs)("span", { className: "block truncate font-medium", children: [
2794
+ content: /* @__PURE__ */ (0, import_jsx_runtime40.jsxs)(import_jsx_runtime40.Fragment, { children: [
2795
+ /* @__PURE__ */ (0, import_jsx_runtime40.jsxs)("span", { className: "block truncate font-medium", children: [
2920
2796
  artist.name,
2921
- artist.disambiguation && /* @__PURE__ */ (0, import_jsx_runtime41.jsxs)("span", { className: "font-normal text-[var(--color-neutral-500)]", children: [
2797
+ artist.disambiguation && /* @__PURE__ */ (0, import_jsx_runtime40.jsxs)("span", { className: "font-normal text-[var(--color-neutral-500)]", children: [
2922
2798
  " \xB7 ",
2923
2799
  artist.disambiguation
2924
2800
  ] })
2925
2801
  ] }),
2926
- artist.matchedAlias && /* @__PURE__ */ (0, import_jsx_runtime41.jsxs)("span", { className: "block truncate text-xs text-[var(--color-neutral-500)]", children: [
2802
+ artist.matchedAlias && /* @__PURE__ */ (0, import_jsx_runtime40.jsxs)("span", { className: "block truncate text-xs text-[var(--color-neutral-500)]", children: [
2927
2803
  "also known as ",
2928
2804
  artist.matchedAlias
2929
2805
  ] })
@@ -2942,9 +2818,9 @@ function GenreTreeView({
2942
2818
  label: "Songs",
2943
2819
  items: songSearchResult.songs.map((song) => ({
2944
2820
  key: song.uuid,
2945
- content: /* @__PURE__ */ (0, import_jsx_runtime41.jsxs)("span", { className: (0, import_genre_tree18.isSongSearchResultPlayable)(song) ? void 0 : "opacity-60", children: [
2946
- /* @__PURE__ */ (0, import_jsx_runtime41.jsx)("span", { className: "block truncate font-medium", children: song.title }),
2947
- /* @__PURE__ */ (0, import_jsx_runtime41.jsx)("span", { className: "block truncate text-xs text-[var(--color-neutral-500)]", children: [song.artists.map((artist) => artist.name).join(", "), song.genre?.name ?? "No genre"].filter(Boolean).join(" \xB7 ") })
2821
+ content: /* @__PURE__ */ (0, import_jsx_runtime40.jsxs)("span", { className: (0, import_genre_tree18.isSongSearchResultPlayable)(song) ? void 0 : "opacity-60", children: [
2822
+ /* @__PURE__ */ (0, import_jsx_runtime40.jsx)("span", { className: "block truncate font-medium", children: song.title }),
2823
+ /* @__PURE__ */ (0, import_jsx_runtime40.jsx)("span", { className: "block truncate text-xs text-[var(--color-neutral-500)]", children: [song.artists.map((artist) => artist.name).join(", "), song.genre?.name ?? "No genre"].filter(Boolean).join(" \xB7 ") })
2948
2824
  ] }),
2949
2825
  onSelect: () => {
2950
2826
  openSong(song.uuid);
@@ -2970,36 +2846,49 @@ function GenreTreeView({
2970
2846
  const handleGenreSearchZoomIn = () => {
2971
2847
  if (selectedUuid) focus.onFocusGenreChange?.(selectedUuid, "zoom-in");
2972
2848
  };
2973
- const zoomInRef = (0, import_react17.useRef)(void 0);
2849
+ const zoomInRef = (0, import_react16.useRef)(void 0);
2974
2850
  zoomInRef.current = canZoomIn ? handleGenreSearchZoomIn : void 0;
2975
- const renderInfoPanelBody = (0, import_react17.useCallback)(
2976
- (node, { toolbar, ancestors, family }) => {
2977
- const genrePlaylist = genrePlaylistByNodeId.get(node.id);
2978
- const criteria = genrePlaylist?.criteria;
2979
- if (!genrePlaylist || !criteria) return { primary: /* @__PURE__ */ (0, import_jsx_runtime41.jsxs)(import_jsx_runtime41.Fragment, { children: [
2980
- toolbar,
2981
- family
2982
- ] }), aside: ancestors };
2983
- const genreUuid = criteria.uuid;
2984
- const parentPlaylist = visibleGenrePlaylists.find(({ uuid }) => uuid === genrePlaylist.parent?.uuid);
2985
- const canToggleSide = !readOnly && parentPlaylist !== void 0 && parentPlaylist.parent === null;
2986
- const side = criteria.side ?? "core";
2987
- const shareTarget = {
2988
- kind: "genre",
2989
- genrePlaylistUuid: genrePlaylist.uuid,
2990
- name: criteria.name,
2991
- scope,
2992
- artistUuid
2993
- };
2994
- const shareUrl = getShareUrlRef.current?.(shareTarget) ?? void 0;
2995
- const popSibling = canToggleSide && side === "core" ? visibleGenrePlaylists.find(
2996
- (sibling) => sibling.parent?.uuid === parentPlaylist.uuid && sibling.uuid !== genrePlaylist.uuid && sibling.criteria?.side === "pop"
2997
- ) : void 0;
2998
- return {
2999
- primary: /* @__PURE__ */ (0, import_jsx_runtime41.jsx)(
2851
+ const selectGenreFromPanel = (nodeId) => {
2852
+ const genrePlaylist = genrePlaylistByNodeId.get(nodeId);
2853
+ if (genrePlaylist) handleGenreSearchSelect(genrePlaylist);
2854
+ };
2855
+ const renderGenreBody = (relation) => {
2856
+ const { node } = relation;
2857
+ const family = /* @__PURE__ */ (0, import_jsx_runtime40.jsx)(
2858
+ import_genre_tree_view4.GenreNodeFamily,
2859
+ {
2860
+ node,
2861
+ fill: relation.fill,
2862
+ textColor: relation.textColor,
2863
+ parentNode: relation.parentNode,
2864
+ childNodes: relation.childNodes,
2865
+ onSelectNode: selectGenreFromPanel
2866
+ }
2867
+ );
2868
+ const ancestors = /* @__PURE__ */ (0, import_jsx_runtime40.jsx)(import_genre_tree_view4.GenreNodeAncestors, { ancestorNodes: relation.ancestorNodes, onSelectNode: selectGenreFromPanel });
2869
+ const genrePlaylist = genrePlaylistByNodeId.get(node.id);
2870
+ const criteria = genrePlaylist?.criteria;
2871
+ if (!genrePlaylist || !criteria) return { primary: family, aside: ancestors };
2872
+ const genreUuid = criteria.uuid;
2873
+ const parentPlaylist = visibleGenrePlaylists.find(({ uuid }) => uuid === genrePlaylist.parent?.uuid);
2874
+ const canToggleSide = !readOnly && parentPlaylist !== void 0 && parentPlaylist.parent === null;
2875
+ const side = criteria.side ?? "core";
2876
+ const shareTarget = {
2877
+ kind: "genre",
2878
+ genrePlaylistUuid: genrePlaylist.uuid,
2879
+ name: criteria.name,
2880
+ scope,
2881
+ artistUuid
2882
+ };
2883
+ const shareUrl = getShareUrlRef.current?.(shareTarget) ?? void 0;
2884
+ const popSibling = canToggleSide && side === "core" ? visibleGenrePlaylists.find(
2885
+ (sibling) => sibling.parent?.uuid === parentPlaylist.uuid && sibling.uuid !== genrePlaylist.uuid && sibling.criteria?.side === "pop"
2886
+ ) : void 0;
2887
+ return {
2888
+ primary: /* @__PURE__ */ (0, import_jsx_runtime40.jsxs)(import_jsx_runtime40.Fragment, { children: [
2889
+ /* @__PURE__ */ (0, import_jsx_runtime40.jsx)(
3000
2890
  GenreInfoPanelBody,
3001
2891
  {
3002
- toolbar,
3003
2892
  family,
3004
2893
  genreUuid,
3005
2894
  genrePlaylist,
@@ -3015,7 +2904,7 @@ function GenreTreeView({
3015
2904
  onAddChild: readOnly ? void 0 : () => handleGenreCreationAction(criteria),
3016
2905
  onRename: readOnly ? void 0 : () => handleGenreRenameAction(criteria),
3017
2906
  onChangeParent: readOnly ? void 0 : () => showPopup(
3018
- /* @__PURE__ */ (0, import_jsx_runtime41.jsx)(
2907
+ /* @__PURE__ */ (0, import_jsx_runtime40.jsx)(
3019
2908
  ChangeParentPopup,
3020
2909
  {
3021
2910
  genrePlaylist: { ...genrePlaylist, criteria },
@@ -3046,49 +2935,65 @@ function GenreTreeView({
3046
2935
  }
3047
2936
  }
3048
2937
  ),
3049
- aside: ancestors
3050
- };
3051
- },
3052
- [
3053
- genrePlaylistByNodeId,
3054
- additionalActions,
3055
- readOnly,
3056
- handleGenreCreationAction,
3057
- handleGenreRenameAction,
3058
- showPopup,
3059
- hidePopup,
3060
- onParentChanged,
3061
- visibleGenrePlaylists,
3062
- updateGenre,
3063
- playback,
3064
- filterArtist,
3065
- scope,
3066
- getBackendBaseUrl,
3067
- criteriaOverviewSchema,
3068
- renderGenreDetailExtras,
3069
- renderArtistLink,
3070
- resolvedRenderSongLink,
3071
- renderSummary,
3072
- renderEssentialTracks,
3073
- renderSongLeading,
3074
- renderSongActions,
3075
- renderTracksToolbar
3076
- ]
3077
- );
2938
+ renderDangerZone?.(node)
2939
+ ] }),
2940
+ aside: ancestors
2941
+ };
2942
+ };
3078
2943
  const isRotationControlled = controlledAllowWheelRotation !== void 0;
3079
2944
  const allowWheelRotation = controlledAllowWheelRotation ?? internalAllowWheelRotation;
3080
- const detailTab = placement === "regions" && (hasSelectedGenreNode || detailOverlay) ? /* @__PURE__ */ (0, import_jsx_runtime41.jsx)(
3081
- PanelEdgeTab,
2945
+ let activePanel = null;
2946
+ if (overlayPanel) {
2947
+ activePanel = {
2948
+ title: overlayPanel.title,
2949
+ content: overlayPanel.content,
2950
+ aside: overlayPanel.extraContent,
2951
+ expanded: overlayPanel.expanded ?? false,
2952
+ onExpandedChange: overlayPanel.onExpandedChange,
2953
+ onClose: overlayPanel.onClose,
2954
+ compact: false
2955
+ };
2956
+ } else if (relations) {
2957
+ const { primary, aside } = renderGenreBody(relations);
2958
+ activePanel = {
2959
+ title: relations.node.name,
2960
+ content: primary,
2961
+ aside,
2962
+ expanded: isExpanded,
2963
+ onExpandedChange: changeExpanded,
2964
+ onClose: clearSelection,
2965
+ compact: true,
2966
+ headerAccent: { background: relations.fill, color: relations.textColor }
2967
+ };
2968
+ } else if (builtInPanel) {
2969
+ activePanel = {
2970
+ title: builtInPanel.title,
2971
+ content: builtInPanel.content,
2972
+ aside: builtInPanel.extraContent,
2973
+ expanded: builtInPanel.expanded,
2974
+ onExpandedChange: builtInPanel.onExpandedChange,
2975
+ onClose: builtInPanel.onClose,
2976
+ compact: true
2977
+ };
2978
+ }
2979
+ const isPanelShown = isTreeReady && activePanel !== null;
2980
+ const detailTab = placement === "regions" && activePanel?.onExpandedChange ? {
2981
+ edge: activePanel.expanded ? { label: "Shrink panel", onClick: () => activePanel.onExpandedChange?.(false) } : isCollapsed ? null : { label: "Hide panel", onClick: () => changeCollapsed(true) },
2982
+ center: activePanel.expanded ? null : isCollapsed ? { label: "Show panel", onClick: () => changeCollapsed(false) } : activePanel.onExpandedChange ? { label: "Extend panel", onClick: () => activePanel.onExpandedChange?.(true) } : null
2983
+ } : void 0;
2984
+ const expandButton = placement === "default" && activePanel?.onExpandedChange ? /* @__PURE__ */ (0, import_jsx_runtime40.jsx)(
2985
+ import_ui18.IconButton,
3082
2986
  {
3083
- side: "left",
3084
- edge: detailExpanded ? { label: "Shrink panel", onClick: () => onDetailExpandedChange?.(false) } : isCollapsed ? null : { label: "Hide panel", onClick: () => changeCollapsed(true) },
3085
- center: detailExpanded ? null : isCollapsed ? { label: "Show panel", onClick: () => changeCollapsed(false) } : onDetailExpandedChange ? { label: "Extend panel", onClick: () => onDetailExpandedChange(true) } : null
2987
+ "aria-label": activePanel.expanded ? "Shrink panel" : "Extend panel",
2988
+ style: activePanel.headerAccent && { color: "inherit" },
2989
+ onClick: () => activePanel.onExpandedChange?.(!activePanel.expanded),
2990
+ children: activePanel.expanded ? /* @__PURE__ */ (0, import_jsx_runtime40.jsx)(import_lucide_react16.Minimize2, { "aria-hidden": "true", className: "h-4 w-4" }) : /* @__PURE__ */ (0, import_jsx_runtime40.jsx)(import_lucide_react16.Maximize2, { "aria-hidden": "true", className: "h-4 w-4" })
3086
2991
  }
3087
2992
  ) : null;
3088
2993
  const renderArtistFilter = () => {
3089
2994
  if (isLoading || !filterArtist) return null;
3090
- return /* @__PURE__ */ (0, import_jsx_runtime41.jsxs)("span", { className: "inline-flex items-center overflow-hidden rounded-full bg-[var(--color-neutral-100)] text-sm", children: [
3091
- /* @__PURE__ */ (0, import_jsx_runtime41.jsx)(
2995
+ return /* @__PURE__ */ (0, import_jsx_runtime40.jsxs)("span", { className: "inline-flex items-center overflow-hidden rounded-full bg-[var(--color-neutral-100)] text-sm", children: [
2996
+ /* @__PURE__ */ (0, import_jsx_runtime40.jsx)(
3092
2997
  "button",
3093
2998
  {
3094
2999
  type: "button",
@@ -3100,7 +3005,7 @@ function GenreTreeView({
3100
3005
  children: filterArtist.name
3101
3006
  }
3102
3007
  ),
3103
- /* @__PURE__ */ (0, import_jsx_runtime41.jsx)(
3008
+ /* @__PURE__ */ (0, import_jsx_runtime40.jsx)(
3104
3009
  "button",
3105
3010
  {
3106
3011
  type: "button",
@@ -3112,22 +3017,22 @@ function GenreTreeView({
3112
3017
  )
3113
3018
  ] });
3114
3019
  };
3115
- const actions = /* @__PURE__ */ (0, import_jsx_runtime41.jsxs)(import_jsx_runtime41.Fragment, { children: [
3116
- !isLoading && /* @__PURE__ */ (0, import_jsx_runtime41.jsx)(
3020
+ const actions = /* @__PURE__ */ (0, import_jsx_runtime40.jsxs)(import_jsx_runtime40.Fragment, { children: [
3021
+ !isLoading && /* @__PURE__ */ (0, import_jsx_runtime40.jsx)(
3117
3022
  GenreSearch,
3118
3023
  {
3119
3024
  genrePlaylists: visibleGenrePlaylists,
3120
3025
  onSelect: handleGenreSearchSelect,
3121
- selectedName: selectedGenrePlaylist?.name ?? builtInPanel?.title ?? null,
3026
+ selectedName: selectedGenrePlaylist?.name ?? (typeof builtInPanel?.title === "string" ? builtInPanel.title : null),
3122
3027
  onClear: builtInSongPanel ? () => changeSong(null) : builtInArtistPanel ? () => changeArtist(null) : handleGenreSearchClear,
3123
3028
  onPrevious: handlePrevious,
3124
3029
  onQueryChange: artistSearch || songSearch ? setSearchText : void 0,
3125
3030
  extraGroups
3126
3031
  }
3127
3032
  ),
3128
- !isLoading && filterArtist && /* @__PURE__ */ (0, import_jsx_runtime41.jsx)("div", { className: "hidden w-full justify-center max-md:flex", children: renderArtistFilter() }),
3129
- /* @__PURE__ */ (0, import_jsx_runtime41.jsxs)("div", { className: "contents max-md:flex max-md:w-full max-md:items-center max-md:gap-2 max-md:overflow-x-auto max-md:[scrollbar-width:none] max-md:[&>*]:shrink-0", children: [
3130
- !isLoading && !isControlled && /* @__PURE__ */ (0, import_jsx_runtime41.jsx)(
3033
+ !isLoading && filterArtist && /* @__PURE__ */ (0, import_jsx_runtime40.jsx)("div", { className: "hidden w-full justify-center max-md:flex", children: renderArtistFilter() }),
3034
+ /* @__PURE__ */ (0, import_jsx_runtime40.jsxs)("div", { className: "contents max-md:flex max-md:w-full max-md:items-center max-md:gap-2 max-md:overflow-x-auto max-md:[scrollbar-width:none] max-md:[&>*]:shrink-0", children: [
3035
+ !isLoading && !isControlled && /* @__PURE__ */ (0, import_jsx_runtime40.jsx)(
3131
3036
  import_ui18.SegmentedControl,
3132
3037
  {
3133
3038
  "aria-label": "Tree view mode",
@@ -3141,13 +3046,13 @@ function GenreTreeView({
3141
3046
  ]
3142
3047
  }
3143
3048
  ),
3144
- !isLoading && showDisplayOptions && /* @__PURE__ */ (0, import_jsx_runtime41.jsx)(
3049
+ !isLoading && showDisplayOptions && /* @__PURE__ */ (0, import_jsx_runtime40.jsx)(
3145
3050
  "div",
3146
3051
  {
3147
3052
  className: "flex items-center gap-1",
3148
3053
  role: "group",
3149
3054
  "aria-label": "Tree display options",
3150
- children: !isRotationControlled && viewMode !== "stacked" && viewMode !== "outline" && /* @__PURE__ */ (0, import_jsx_runtime41.jsx)(
3055
+ children: !isRotationControlled && viewMode !== "stacked" && viewMode !== "outline" && /* @__PURE__ */ (0, import_jsx_runtime40.jsx)(
3151
3056
  import_ui18.Button,
3152
3057
  {
3153
3058
  variant: allowWheelRotation ? "default" : "outline",
@@ -3158,52 +3063,44 @@ function GenreTreeView({
3158
3063
  )
3159
3064
  }
3160
3065
  ),
3161
- !isLoading && !readOnly && showAddRoot && /* @__PURE__ */ (0, import_jsx_runtime41.jsx)(
3066
+ !isLoading && !readOnly && showAddRoot && /* @__PURE__ */ (0, import_jsx_runtime40.jsx)(
3162
3067
  import_ui18.IconTextButton,
3163
3068
  {
3164
- icon: import_lucide_react16.Plus,
3069
+ icon: import_lucide_react15.Plus,
3165
3070
  text: "Add root",
3166
3071
  onClick: () => handleGenreCreationAction(null)
3167
3072
  }
3168
3073
  )
3169
3074
  ] })
3170
3075
  ] });
3171
- return /* @__PURE__ */ (0, import_jsx_runtime41.jsxs)(
3076
+ return /* @__PURE__ */ (0, import_jsx_runtime40.jsxs)(
3172
3077
  "div",
3173
3078
  {
3174
- ref: rootRef,
3079
+ "data-left-panel": isPanelShown && !isCollapsed ? void 0 : "hidden",
3175
3080
  className: ["genre-tree-view relative flex flex-col h-full", placement === "regions" && REGIONS_CLASS, className2].filter(Boolean).join(" "),
3176
3081
  children: [
3177
- /* @__PURE__ */ (0, import_jsx_runtime41.jsx)("style", { children: PREVIOUS_BUTTON_CSS }),
3178
- /* @__PURE__ */ (0, import_jsx_runtime41.jsx)(
3082
+ /* @__PURE__ */ (0, import_jsx_runtime40.jsx)(
3179
3083
  "div",
3180
3084
  {
3181
3085
  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"}`,
3182
3086
  children: actions
3183
3087
  }
3184
3088
  ),
3185
- !isLoading && filterArtist && /* @__PURE__ */ (0, import_jsx_runtime41.jsx)(
3089
+ !isLoading && filterArtist && /* @__PURE__ */ (0, import_jsx_runtime40.jsx)(
3186
3090
  "div",
3187
3091
  {
3188
3092
  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"}`,
3189
3093
  children: renderArtistFilter()
3190
3094
  }
3191
3095
  ),
3192
- detailTab && /* @__PURE__ */ (0, import_jsx_runtime41.jsx)(
3193
- "div",
3194
- {
3195
- className: `absolute top-1/2 z-30 -translate-y-1/2 max-md:hidden transition-[opacity,visibility] duration-150 ease-out ${isTreeReady ? "opacity-100" : "pointer-events-none invisible opacity-0"} ${detailExpanded ? "right-[calc(var(--layout-center-right)-var(--layout-edge-tab-w))]" : isCollapsed ? "left-0" : "left-[calc(var(--layout-gutter)+var(--layout-left-w))]"}`,
3196
- children: detailTab
3197
- }
3198
- ),
3199
- /* @__PURE__ */ (0, import_jsx_runtime41.jsx)("div", { className: "content-container flex-1 min-h-0 flex flex-row gap-4", children: /* @__PURE__ */ (0, import_jsx_runtime41.jsxs)("div", { className: "tree-view-container relative flex-1 min-w-0 flex flex-col h-full", children: [
3200
- !isLoading && viewMode === "outline" && floatingControlsLeading && /* @__PURE__ */ (0, import_jsx_runtime41.jsx)("div", { className: "absolute bottom-3 right-3 z-10", children: floatingControlsLeading }),
3201
- isLoading ? /* @__PURE__ */ (0, import_jsx_runtime41.jsx)(import_genre_tree_view4.GenreTreeViewSkeleton, { viewMode }) : viewMode === "wheel" ? (
3096
+ /* @__PURE__ */ (0, import_jsx_runtime40.jsx)("div", { className: "content-container flex-1 min-h-0 flex flex-row gap-4", children: /* @__PURE__ */ (0, import_jsx_runtime40.jsxs)("div", { className: "tree-view-container relative flex-1 min-w-0 flex flex-col h-full", children: [
3097
+ !isLoading && viewMode === "outline" && floatingControlsLeading && /* @__PURE__ */ (0, import_jsx_runtime40.jsx)("div", { className: "absolute bottom-3 right-3 z-10", children: floatingControlsLeading }),
3098
+ isLoading ? /* @__PURE__ */ (0, import_jsx_runtime40.jsx)(import_genre_tree_view4.GenreTreeViewSkeleton, { viewMode }) : viewMode === "wheel" ? (
3202
3099
  // The info panel's top edge aligns with the tree's; the search bar floats `--layout-panel-inset` below it,
3203
3100
  // and the header inset (the bar's height plus that inset) extends the header's fill behind the bar,
3204
3101
  // Google Maps style; the 8px bottom padding mirrors the inset above the bar, and the hidden title's height is zeroed.
3205
3102
  // The panel's own Zoom in button is hidden: the search bar carries it.
3206
- /* @__PURE__ */ (0, import_jsx_runtime41.jsx)("div", { className: "tree-container search-overlay relative flex-1 w-full min-h-0 [--gtv-info-panel-header-inset:calc(var(--layout-search-h,2.5rem)+var(--layout-panel-inset,0.5rem))] [&_.gtv-info-panel-header]:pb-2 [&_.gtv-info-panel-title--hidden]:h-0 [&_.gtv-toolbar]:hidden", children: /* @__PURE__ */ (0, import_jsx_runtime41.jsx)(GenreTreeWheelHandoff, { skeleton: /* @__PURE__ */ (0, import_jsx_runtime41.jsx)(import_genre_tree_view4.GenreTreeWheelSkeleton, {}), onRevealedChange: setIsWheelRevealed, children: /* @__PURE__ */ (0, import_jsx_runtime41.jsx)(
3103
+ /* @__PURE__ */ (0, import_jsx_runtime40.jsx)("div", { className: "tree-container search-overlay relative flex-1 w-full min-h-0 ", children: /* @__PURE__ */ (0, import_jsx_runtime40.jsx)(GenreTreeWheelHandoff, { skeleton: /* @__PURE__ */ (0, import_jsx_runtime40.jsx)(import_genre_tree_view4.GenreTreeWheelSkeleton, {}), onRevealedChange: setIsWheelRevealed, children: /* @__PURE__ */ (0, import_jsx_runtime40.jsx)(
3207
3104
  GenrePlaylistTreeWheel,
3208
3105
  {
3209
3106
  scope,
@@ -3212,29 +3109,26 @@ function GenreTreeView({
3212
3109
  handleGenreRenameAction,
3213
3110
  additionalActions,
3214
3111
  onSelectedNodeChange: handleSelectedNodeChange,
3215
- hideInfoPanelClose: true,
3216
- hideInfoPanelTitle: true,
3217
3112
  onNodeHover: handleNodeHover,
3218
- renderInfoPanelBody,
3113
+ ...hostPanel,
3219
3114
  playback,
3220
3115
  selectedNodeId,
3221
3116
  readOnly,
3222
3117
  allowWheelRotation,
3223
3118
  sizeMetric: isArtistFiltered || !canSizeByViews ? "songs" : deferredSizeMetric,
3224
3119
  busy,
3225
- floatingControlsLeading: floatingControlsLeading || canSizeByViews ? /* @__PURE__ */ (0, import_jsx_runtime41.jsxs)("div", { className: "flex items-center gap-2", children: [
3120
+ floatingControlsLeading: floatingControlsLeading || canSizeByViews ? /* @__PURE__ */ (0, import_jsx_runtime40.jsxs)("div", { className: "flex items-center gap-2", children: [
3226
3121
  floatingControlsLeading,
3227
- canSizeByViews && /* @__PURE__ */ (0, import_jsx_runtime41.jsx)(SizeMetricToggle, { value: sizeMetric, onChange: changeSizeMetric })
3122
+ canSizeByViews && /* @__PURE__ */ (0, import_jsx_runtime40.jsx)(SizeMetricToggle, { value: sizeMetric, onChange: changeSizeMetric })
3228
3123
  ] }) : void 0,
3229
3124
  defaultViewport,
3230
3125
  viewportRestore,
3231
- onViewportChange,
3232
- ...infoPanelCuration
3126
+ onViewportChange
3233
3127
  }
3234
3128
  ) }) })
3235
3129
  ) : viewMode === "pop-core" ? (
3236
3130
  // Same as the wheel: the header inset extends its fill behind the floating search bar, Google Maps style.
3237
- /* @__PURE__ */ (0, import_jsx_runtime41.jsx)("div", { className: "tree-container search-overlay relative flex-1 w-full min-h-0 [--gtv-info-panel-header-inset:calc(var(--layout-search-h,2.5rem)+var(--layout-panel-inset,0.5rem))] [&_.gtv-info-panel-header]:pb-2 [&_.gtv-info-panel-title--hidden]:h-0 [&_.gtv-toolbar]:hidden [&_.gtv-info-panel-zoom-in]:hidden", children: /* @__PURE__ */ (0, import_jsx_runtime41.jsx)(GenreTreeWheelHandoff, { skeleton: /* @__PURE__ */ (0, import_jsx_runtime41.jsx)(import_genre_tree_view4.GenreTreeWheelSkeleton, {}), onRevealedChange: setIsWheelRevealed, children: /* @__PURE__ */ (0, import_jsx_runtime41.jsx)(
3131
+ /* @__PURE__ */ (0, import_jsx_runtime40.jsx)("div", { className: "tree-container search-overlay relative flex-1 w-full min-h-0 ", children: /* @__PURE__ */ (0, import_jsx_runtime40.jsx)(GenreTreeWheelHandoff, { skeleton: /* @__PURE__ */ (0, import_jsx_runtime40.jsx)(import_genre_tree_view4.GenreTreeWheelSkeleton, {}), onRevealedChange: setIsWheelRevealed, children: /* @__PURE__ */ (0, import_jsx_runtime40.jsx)(
3238
3132
  GenrePlaylistTreeWheelRadialPopCore,
3239
3133
  {
3240
3134
  scope,
@@ -3243,28 +3137,25 @@ function GenreTreeView({
3243
3137
  handleGenreRenameAction,
3244
3138
  additionalActions,
3245
3139
  onSelectedNodeChange: handleSelectedNodeChange,
3246
- hideInfoPanelClose: true,
3247
- hideInfoPanelTitle: true,
3248
3140
  onNodeHover: handleNodeHover,
3249
- renderInfoPanelBody,
3141
+ ...hostPanel,
3250
3142
  playback,
3251
3143
  selectedNodeId,
3252
3144
  readOnly,
3253
3145
  allowWheelRotation,
3254
3146
  sizeMetric: isArtistFiltered || !canSizeByViews ? "songs" : deferredSizeMetric,
3255
3147
  busy,
3256
- floatingControlsLeading: floatingControlsLeading || canSizeByViews ? /* @__PURE__ */ (0, import_jsx_runtime41.jsxs)("div", { className: "flex items-center gap-2", children: [
3148
+ floatingControlsLeading: floatingControlsLeading || canSizeByViews ? /* @__PURE__ */ (0, import_jsx_runtime40.jsxs)("div", { className: "flex items-center gap-2", children: [
3257
3149
  floatingControlsLeading,
3258
- canSizeByViews && /* @__PURE__ */ (0, import_jsx_runtime41.jsx)(SizeMetricToggle, { value: sizeMetric, onChange: changeSizeMetric })
3150
+ canSizeByViews && /* @__PURE__ */ (0, import_jsx_runtime40.jsx)(SizeMetricToggle, { value: sizeMetric, onChange: changeSizeMetric })
3259
3151
  ] }) : void 0,
3260
3152
  defaultViewport,
3261
3153
  viewportRestore,
3262
3154
  onViewportChange,
3263
- ...infoPanelCuration,
3264
3155
  ...focus
3265
3156
  }
3266
3157
  ) }) })
3267
- ) : viewMode === "outline" ? /* @__PURE__ */ (0, import_jsx_runtime41.jsx)("div", { className: "tree-container flex-1 min-h-0 w-full overflow-y-auto relative [--gtv-info-panel-header-inset:calc(var(--layout-search-h,2.5rem)+var(--layout-panel-inset,0.5rem)+0.75rem)] [&_.gtv-toolbar]:hidden", children: /* @__PURE__ */ (0, import_jsx_runtime41.jsx)(
3158
+ ) : viewMode === "outline" ? /* @__PURE__ */ (0, import_jsx_runtime40.jsx)("div", { className: "tree-container flex-1 min-h-0 w-full overflow-y-auto relative", children: /* @__PURE__ */ (0, import_jsx_runtime40.jsx)(
3268
3159
  GenrePlaylistTreeWheelRadialPopCore,
3269
3160
  {
3270
3161
  outline: true,
@@ -3274,22 +3165,19 @@ function GenreTreeView({
3274
3165
  handleGenreRenameAction,
3275
3166
  additionalActions,
3276
3167
  onSelectedNodeChange: handleSelectedNodeChange,
3277
- hideInfoPanelClose: true,
3278
- hideInfoPanelTitle: true,
3279
3168
  onNodeHover: handleNodeHover,
3280
- renderInfoPanelBody,
3169
+ ...hostPanel,
3281
3170
  playback,
3282
3171
  selectedNodeId,
3283
- readOnly,
3284
- ...infoPanelCuration
3172
+ readOnly
3285
3173
  }
3286
- ) }) : /* @__PURE__ */ (0, import_jsx_runtime41.jsx)("div", { className: "tree-container relative flex flex-col gap-4 w-full pt-14 overflow-x-auto overflow-y-auto text-[var(--color-text)] [&_.gtv-toolbar]:hidden", children: Object.entries(groupedGenrePlaylistsByRoot).map(
3287
- ([uuid, genrePlaylistTreePerRoot], rootIndex) => {
3288
- return /* @__PURE__ */ (0, import_jsx_runtime41.jsx)(
3174
+ ) }) : /* @__PURE__ */ (0, import_jsx_runtime40.jsx)("div", { className: "tree-container relative flex flex-col gap-4 w-full pt-14 overflow-x-auto overflow-y-auto text-[var(--color-text)]", children: Object.entries(groupedGenrePlaylistsByRoot).map(
3175
+ ([uuid, genrePlaylistTreePerRoot]) => {
3176
+ return /* @__PURE__ */ (0, import_jsx_runtime40.jsx)(
3289
3177
  "div",
3290
3178
  {
3291
3179
  className: "tree-per-root-container relative shrink-0 h-[28rem] mt-2 mr-16 p-2 bg-[var(--color-neutral-100)] rounded-lg",
3292
- children: /* @__PURE__ */ (0, import_jsx_runtime41.jsx)("div", { className: "graph-container relative z-10 h-full", children: /* @__PURE__ */ (0, import_jsx_runtime41.jsx)(
3180
+ children: /* @__PURE__ */ (0, import_jsx_runtime40.jsx)("div", { className: "graph-container relative z-10 h-full", children: /* @__PURE__ */ (0, import_jsx_runtime40.jsx)(
3293
3181
  GenrePlaylistTreePerRoot,
3294
3182
  {
3295
3183
  scope,
@@ -3299,26 +3187,45 @@ function GenreTreeView({
3299
3187
  handleGenreRenameAction,
3300
3188
  additionalActions,
3301
3189
  onSelectedNodeChange: handleSelectedNodeChange,
3302
- hideInfoPanelClose: true,
3303
- hideInfoPanelTitle: true,
3304
3190
  onNodeHover: handleNodeHover,
3305
- renderInfoPanelBody,
3191
+ ...hostPanel,
3306
3192
  playback,
3307
3193
  selectedNodeId: uuid === selectedGenrePlaylist?.root.uuid ? selectedNodeId : null,
3308
3194
  readOnly,
3309
3195
  wheelZoom: "modifier",
3310
3196
  defaultViewport: uuid === selectedGenrePlaylist?.root.uuid ? defaultViewport : void 0,
3311
3197
  viewportRestore: uuid === selectedGenrePlaylist?.root.uuid && uuid === restoreRootUuid ? viewportRestore : void 0,
3312
- onViewportChange: uuid === selectedGenrePlaylist?.root.uuid ? onViewportChange : void 0,
3313
- ...infoPanelCuration,
3314
- overlayPanel: rootIndex === 0 ? activeOverlayPanel : void 0
3198
+ onViewportChange: uuid === selectedGenrePlaylist?.root.uuid ? onViewportChange : void 0
3315
3199
  }
3316
3200
  ) })
3317
3201
  },
3318
3202
  uuid
3319
3203
  );
3320
3204
  }
3321
- ) })
3205
+ ) }),
3206
+ /* @__PURE__ */ (0, import_jsx_runtime40.jsx)(
3207
+ import_ui18.DetailPanel,
3208
+ {
3209
+ ref: panelRef,
3210
+ side: "left",
3211
+ title: activePanel?.title ?? "",
3212
+ hideTitle: activePanel?.compact,
3213
+ hideClose: activePanel?.compact,
3214
+ headerAccent: activePanel?.headerAccent,
3215
+ headerActions: expandButton,
3216
+ onClose: activePanel?.onClose ?? noop,
3217
+ collapsed: isCollapsed || !isTreeReady || !activePanel,
3218
+ expanded: activePanel?.expanded,
3219
+ onExpandedChange: activePanel?.onExpandedChange,
3220
+ aside: activePanel?.aside,
3221
+ edgeTab: detailTab,
3222
+ headerClassName: activePanel?.compact ? "pt-[calc(var(--layout-search-h)+var(--layout-panel-inset)+0.5rem)]" : void 0,
3223
+ children: activePanel?.aside && !activePanel.expanded ? /* @__PURE__ */ (0, import_jsx_runtime40.jsxs)(import_jsx_runtime40.Fragment, { children: [
3224
+ activePanel.content,
3225
+ activePanel.aside
3226
+ ] }) : activePanel?.content
3227
+ }
3228
+ )
3322
3229
  ] }) })
3323
3230
  ]
3324
3231
  }
@@ -3326,12 +3233,12 @@ function GenreTreeView({
3326
3233
  }
3327
3234
 
3328
3235
  // src/genre-tree/GenreDetailPanel.tsx
3329
- var import_react18 = require("react");
3236
+ var import_react17 = require("react");
3330
3237
  var import_genre_tree_view5 = require("@behindthemusictree/genre-tree-view");
3331
3238
  var import_ui19 = require("@behindthemusictree/ui");
3239
+ var import_lucide_react17 = require("lucide-react");
3332
3240
  var import_genre_tree20 = require("@behindthemusictree/app-kit/genre-tree");
3333
- var import_jsx_runtime42 = require("react/jsx-runtime");
3334
- var ITEM_COUNT_LABEL = "Songs";
3241
+ var import_jsx_runtime41 = require("react/jsx-runtime");
3335
3242
  var RELATIVE_ITEM_COUNT = 1;
3336
3243
  function toNode(genre, parentId) {
3337
3244
  return {
@@ -3355,11 +3262,12 @@ function GenreDetailPanel({
3355
3262
  collapsed,
3356
3263
  expanded,
3357
3264
  onExpandedChange,
3265
+ edgeTab,
3358
3266
  extras
3359
3267
  }) {
3360
3268
  const { data: detail, isPending, isError } = (0, import_genre_tree20.useFetchGenreDetail)(genreUuid, scope, getBackendBaseUrl);
3361
3269
  const playback = useGenrePlaylistPlayback({ scope, genrePlaylists: [] });
3362
- const view = (0, import_react18.useMemo)(() => {
3270
+ const view = (0, import_react17.useMemo)(() => {
3363
3271
  if (!detail) return null;
3364
3272
  const rootColor = (0, import_genre_tree_view5.getGenreTreeColor)(detail.root.wikidataId);
3365
3273
  const relative = (genre, parentId) => ({
@@ -3367,7 +3275,7 @@ function GenreDetailPanel({
3367
3275
  fill: (0, import_genre_tree_view5.tintSurface)(rootColor),
3368
3276
  textColor: import_genre_tree_view5.TEXT_COLOR
3369
3277
  });
3370
- const ancestors = [...detail.ascendants].sort((a, b) => b.degree - a.degree);
3278
+ const ancestors2 = [...detail.ascendants].sort((a, b) => b.degree - a.degree);
3371
3279
  return {
3372
3280
  node: {
3373
3281
  id: detail.uuid,
@@ -3379,57 +3287,72 @@ function GenreDetailPanel({
3379
3287
  fill: rootColor,
3380
3288
  parentNode: detail.parent ? relative(detail.parent, null) : null,
3381
3289
  childNodes: detail.children.map((child) => relative(child, detail.uuid)),
3382
- ancestorNodes: ancestors.map(
3383
- ({ ascendant }, index) => relative(ascendant, index === 0 ? null : ancestors[index - 1].ascendant.uuid)
3290
+ ancestorNodes: ancestors2.map(
3291
+ ({ ascendant }, index) => relative(ascendant, index === 0 ? null : ancestors2[index - 1].ascendant.uuid)
3384
3292
  )
3385
3293
  };
3386
3294
  }, [detail]);
3387
3295
  if (isError) {
3388
- return /* @__PURE__ */ (0, import_jsx_runtime42.jsx)("div", { role: "alert", className: "p-4 text-sm text-[var(--color-text)]", children: "This genre could not be loaded." });
3296
+ return /* @__PURE__ */ (0, import_jsx_runtime41.jsx)("div", { role: "alert", className: "p-4 text-sm text-[var(--color-text)]", children: "This genre could not be loaded." });
3389
3297
  }
3390
3298
  if (isPending || !detail || !view) {
3391
- return /* @__PURE__ */ (0, import_jsx_runtime42.jsxs)("div", { className: "flex flex-col gap-3 p-4", "aria-busy": "true", "data-testid": "genre-detail-panel-skeleton", children: [
3392
- /* @__PURE__ */ (0, import_jsx_runtime42.jsx)(import_ui19.Skeleton, { className: "h-6 w-2/3" }),
3393
- /* @__PURE__ */ (0, import_jsx_runtime42.jsx)(import_ui19.Skeleton, { className: "h-4 w-full" }),
3394
- /* @__PURE__ */ (0, import_jsx_runtime42.jsx)(import_ui19.Skeleton, { className: "h-4 w-3/4" })
3299
+ return /* @__PURE__ */ (0, import_jsx_runtime41.jsxs)("div", { className: "flex flex-col gap-3 p-4", "aria-busy": "true", "data-testid": "genre-detail-panel-skeleton", children: [
3300
+ /* @__PURE__ */ (0, import_jsx_runtime41.jsx)(import_ui19.Skeleton, { className: "h-6 w-2/3" }),
3301
+ /* @__PURE__ */ (0, import_jsx_runtime41.jsx)(import_ui19.Skeleton, { className: "h-4 w-full" }),
3302
+ /* @__PURE__ */ (0, import_jsx_runtime41.jsx)(import_ui19.Skeleton, { className: "h-4 w-3/4" })
3395
3303
  ] });
3396
3304
  }
3397
- const renderBody = (node, sections) => ({
3398
- primary: /* @__PURE__ */ (0, import_jsx_runtime42.jsx)(
3399
- GenreInfoPanelBody,
3400
- {
3401
- toolbar: sections.toolbar,
3402
- family: sections.family,
3403
- genreUuid: detail.uuid,
3404
- genrePlaylist: { ...detail.criteriaPlaylist, tracksCount: detail.tracksCount },
3405
- wikidataId: detail.wikidataId,
3406
- scope,
3407
- getBackendBaseUrl,
3408
- playback,
3409
- band: { node, onZoomIn, shareUrl, shareTarget: shareUrl ? shareTarget : void 0 },
3410
- extras
3411
- }
3412
- ),
3413
- aside: null
3414
- });
3415
- return /* @__PURE__ */ (0, import_jsx_runtime42.jsx)(
3416
- import_genre_tree_view5.InfoPanel,
3305
+ const selectGenre = (nodeId) => onSelectGenre(nodeId);
3306
+ const family = /* @__PURE__ */ (0, import_jsx_runtime41.jsx)(
3307
+ import_genre_tree_view5.GenreNodeFamily,
3417
3308
  {
3418
3309
  node: view.node,
3419
3310
  fill: view.fill,
3420
3311
  textColor: import_genre_tree_view5.ACCENT_TEXT_COLOR,
3421
3312
  parentNode: view.parentNode,
3422
3313
  childNodes: view.childNodes,
3423
- ancestorNodes: view.ancestorNodes,
3314
+ onSelectNode: selectGenre
3315
+ }
3316
+ );
3317
+ const ancestors = /* @__PURE__ */ (0, import_jsx_runtime41.jsx)(import_genre_tree_view5.GenreNodeAncestors, { ancestorNodes: view.ancestorNodes, onSelectNode: selectGenre });
3318
+ return /* @__PURE__ */ (0, import_jsx_runtime41.jsxs)(
3319
+ import_ui19.DetailPanel,
3320
+ {
3424
3321
  side: "left",
3322
+ title: view.node.name,
3323
+ headerAccent: { background: view.fill, color: import_genre_tree_view5.ACCENT_TEXT_COLOR },
3324
+ headerActions: onExpandedChange ? /* @__PURE__ */ (0, import_jsx_runtime41.jsx)(
3325
+ import_ui19.IconButton,
3326
+ {
3327
+ "aria-label": expanded ? "Shrink panel" : "Extend panel",
3328
+ style: { color: "inherit" },
3329
+ onClick: () => onExpandedChange(!expanded),
3330
+ children: expanded ? /* @__PURE__ */ (0, import_jsx_runtime41.jsx)(import_lucide_react17.Minimize2, { "aria-hidden": "true", className: "h-4 w-4" }) : /* @__PURE__ */ (0, import_jsx_runtime41.jsx)(import_lucide_react17.Maximize2, { "aria-hidden": "true", className: "h-4 w-4" })
3331
+ }
3332
+ ) : void 0,
3425
3333
  onClose,
3426
- onSelectNode: (nodeId) => onSelectGenre(nodeId),
3427
- itemCountLabel: ITEM_COUNT_LABEL,
3428
- renderInfoPanelBody: renderBody,
3334
+ collapsed,
3429
3335
  expanded,
3430
3336
  onExpandedChange,
3431
- collapsed,
3432
- hideExpandButton: onExpandedChange === void 0
3337
+ edgeTab,
3338
+ aside: ancestors,
3339
+ children: [
3340
+ /* @__PURE__ */ (0, import_jsx_runtime41.jsx)(
3341
+ GenreInfoPanelBody,
3342
+ {
3343
+ family,
3344
+ genreUuid: detail.uuid,
3345
+ genrePlaylist: { ...detail.criteriaPlaylist, tracksCount: detail.tracksCount },
3346
+ wikidataId: detail.wikidataId,
3347
+ scope,
3348
+ getBackendBaseUrl,
3349
+ playback,
3350
+ band: { node: view.node, onZoomIn, shareUrl, shareTarget: shareUrl ? shareTarget : void 0 },
3351
+ extras
3352
+ }
3353
+ ),
3354
+ !expanded && ancestors
3355
+ ]
3433
3356
  }
3434
3357
  );
3435
3358
  }
@@ -3438,13 +3361,13 @@ function GenreDetailPanel({
3438
3361
  var import_genre_tree_view6 = require("@behindthemusictree/genre-tree-view");
3439
3362
 
3440
3363
  // src/genre-tree/track-list-sidebar/TrackListSidebar.tsx
3441
- var import_react19 = require("react");
3442
- var import_lucide_react17 = require("lucide-react");
3364
+ var import_react18 = require("react");
3365
+ var import_lucide_react18 = require("lucide-react");
3443
3366
  var import_genre_tree21 = require("@behindthemusictree/app-kit/genre-tree");
3444
3367
  var import_genre_tree22 = require("@behindthemusictree/app-kit/genre-tree");
3445
- var import_track_noun7 = require("@behindthemusictree/app-kit/track-noun");
3446
- var import_jsx_runtime43 = require("react/jsx-runtime");
3447
- var useBrowserLayoutEffect = typeof window === "undefined" ? import_react19.useEffect : import_react19.useLayoutEffect;
3368
+ var import_track_noun4 = require("@behindthemusictree/app-kit/track-noun");
3369
+ var import_jsx_runtime42 = require("react/jsx-runtime");
3370
+ var useBrowserLayoutEffect = typeof window === "undefined" ? import_react18.useEffect : import_react18.useLayoutEffect;
3448
3371
  function TrackListSidebar({
3449
3372
  className: className2 = "",
3450
3373
  renderDuration,
@@ -3457,17 +3380,17 @@ function TrackListSidebar({
3457
3380
  renderSongLink
3458
3381
  }) {
3459
3382
  const { trackList, loadMore, autoAdvanceScrollRequest } = (0, import_genre_tree21.useTrackList)();
3460
- const trackNoun = (0, import_track_noun7.useTrackNoun)();
3461
- const trackListRef = (0, import_react19.useRef)(null);
3462
- const previousOriginRef = (0, import_react19.useRef)(trackList?.origin ?? null);
3463
- const handledAutoAdvanceRequestIdRef = (0, import_react19.useRef)(null);
3464
- const shouldFollowPlaybackRef = (0, import_react19.useRef)(true);
3465
- const programmaticScrollTopRef = (0, import_react19.useRef)(null);
3466
- const userScrollIntentRef = (0, import_react19.useRef)(false);
3467
- const userScrollIntentTimerRef = (0, import_react19.useRef)(null);
3468
- const loadMoreSentinelRef = (0, import_react19.useRef)(null);
3383
+ const trackNoun = (0, import_track_noun4.useTrackNoun)();
3384
+ const trackListRef = (0, import_react18.useRef)(null);
3385
+ const previousOriginRef = (0, import_react18.useRef)(trackList?.origin ?? null);
3386
+ const handledAutoAdvanceRequestIdRef = (0, import_react18.useRef)(null);
3387
+ const shouldFollowPlaybackRef = (0, import_react18.useRef)(true);
3388
+ const programmaticScrollTopRef = (0, import_react18.useRef)(null);
3389
+ const userScrollIntentRef = (0, import_react18.useRef)(false);
3390
+ const userScrollIntentTimerRef = (0, import_react18.useRef)(null);
3391
+ const loadMoreSentinelRef = (0, import_react18.useRef)(null);
3469
3392
  const hasMore = !!trackList && trackList.nextPage !== null;
3470
- (0, import_react19.useEffect)(() => {
3393
+ (0, import_react18.useEffect)(() => {
3471
3394
  const sentinel = loadMoreSentinelRef.current;
3472
3395
  if (!hasMore || !sentinel) return;
3473
3396
  const observer = new IntersectionObserver((entries) => {
@@ -3487,11 +3410,11 @@ function TrackListSidebar({
3487
3410
  programmaticScrollTopRef.current = null;
3488
3411
  if (trackListRef.current) trackListRef.current.scrollTop = 0;
3489
3412
  }, [trackList?.origin]);
3490
- (0, import_react19.useEffect)(() => {
3413
+ (0, import_react18.useEffect)(() => {
3491
3414
  const list = trackListRef.current;
3492
3415
  if (!isCollapsed && list?.scrollTop === 0) shouldFollowPlaybackRef.current = true;
3493
3416
  }, [isCollapsed]);
3494
- (0, import_react19.useEffect)(
3417
+ (0, import_react18.useEffect)(
3495
3418
  () => () => {
3496
3419
  if (userScrollIntentTimerRef.current !== null) clearTimeout(userScrollIntentTimerRef.current);
3497
3420
  },
@@ -3545,25 +3468,25 @@ function TrackListSidebar({
3545
3468
  }
3546
3469
  };
3547
3470
  const positionClasses = layout === "inline" ? "relative w-full flex h-full flex-col" : "fixed right-0 w-[50rem] flex flex-col";
3548
- return trackList ? /* @__PURE__ */ (0, import_jsx_runtime43.jsxs)(
3471
+ return trackList ? /* @__PURE__ */ (0, import_jsx_runtime42.jsxs)(
3549
3472
  "div",
3550
3473
  {
3551
3474
  "data-theme": "dark",
3552
3475
  className: `track-list-sidebar ${positionClasses} rounded-2xl bg-[var(--color-surface)] pb-1 ${className2}`,
3553
3476
  style: layout === "fixed" ? { top: "63.5px", maxHeight: "calc(100vh - 63.5px - 79px)" } : void 0,
3554
3477
  children: [
3555
- /* @__PURE__ */ (0, import_jsx_runtime43.jsxs)("div", { className: "header flex h-16 items-center px-4 py-2 text-[var(--color-text)]", children: [
3556
- /* @__PURE__ */ (0, import_jsx_runtime43.jsxs)("div", { className: "min-w-0 flex flex-1 flex-col justify-center", children: [
3557
- /* @__PURE__ */ (0, import_jsx_runtime43.jsxs)("div", { className: "origin flex text-xl", children: [
3558
- /* @__PURE__ */ (0, import_jsx_runtime43.jsx)("div", { className: "from h-auto flex flex-col justify-center items-center mr-2", children: "From" }),
3559
- /* @__PURE__ */ (0, import_jsx_runtime43.jsx)("div", { className: "name-container flex flex-col justify-center items-center text-[var(--color-text)] font-bold pr-2 max-w-[27.5rem]", children: /* @__PURE__ */ (0, import_jsx_runtime43.jsx)("div", { className: "name max-w-full truncate", children: trackList.origin.label }) })
3478
+ /* @__PURE__ */ (0, import_jsx_runtime42.jsxs)("div", { className: "header flex h-16 items-center px-4 py-2 text-[var(--color-text)]", children: [
3479
+ /* @__PURE__ */ (0, import_jsx_runtime42.jsxs)("div", { className: "min-w-0 flex flex-1 flex-col justify-center", children: [
3480
+ /* @__PURE__ */ (0, import_jsx_runtime42.jsxs)("div", { className: "origin flex text-xl", children: [
3481
+ /* @__PURE__ */ (0, import_jsx_runtime42.jsx)("div", { className: "from h-auto flex flex-col justify-center items-center mr-2", children: "From" }),
3482
+ /* @__PURE__ */ (0, import_jsx_runtime42.jsx)("div", { className: "name-container flex flex-col justify-center items-center text-[var(--color-text)] font-bold pr-2 max-w-[27.5rem]", children: /* @__PURE__ */ (0, import_jsx_runtime42.jsx)("div", { className: "name max-w-full truncate", children: trackList.origin.label }) })
3560
3483
  ] }),
3561
- /* @__PURE__ */ (0, import_jsx_runtime43.jsxs)("div", { className: "info flex items-center text-sm text-[var(--color-text-muted)]", children: [
3484
+ /* @__PURE__ */ (0, import_jsx_runtime42.jsxs)("div", { className: "info flex items-center text-sm text-[var(--color-text-muted)]", children: [
3562
3485
  trackList.origin.type === import_genre_tree22.TrackListOriginType.GENRE_PLAYLIST ? "Genre \u2022 " : trackList.origin.type === import_genre_tree22.TrackListOriginType.ARTIST ? "Artist \u2022 " : `${trackNoun.singular} playlist \u2022 `,
3563
3486
  `${trackList.total} ${trackList.total > 1 ? trackNoun.plural : trackNoun.singular}`
3564
3487
  ] })
3565
3488
  ] }),
3566
- (isCollapsed && onExpand || !isCollapsed && onMinimize) && /* @__PURE__ */ (0, import_jsx_runtime43.jsx)(
3489
+ (isCollapsed && onExpand || !isCollapsed && onMinimize) && /* @__PURE__ */ (0, import_jsx_runtime42.jsx)(
3567
3490
  "button",
3568
3491
  {
3569
3492
  type: "button",
@@ -3572,11 +3495,11 @@ function TrackListSidebar({
3572
3495
  "aria-expanded": !isCollapsed,
3573
3496
  className: `ml-2 flex h-8 w-8 shrink-0 items-center justify-center rounded-md hover:bg-[var(--color-surface-muted)] ${isCollapsed ? "text-[var(--color-text-muted)]" : "text-white"}`,
3574
3497
  onClick: isCollapsed ? onExpand : onMinimize,
3575
- children: /* @__PURE__ */ (0, import_jsx_runtime43.jsx)(import_lucide_react17.ListMusic, { "aria-hidden": "true", className: "h-5 w-5" })
3498
+ children: /* @__PURE__ */ (0, import_jsx_runtime42.jsx)(import_lucide_react18.ListMusic, { "aria-hidden": "true", className: "h-5 w-5" })
3576
3499
  }
3577
3500
  )
3578
3501
  ] }),
3579
- !isCollapsed && /* @__PURE__ */ (0, import_jsx_runtime43.jsxs)(
3502
+ !isCollapsed && /* @__PURE__ */ (0, import_jsx_runtime42.jsxs)(
3580
3503
  "ul",
3581
3504
  {
3582
3505
  ref: trackListRef,
@@ -3586,7 +3509,7 @@ function TrackListSidebar({
3586
3509
  onPointerDown: markUserScrollIntent,
3587
3510
  className: layout === "inline" ? "track-list flex-1 min-h-0 overflow-auto list-none p-0 m-0" : "track-list flex-1 min-h-0 overflow-auto list-none p-0 m-0",
3588
3511
  children: [
3589
- trackList.tracks.map((track, index) => /* @__PURE__ */ (0, import_jsx_runtime43.jsx)("li", { "data-track-uuid": track.uuid, children: /* @__PURE__ */ (0, import_jsx_runtime43.jsx)(
3512
+ trackList.tracks.map((track, index) => /* @__PURE__ */ (0, import_jsx_runtime42.jsx)("li", { "data-track-uuid": track.uuid, children: /* @__PURE__ */ (0, import_jsx_runtime42.jsx)(
3590
3513
  TrackItem,
3591
3514
  {
3592
3515
  track,
@@ -3597,7 +3520,7 @@ function TrackListSidebar({
3597
3520
  renderSongLink
3598
3521
  }
3599
3522
  ) }, track.uuid)),
3600
- hasMore && /* @__PURE__ */ (0, import_jsx_runtime43.jsx)("li", { ref: loadMoreSentinelRef, "aria-hidden": "true", className: "h-px" })
3523
+ hasMore && /* @__PURE__ */ (0, import_jsx_runtime42.jsx)("li", { ref: loadMoreSentinelRef, "aria-hidden": "true", className: "h-px" })
3601
3524
  ]
3602
3525
  }
3603
3526
  )
@@ -3607,16 +3530,17 @@ function TrackListSidebar({
3607
3530
  }
3608
3531
 
3609
3532
  // src/genre-tree/PlayerPanel.tsx
3610
- var import_react20 = require("react");
3611
- var import_tailwind_merge2 = require("tailwind-merge");
3533
+ var import_react19 = require("react");
3534
+ var import_tailwind_merge = require("tailwind-merge");
3612
3535
  var import_player13 = require("@behindthemusictree/app-kit/player");
3613
3536
  var import_genre_tree24 = require("@behindthemusictree/app-kit/genre-tree");
3537
+ var import_ui20 = require("@behindthemusictree/ui");
3614
3538
 
3615
3539
  // src/genre-tree/PlayerControls.tsx
3616
- var import_lucide_react18 = require("lucide-react");
3540
+ var import_lucide_react19 = require("lucide-react");
3617
3541
  var import_genre_tree23 = require("@behindthemusictree/app-kit/genre-tree");
3618
3542
  var import_player12 = require("@behindthemusictree/app-kit/player");
3619
- var import_jsx_runtime44 = require("react/jsx-runtime");
3543
+ var import_jsx_runtime43 = require("react/jsx-runtime");
3620
3544
  function PlayerControls() {
3621
3545
  const {
3622
3546
  duration,
@@ -3635,15 +3559,15 @@ function PlayerControls() {
3635
3559
  const safeDuration = Number.isFinite(duration) && duration > 0 ? duration : 0;
3636
3560
  const safeCurrentTime = Number.isFinite(currentTime) ? Math.max(0, Math.min(currentTime, safeDuration)) : 0;
3637
3561
  const isPlaying = playState === import_player12.PlayStates.PLAYING;
3638
- return /* @__PURE__ */ (0, import_jsx_runtime44.jsxs)(
3562
+ return /* @__PURE__ */ (0, import_jsx_runtime43.jsxs)(
3639
3563
  "section",
3640
3564
  {
3641
3565
  "aria-label": "Player controls",
3642
3566
  className: "player-controls flex shrink-0 flex-col gap-2 border-b border-zinc-800 px-3 py-2 text-zinc-200",
3643
3567
  children: [
3644
- /* @__PURE__ */ (0, import_jsx_runtime44.jsxs)("div", { className: "flex min-w-0 items-center gap-2 text-xs tabular-nums text-zinc-400", children: [
3645
- /* @__PURE__ */ (0, import_jsx_runtime44.jsx)("span", { "aria-label": "Elapsed time", children: (0, import_genre_tree23.formatTime)(safeCurrentTime) }),
3646
- /* @__PURE__ */ (0, import_jsx_runtime44.jsx)(
3568
+ /* @__PURE__ */ (0, import_jsx_runtime43.jsxs)("div", { className: "flex min-w-0 items-center gap-2 text-xs tabular-nums text-zinc-400", children: [
3569
+ /* @__PURE__ */ (0, import_jsx_runtime43.jsx)("span", { "aria-label": "Elapsed time", children: (0, import_genre_tree23.formatTime)(safeCurrentTime) }),
3570
+ /* @__PURE__ */ (0, import_jsx_runtime43.jsx)(
3647
3571
  "input",
3648
3572
  {
3649
3573
  "aria-label": "Seek",
@@ -3657,11 +3581,11 @@ function PlayerControls() {
3657
3581
  onChange: (event) => seekTo(Number(event.currentTarget.value))
3658
3582
  }
3659
3583
  ),
3660
- /* @__PURE__ */ (0, import_jsx_runtime44.jsx)("span", { "aria-label": "Duration", children: (0, import_genre_tree23.formatTime)(safeDuration) })
3584
+ /* @__PURE__ */ (0, import_jsx_runtime43.jsx)("span", { "aria-label": "Duration", children: (0, import_genre_tree23.formatTime)(safeDuration) })
3661
3585
  ] }),
3662
- /* @__PURE__ */ (0, import_jsx_runtime44.jsxs)("div", { className: "grid min-h-9 grid-cols-[1fr_auto_1fr] items-center gap-3", children: [
3663
- /* @__PURE__ */ (0, import_jsx_runtime44.jsxs)("div", { className: "col-start-2 flex items-center justify-center gap-2", children: [
3664
- /* @__PURE__ */ (0, import_jsx_runtime44.jsx)(
3586
+ /* @__PURE__ */ (0, import_jsx_runtime43.jsxs)("div", { className: "grid min-h-9 grid-cols-[1fr_auto_1fr] items-center gap-3", children: [
3587
+ /* @__PURE__ */ (0, import_jsx_runtime43.jsxs)("div", { className: "col-start-2 flex items-center justify-center gap-2", children: [
3588
+ /* @__PURE__ */ (0, import_jsx_runtime43.jsx)(
3665
3589
  "button",
3666
3590
  {
3667
3591
  type: "button",
@@ -3670,10 +3594,10 @@ function PlayerControls() {
3670
3594
  disabled: !isReady || !hasPreviousTrack,
3671
3595
  onClick: playPreviousTrack,
3672
3596
  className: "flex h-8 w-8 shrink-0 items-center justify-center rounded-full text-zinc-300 hover:bg-zinc-800 hover:text-white disabled:cursor-not-allowed disabled:opacity-40",
3673
- children: /* @__PURE__ */ (0, import_jsx_runtime44.jsx)(import_lucide_react18.SkipBack, { "aria-hidden": "true", className: "h-4 w-4" })
3597
+ children: /* @__PURE__ */ (0, import_jsx_runtime43.jsx)(import_lucide_react19.SkipBack, { "aria-hidden": "true", className: "h-4 w-4" })
3674
3598
  }
3675
3599
  ),
3676
- /* @__PURE__ */ (0, import_jsx_runtime44.jsx)(
3600
+ /* @__PURE__ */ (0, import_jsx_runtime43.jsx)(
3677
3601
  "button",
3678
3602
  {
3679
3603
  type: "button",
@@ -3682,10 +3606,10 @@ function PlayerControls() {
3682
3606
  disabled: !isReady,
3683
3607
  onClick: handlePlayPauseAction,
3684
3608
  className: "flex h-9 w-9 shrink-0 items-center justify-center rounded-full bg-white text-zinc-950 hover:bg-zinc-200 disabled:cursor-not-allowed disabled:opacity-40",
3685
- children: isPlaying ? /* @__PURE__ */ (0, import_jsx_runtime44.jsx)(import_lucide_react18.Pause, { "aria-hidden": "true", className: "h-4 w-4", fill: "currentColor" }) : /* @__PURE__ */ (0, import_jsx_runtime44.jsx)(import_lucide_react18.Play, { "aria-hidden": "true", className: "h-4 w-4", fill: "currentColor" })
3609
+ children: isPlaying ? /* @__PURE__ */ (0, import_jsx_runtime43.jsx)(import_lucide_react19.Pause, { "aria-hidden": "true", className: "h-4 w-4", fill: "currentColor" }) : /* @__PURE__ */ (0, import_jsx_runtime43.jsx)(import_lucide_react19.Play, { "aria-hidden": "true", className: "h-4 w-4", fill: "currentColor" })
3686
3610
  }
3687
3611
  ),
3688
- /* @__PURE__ */ (0, import_jsx_runtime44.jsx)(
3612
+ /* @__PURE__ */ (0, import_jsx_runtime43.jsx)(
3689
3613
  "button",
3690
3614
  {
3691
3615
  type: "button",
@@ -3694,14 +3618,14 @@ function PlayerControls() {
3694
3618
  disabled: !isReady || !hasNextTrack,
3695
3619
  onClick: playNextTrack,
3696
3620
  className: "flex h-8 w-8 shrink-0 items-center justify-center rounded-full text-zinc-300 hover:bg-zinc-800 hover:text-white disabled:cursor-not-allowed disabled:opacity-40",
3697
- children: /* @__PURE__ */ (0, import_jsx_runtime44.jsx)(import_lucide_react18.SkipForward, { "aria-hidden": "true", className: "h-4 w-4" })
3621
+ children: /* @__PURE__ */ (0, import_jsx_runtime43.jsx)(import_lucide_react19.SkipForward, { "aria-hidden": "true", className: "h-4 w-4" })
3698
3622
  }
3699
3623
  )
3700
3624
  ] }),
3701
- /* @__PURE__ */ (0, import_jsx_runtime44.jsxs)("label", { className: "col-start-3 flex w-[4.2rem] shrink-0 items-center gap-[0.35rem] justify-self-end text-zinc-400 sm:w-[4.9rem]", children: [
3702
- volume === 0 ? /* @__PURE__ */ (0, import_jsx_runtime44.jsx)(import_lucide_react18.VolumeX, { "aria-hidden": "true", className: "h-[0.7rem] w-[0.7rem] shrink-0" }) : /* @__PURE__ */ (0, import_jsx_runtime44.jsx)(import_lucide_react18.Volume2, { "aria-hidden": "true", className: "h-[0.7rem] w-[0.7rem] shrink-0" }),
3703
- /* @__PURE__ */ (0, import_jsx_runtime44.jsx)("span", { className: "sr-only", children: "Volume" }),
3704
- /* @__PURE__ */ (0, import_jsx_runtime44.jsx)(
3625
+ /* @__PURE__ */ (0, import_jsx_runtime43.jsxs)("label", { className: "col-start-3 flex w-[4.2rem] shrink-0 items-center gap-[0.35rem] justify-self-end text-zinc-400 sm:w-[4.9rem]", children: [
3626
+ volume === 0 ? /* @__PURE__ */ (0, import_jsx_runtime43.jsx)(import_lucide_react19.VolumeX, { "aria-hidden": "true", className: "h-[0.7rem] w-[0.7rem] shrink-0" }) : /* @__PURE__ */ (0, import_jsx_runtime43.jsx)(import_lucide_react19.Volume2, { "aria-hidden": "true", className: "h-[0.7rem] w-[0.7rem] shrink-0" }),
3627
+ /* @__PURE__ */ (0, import_jsx_runtime43.jsx)("span", { className: "sr-only", children: "Volume" }),
3628
+ /* @__PURE__ */ (0, import_jsx_runtime43.jsx)(
3705
3629
  "input",
3706
3630
  {
3707
3631
  "aria-label": "Volume",
@@ -3723,7 +3647,7 @@ function PlayerControls() {
3723
3647
  }
3724
3648
 
3725
3649
  // src/genre-tree/PlayerPanel.tsx
3726
- var import_jsx_runtime45 = require("react/jsx-runtime");
3650
+ var import_jsx_runtime44 = require("react/jsx-runtime");
3727
3651
  var PANEL_ID = "player-panel";
3728
3652
  var DEFAULT_EXTENDED_WIDTH = "calc(100vw - var(--layout-center-left) - var(--layout-gutter))";
3729
3653
  function PlayerPanel({
@@ -3737,12 +3661,12 @@ function PlayerPanel({
3737
3661
  }) {
3738
3662
  const { playerTrackObject } = (0, import_player13.usePlayer)();
3739
3663
  const { toggleTrackListSidebar, isTrackListSidebarVisible } = (0, import_genre_tree24.useTrackListSidebarVisibility)();
3740
- const [lastLoadedTrackTitle, setLastLoadedTrackTitle] = (0, import_react20.useState)("");
3664
+ const [lastLoadedTrackTitle, setLastLoadedTrackTitle] = (0, import_react19.useState)("");
3741
3665
  const loadedTrackTitle = playerTrackObject?.track.title;
3742
- (0, import_react20.useEffect)(() => {
3666
+ (0, import_react19.useEffect)(() => {
3743
3667
  if (loadedTrackTitle) setLastLoadedTrackTitle(loadedTrackTitle);
3744
3668
  }, [loadedTrackTitle]);
3745
- const [uncontrolledExtended, setUncontrolledExtended] = (0, import_react20.useState)(false);
3669
+ const [uncontrolledExtended, setUncontrolledExtended] = (0, import_react19.useState)(false);
3746
3670
  const isExtendRequested = expanded ?? uncontrolledExtended;
3747
3671
  const setIsExtendRequested = (next) => {
3748
3672
  setUncontrolledExtended(next);
@@ -3763,11 +3687,11 @@ function PlayerPanel({
3763
3687
  onClick: () => setIsExtendRequested(true),
3764
3688
  expanded: false
3765
3689
  } : null;
3766
- return /* @__PURE__ */ (0, import_jsx_runtime45.jsxs)(
3690
+ return /* @__PURE__ */ (0, import_jsx_runtime44.jsxs)(
3767
3691
  "div",
3768
3692
  {
3769
3693
  id: PANEL_ID,
3770
- className: (0, import_tailwind_merge2.twMerge)(
3694
+ className: (0, import_tailwind_merge.twMerge)(
3771
3695
  "fixed right-[var(--layout-gutter)] top-[var(--layout-band-top)] z-40 max-h-[calc(100vh-var(--layout-band-top)-3.5rem)]",
3772
3696
  !isExtended && "w-[var(--layout-right-w)]",
3773
3697
  isTrackListSidebarVisible && "h-[calc(100vh-var(--layout-band-top)-3.5rem)]",
@@ -3776,8 +3700,8 @@ function PlayerPanel({
3776
3700
  ),
3777
3701
  style: isExtended ? { width: extendedWidth } : void 0,
3778
3702
  children: [
3779
- /* @__PURE__ */ (0, import_jsx_runtime45.jsx)(
3780
- PanelEdgeTab,
3703
+ /* @__PURE__ */ (0, import_jsx_runtime44.jsx)(
3704
+ import_ui20.PanelEdgeTab,
3781
3705
  {
3782
3706
  side: "right",
3783
3707
  edge: tabEdge,
@@ -3786,30 +3710,30 @@ function PlayerPanel({
3786
3710
  className: "absolute right-full top-1/2 -translate-y-1/2"
3787
3711
  }
3788
3712
  ),
3789
- /* @__PURE__ */ (0, import_jsx_runtime45.jsxs)(
3713
+ /* @__PURE__ */ (0, import_jsx_runtime44.jsxs)(
3790
3714
  "div",
3791
3715
  {
3792
- className: (0, import_tailwind_merge2.twMerge)(
3716
+ className: (0, import_tailwind_merge.twMerge)(
3793
3717
  "flex h-full max-h-[inherit] overflow-hidden rounded-[var(--radius-panel,1rem)] bg-zinc-950 text-zinc-100 shadow-[0_1px_2px_rgba(0,0,0,0.5),0_2px_6px_2px_rgba(0,0,0,0.3)]",
3794
3718
  isExtended ? "flex-row" : "flex-col"
3795
3719
  ),
3796
3720
  children: [
3797
- /* @__PURE__ */ (0, import_jsx_runtime45.jsxs)("div", { className: "flex w-[var(--layout-right-w)] shrink-0 flex-col", children: [
3798
- /* @__PURE__ */ (0, import_jsx_runtime45.jsx)("div", { className: "flex shrink-0 items-center justify-between gap-2 border-b border-zinc-800 p-2", children: /* @__PURE__ */ (0, import_jsx_runtime45.jsx)("span", { className: "min-w-0 truncate text-sm font-semibold", children: loadedTrackTitle || lastLoadedTrackTitle }) }),
3799
- /* @__PURE__ */ (0, import_jsx_runtime45.jsxs)("div", { className: "relative w-full shrink-0", children: [
3800
- /* @__PURE__ */ (0, import_jsx_runtime45.jsx)(import_player13.PlayerVideoSurface, { className: "aspect-video w-full bg-black" }),
3801
- playerTrackObject?.loadError && /* @__PURE__ */ (0, import_jsx_runtime45.jsx)("span", { className: "absolute bottom-2 left-2 right-2 text-xs text-red-400", children: playerTrackObject.loadError })
3721
+ /* @__PURE__ */ (0, import_jsx_runtime44.jsxs)("div", { className: "flex w-[var(--layout-right-w)] shrink-0 flex-col", children: [
3722
+ /* @__PURE__ */ (0, import_jsx_runtime44.jsx)("div", { className: "flex shrink-0 items-center justify-between gap-2 border-b border-zinc-800 p-2", children: /* @__PURE__ */ (0, import_jsx_runtime44.jsx)("span", { className: "min-w-0 truncate text-sm font-semibold", children: loadedTrackTitle || lastLoadedTrackTitle }) }),
3723
+ /* @__PURE__ */ (0, import_jsx_runtime44.jsxs)("div", { className: "relative w-full shrink-0", children: [
3724
+ /* @__PURE__ */ (0, import_jsx_runtime44.jsx)(import_player13.PlayerVideoSurface, { className: "aspect-video w-full bg-black" }),
3725
+ playerTrackObject?.loadError && /* @__PURE__ */ (0, import_jsx_runtime44.jsx)("span", { className: "absolute bottom-2 left-2 right-2 text-xs text-red-400", children: playerTrackObject.loadError })
3802
3726
  ] }),
3803
- /* @__PURE__ */ (0, import_jsx_runtime45.jsx)(PlayerControls, {})
3727
+ /* @__PURE__ */ (0, import_jsx_runtime44.jsx)(PlayerControls, {})
3804
3728
  ] }),
3805
- /* @__PURE__ */ (0, import_jsx_runtime45.jsx)(
3729
+ /* @__PURE__ */ (0, import_jsx_runtime44.jsx)(
3806
3730
  TrackListSidebar,
3807
3731
  {
3808
3732
  layout: "inline",
3809
3733
  isCollapsed: !isTrackListSidebarVisible,
3810
3734
  onExpand: toggleTrackListSidebar,
3811
3735
  onMinimize: hideSongList,
3812
- className: (0, import_tailwind_merge2.twMerge)(
3736
+ className: (0, import_tailwind_merge.twMerge)(
3813
3737
  "min-h-0 min-w-0 flex-1",
3814
3738
  isExtended && "border-l border-zinc-800"
3815
3739
  ),
@@ -3829,11 +3753,11 @@ function PlayerPanel({
3829
3753
  var import_uuid = require("uuid");
3830
3754
  var import_ai = require("react-icons/ai");
3831
3755
  var import_genre_tree25 = require("@behindthemusictree/app-kit/genre-tree");
3832
- var import_jsx_runtime46 = require("react/jsx-runtime");
3756
+ var import_jsx_runtime45 = require("react/jsx-runtime");
3833
3757
  function Rating({ rating, handleChange }) {
3834
3758
  const instanceId = (0, import_uuid.v4)();
3835
- return /* @__PURE__ */ (0, import_jsx_runtime46.jsx)("div", { className: "flex items-center", children: [...Array(6)].map((_, i) => /* @__PURE__ */ (0, import_jsx_runtime46.jsxs)("div", { className: `relative ${i === 0 ? "mr-4" : "mr-1"}`, children: [
3836
- /* @__PURE__ */ (0, import_jsx_runtime46.jsx)(
3759
+ return /* @__PURE__ */ (0, import_jsx_runtime45.jsx)("div", { className: "flex items-center", children: [...Array(6)].map((_, i) => /* @__PURE__ */ (0, import_jsx_runtime45.jsxs)("div", { className: `relative ${i === 0 ? "mr-4" : "mr-1"}`, children: [
3760
+ /* @__PURE__ */ (0, import_jsx_runtime45.jsx)(
3837
3761
  "input",
3838
3762
  {
3839
3763
  id: `star-${instanceId}-${i}`,
@@ -3845,16 +3769,16 @@ function Rating({ rating, handleChange }) {
3845
3769
  onChange: handleChange
3846
3770
  }
3847
3771
  ),
3848
- /* @__PURE__ */ (0, import_jsx_runtime46.jsx)("label", { htmlFor: `star-${instanceId}-${i}`, className: "cursor-pointer", children: i === 0 ? /* @__PURE__ */ (0, import_jsx_runtime46.jsxs)("div", { className: "flex", children: [
3849
- /* @__PURE__ */ (0, import_jsx_runtime46.jsx)(
3772
+ /* @__PURE__ */ (0, import_jsx_runtime45.jsx)("label", { htmlFor: `star-${instanceId}-${i}`, className: "cursor-pointer", children: i === 0 ? /* @__PURE__ */ (0, import_jsx_runtime45.jsxs)("div", { className: "flex", children: [
3773
+ /* @__PURE__ */ (0, import_jsx_runtime45.jsx)(
3850
3774
  import_ai.AiOutlineStar,
3851
3775
  {
3852
3776
  className: `w-7 h-7 absolute top-0 left-0 ${rating === import_genre_tree25.FORM_RATING_NULL_VALUE ? "text-[var(--color-accent-rating)]" : "text-[var(--color-text-muted)]"}`
3853
3777
  }
3854
3778
  ),
3855
- /* @__PURE__ */ (0, import_jsx_runtime46.jsx)("span", { className: "flex-grow w-8" }),
3779
+ /* @__PURE__ */ (0, import_jsx_runtime45.jsx)("span", { className: "flex-grow w-8" }),
3856
3780
  " "
3857
- ] }) : /* @__PURE__ */ (0, import_jsx_runtime46.jsx)(
3781
+ ] }) : /* @__PURE__ */ (0, import_jsx_runtime45.jsx)(
3858
3782
  import_ai.AiFillStar,
3859
3783
  {
3860
3784
  className: `w-7 h-7 absolute top-0 left-0 ${(rating ?? -1) >= i ? "text-[var(--color-accent-rating)]" : "text-[var(--color-text-muted)]"}`
@@ -3880,7 +3804,6 @@ function Rating({ rating, handleChange }) {
3880
3804
  GenreTreeWheelSkeleton,
3881
3805
  InternalErrorPopup,
3882
3806
  NetworkErrorPopup,
3883
- PanelEdgeTab,
3884
3807
  PlayerPanel,
3885
3808
  PopupButtons,
3886
3809
  PopupTitle,
@@ -3891,7 +3814,6 @@ function Rating({ rating, handleChange }) {
3891
3814
  TrackListSidebar,
3892
3815
  TrackPositionPlayPause,
3893
3816
  TrackUploadPopup,
3894
- useGenrePlaylistPlayback,
3895
- useLeftDetailPanelLayout
3817
+ useGenrePlaylistPlayback
3896
3818
  });
3897
3819
  //# sourceMappingURL=index.cjs.map