@behindthemusictree/app-kit-ui 29.1.0 → 31.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,15 +578,42 @@ function AuthCallbackHandler({ renderStatus, ...options }) {
580
578
  }
581
579
 
582
580
  // src/genre-tree/GenreTreeView.tsx
583
- var import_react15 = require("react");
581
+ var import_react16 = require("react");
584
582
  var import_react_query3 = require("@tanstack/react-query");
585
583
  var import_lucide_react15 = require("lucide-react");
586
- var import_ui16 = require("@behindthemusictree/ui");
584
+ var import_lucide_react16 = require("lucide-react");
585
+ var import_ui18 = require("@behindthemusictree/ui");
587
586
  var import_genre_tree_view4 = require("@behindthemusictree/genre-tree-view");
588
- var import_genre_tree16 = require("@behindthemusictree/app-kit/genre-tree");
589
- var import_genre_tree17 = require("@behindthemusictree/app-kit/genre-tree");
587
+ var import_genre_tree18 = require("@behindthemusictree/app-kit/genre-tree");
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,
@@ -1058,6 +1017,7 @@ function GenreTreeWheelHandoff({ skeleton, children, onRevealedChange }) {
1058
1017
  }
1059
1018
 
1060
1019
  // src/genre-tree/GenreSearch.tsx
1020
+ var import_react8 = require("react");
1061
1021
  var import_lucide_react7 = require("lucide-react");
1062
1022
  var import_ui6 = require("@behindthemusictree/ui");
1063
1023
  var import_genre_tree2 = require("@behindthemusictree/app-kit/genre-tree");
@@ -1070,7 +1030,7 @@ function GenreSearch({
1070
1030
  onPrevious,
1071
1031
  placeholder,
1072
1032
  onQueryChange,
1073
- extraResults
1033
+ extraGroups = []
1074
1034
  }) {
1075
1035
  const { query, setQuery, results, canClear, clear, select } = (0, import_genre_tree2.useGenreSearch)({
1076
1036
  genrePlaylists,
@@ -1078,11 +1038,45 @@ function GenreSearch({
1078
1038
  selectedName,
1079
1039
  onClear
1080
1040
  });
1081
- const extraItems = query !== "" && query !== selectedName ? extraResults?.items ?? [] : [];
1082
- const label = extraResults ? "Search" : "Search a genre";
1083
- const loading = query !== "" && query !== selectedName && (extraResults?.loading ?? false);
1084
- const showList = results.length > 0 || extraItems.length > 0;
1085
- const heading = "px-3 pt-2 pb-1 text-xs font-medium uppercase text-[var(--color-text-muted)]";
1041
+ const [open, setOpen] = (0, import_react8.useState)(false);
1042
+ const active = query !== "" && query !== selectedName;
1043
+ const hasExtras = extraGroups.length > 0;
1044
+ const label = hasExtras ? "Search" : "Search a genre";
1045
+ const loading = active && extraGroups.some((group) => group.loading);
1046
+ const status = active ? extraGroups.map((group) => group.status).filter(Boolean).join(". ") : "";
1047
+ const groups = [
1048
+ {
1049
+ key: "genres",
1050
+ label: hasExtras ? "Genres" : void 0,
1051
+ options: results.map((genrePlaylist) => ({
1052
+ key: genrePlaylist.uuid,
1053
+ content: /* @__PURE__ */ (0, import_jsx_runtime16.jsxs)(import_jsx_runtime16.Fragment, { children: [
1054
+ genrePlaylist.name,
1055
+ genrePlaylist.isUmbrella && /* @__PURE__ */ (0, import_jsx_runtime16.jsx)("span", { className: "ml-2 text-xs italic text-[var(--color-text-muted)]", children: "umbrella" })
1056
+ ] })
1057
+ }))
1058
+ },
1059
+ ...extraGroups.map((group) => ({
1060
+ key: group.key,
1061
+ label: group.label,
1062
+ options: active ? group.items.map((item) => ({ key: item.key, content: item.content })) : []
1063
+ }))
1064
+ ];
1065
+ const handlePick = (groupKey, optionKey) => {
1066
+ setOpen(false);
1067
+ if (groupKey === "genres") {
1068
+ const genrePlaylist = results.find((result) => result.uuid === optionKey);
1069
+ if (!genrePlaylist) return;
1070
+ select(genrePlaylist);
1071
+ onQueryChange?.("");
1072
+ return;
1073
+ }
1074
+ const item = extraGroups.find((group) => group.key === groupKey)?.items.find((entry) => entry.key === optionKey);
1075
+ if (!item) return;
1076
+ setQuery("");
1077
+ onQueryChange?.("");
1078
+ item.onSelect();
1079
+ };
1086
1080
  return /* @__PURE__ */ (0, import_jsx_runtime16.jsxs)("div", { className: "genre-search relative w-[calc(var(--layout-detail-panel-w,25.5rem)-2*var(--layout-panel-inset,1rem))] shrink-0 max-md:w-full", children: [
1087
1081
  onPrevious && /* @__PURE__ */ (0, import_jsx_runtime16.jsx)(
1088
1082
  "button",
@@ -1090,53 +1084,51 @@ function GenreSearch({
1090
1084
  type: "button",
1091
1085
  "aria-label": "Previous",
1092
1086
  title: "Previous",
1093
- 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)]",
1094
1088
  onClick: onPrevious,
1095
1089
  children: /* @__PURE__ */ (0, import_jsx_runtime16.jsx)(import_lucide_react7.ArrowLeft, { "aria-hidden": "true", className: "w-4 h-4" })
1096
1090
  }
1097
1091
  ),
1098
1092
  /* @__PURE__ */ (0, import_jsx_runtime16.jsx)(
1099
- import_ui6.Input,
1093
+ import_ui6.Combobox,
1100
1094
  {
1101
- type: "text",
1095
+ ariaLabel: label,
1096
+ listLabel: `${label} results`,
1102
1097
  value: query,
1103
- onChange: (event) => {
1104
- setQuery(event.target.value);
1105
- onQueryChange?.(event.target.value);
1106
- },
1107
1098
  placeholder: placeholder ?? `${label}\u2026`,
1108
- "aria-label": label,
1109
- "aria-busy": loading,
1110
- inputMode: "search",
1111
- enterKeyHint: "search",
1112
- autoComplete: "off",
1113
- className: "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"
1099
+ groups,
1100
+ open,
1101
+ onOpenChange: setOpen,
1102
+ onValueChange: (value) => {
1103
+ setQuery(value);
1104
+ onQueryChange?.(value);
1105
+ },
1106
+ onPick: handlePick,
1107
+ status,
1108
+ loading: loading ? "Searching\u2026" : void 0,
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`,
1110
+ className: "genre-search-results z-40 rounded-xl shadow-md",
1111
+ listClassName: "max-h-[min(20rem,50dvh)]",
1112
+ optionClassName: "[@media(pointer:coarse)]:min-h-[var(--layout-touch-target)]"
1114
1113
  }
1115
1114
  ),
1116
1115
  /* @__PURE__ */ (0, import_jsx_runtime16.jsxs)("div", { className: "absolute right-3 top-1/2 flex items-center -translate-y-1/2", children: [
1117
- /* @__PURE__ */ (0, import_jsx_runtime16.jsxs)(
1118
- "span",
1119
- {
1120
- "data-loading": loading,
1121
- className: "relative p-2 text-[var(--color-text-muted)] pointer-events-none",
1122
- children: [
1123
- /* @__PURE__ */ (0, import_jsx_runtime16.jsx)(
1124
- import_lucide_react7.Search,
1125
- {
1126
- "aria-hidden": "true",
1127
- className: `w-6 h-6 transition-opacity duration-200 motion-reduce:transition-none ${loading ? "opacity-0" : ""}`
1128
- }
1129
- ),
1130
- /* @__PURE__ */ (0, import_jsx_runtime16.jsx)(
1131
- import_lucide_react7.LoaderCircle,
1132
- {
1133
- "aria-hidden": "true",
1134
- className: `absolute inset-2 w-6 h-6 transition-opacity duration-200 motion-reduce:transition-none ${loading ? "opacity-100 delay-300 animate-spin motion-reduce:animate-none" : "opacity-0"}`
1135
- }
1136
- )
1137
- ]
1138
- }
1139
- ),
1116
+ /* @__PURE__ */ (0, import_jsx_runtime16.jsxs)("span", { "data-loading": loading, className: "relative p-2 text-[var(--color-text-muted)] pointer-events-none", children: [
1117
+ /* @__PURE__ */ (0, import_jsx_runtime16.jsx)(
1118
+ import_lucide_react7.Search,
1119
+ {
1120
+ "aria-hidden": "true",
1121
+ className: `w-6 h-6 transition-opacity duration-200 motion-reduce:transition-none ${loading ? "opacity-0" : ""}`
1122
+ }
1123
+ ),
1124
+ /* @__PURE__ */ (0, import_jsx_runtime16.jsx)(
1125
+ import_lucide_react7.LoaderCircle,
1126
+ {
1127
+ "aria-hidden": "true",
1128
+ className: `absolute inset-2 w-6 h-6 transition-opacity duration-200 motion-reduce:transition-none ${loading ? "opacity-100 delay-300 animate-spin motion-reduce:animate-none" : "opacity-0"}`
1129
+ }
1130
+ )
1131
+ ] }),
1140
1132
  canClear && /* @__PURE__ */ (0, import_jsx_runtime16.jsx)(
1141
1133
  "button",
1142
1134
  {
@@ -1150,39 +1142,6 @@ function GenreSearch({
1150
1142
  children: /* @__PURE__ */ (0, import_jsx_runtime16.jsx)(import_lucide_react7.X, { "aria-hidden": "true", className: "w-6 h-6" })
1151
1143
  }
1152
1144
  )
1153
- ] }),
1154
- extraResults?.status && /* @__PURE__ */ (0, import_jsx_runtime16.jsx)("p", { role: "status", "aria-live": "polite", className: "sr-only", children: extraResults.status }),
1155
- showList && /* @__PURE__ */ (0, import_jsx_runtime16.jsxs)("ul", { className: "genre-search-results absolute z-40 w-full max-h-[min(16rem,50dvh)] mt-1 overflow-y-auto bg-[var(--color-surface)] border border-[var(--color-border)] rounded-xl shadow-md", children: [
1156
- extraResults && results.length > 0 && /* @__PURE__ */ (0, import_jsx_runtime16.jsx)("li", { className: heading, children: "Genres" }),
1157
- results.map((genrePlaylist) => /* @__PURE__ */ (0, import_jsx_runtime16.jsx)("li", { children: /* @__PURE__ */ (0, import_jsx_runtime16.jsxs)(
1158
- "button",
1159
- {
1160
- type: "button",
1161
- className: "w-full px-3 py-1.5 [@media(pointer:coarse)]:min-h-[var(--layout-touch-target)] text-left hover:bg-[var(--color-surface-muted)]",
1162
- onClick: () => {
1163
- select(genrePlaylist);
1164
- onQueryChange?.("");
1165
- },
1166
- children: [
1167
- genrePlaylist.name,
1168
- genrePlaylist.isUmbrella && /* @__PURE__ */ (0, import_jsx_runtime16.jsx)("span", { className: "ml-2 text-xs italic text-[var(--color-text-muted)]", children: "umbrella" })
1169
- ]
1170
- }
1171
- ) }, genrePlaylist.uuid)),
1172
- extraResults && extraItems.length > 0 && /* @__PURE__ */ (0, import_jsx_runtime16.jsx)("li", { className: heading, children: extraResults.label }),
1173
- extraItems.map((item) => /* @__PURE__ */ (0, import_jsx_runtime16.jsx)("li", { children: /* @__PURE__ */ (0, import_jsx_runtime16.jsx)(
1174
- "button",
1175
- {
1176
- type: "button",
1177
- className: "w-full px-3 py-1.5 [@media(pointer:coarse)]:min-h-[var(--layout-touch-target)] text-left hover:bg-[var(--color-surface-muted)]",
1178
- onClick: () => {
1179
- setQuery("");
1180
- onQueryChange?.("");
1181
- item.onSelect();
1182
- },
1183
- children: item.content
1184
- }
1185
- ) }, item.key))
1186
1145
  ] })
1187
1146
  ] });
1188
1147
  }
@@ -1198,7 +1157,7 @@ var import_lucide_react10 = require("lucide-react");
1198
1157
  var import_popup4 = require("@behindthemusictree/app-kit/popup");
1199
1158
 
1200
1159
  // src/genre-tree/SharePopup.tsx
1201
- var import_react8 = require("react");
1160
+ var import_react9 = require("react");
1202
1161
  var import_lucide_react9 = require("lucide-react");
1203
1162
  var import_fa3 = require("react-icons/fa");
1204
1163
  var import_si = require("react-icons/si");
@@ -1230,8 +1189,8 @@ function ShareDestinationButton({
1230
1189
  var import_lucide_react8 = require("lucide-react");
1231
1190
  var import_jsx_runtime18 = require("react/jsx-runtime");
1232
1191
  function SharePreviewCard({ target, url }) {
1233
- const Icon = target.kind === "genre" ? import_lucide_react8.Music2 : import_lucide_react8.UserRound;
1234
- const typeLabel = target.kind === "genre" ? "Genre" : "Artist";
1192
+ const Icon = { genre: import_lucide_react8.Music2, artist: import_lucide_react8.UserRound, song: import_lucide_react8.Music }[target.kind];
1193
+ const typeLabel = { genre: "Genre", artist: "Artist", song: "Song" }[target.kind];
1235
1194
  return /* @__PURE__ */ (0, import_jsx_runtime18.jsx)("div", { className: "rounded-3xl bg-[var(--color-surface-muted)] p-4", children: /* @__PURE__ */ (0, import_jsx_runtime18.jsxs)("div", { className: "flex min-h-32 overflow-hidden rounded-2xl bg-[var(--color-surface)] shadow-lg", children: [
1236
1195
  /* @__PURE__ */ (0, import_jsx_runtime18.jsx)("div", { className: "flex w-28 shrink-0 items-center justify-center bg-[var(--color-primary)]/15 text-[var(--color-primary)]", children: /* @__PURE__ */ (0, import_jsx_runtime18.jsx)(Icon, { "aria-hidden": "true", className: "h-10 w-10" }) }),
1237
1196
  /* @__PURE__ */ (0, import_jsx_runtime18.jsxs)("div", { className: "flex min-w-0 flex-1 flex-col justify-center gap-1 p-4", children: [
@@ -1276,20 +1235,20 @@ async function copyText(text) {
1276
1235
  }
1277
1236
  }
1278
1237
  function SharePopup({ target, url, onClose }) {
1279
- const [absoluteUrl, setAbsoluteUrl] = (0, import_react8.useState)(url);
1280
- const [copyStatus, setCopyStatus] = (0, import_react8.useState)("idle");
1281
- const [canUseNativeShare, setCanUseNativeShare] = (0, import_react8.useState)(false);
1282
- const [shareError, setShareError] = (0, import_react8.useState)(null);
1283
- const [toastId, setToastId] = (0, import_react8.useState)(0);
1284
- (0, import_react8.useEffect)(() => {
1238
+ const [absoluteUrl, setAbsoluteUrl] = (0, import_react9.useState)(url);
1239
+ const [copyStatus, setCopyStatus] = (0, import_react9.useState)("idle");
1240
+ const [canUseNativeShare, setCanUseNativeShare] = (0, import_react9.useState)(false);
1241
+ const [shareError, setShareError] = (0, import_react9.useState)(null);
1242
+ const [toastId, setToastId] = (0, import_react9.useState)(0);
1243
+ (0, import_react9.useEffect)(() => {
1285
1244
  setAbsoluteUrl(resolveAbsoluteUrl(url) ?? "");
1286
1245
  setCopyStatus("idle");
1287
1246
  setShareError(null);
1288
1247
  }, [url]);
1289
- (0, import_react8.useEffect)(() => {
1248
+ (0, import_react9.useEffect)(() => {
1290
1249
  setCanUseNativeShare(typeof navigator !== "undefined" && typeof navigator.share === "function");
1291
1250
  }, []);
1292
- const destinationUrls = (0, import_react8.useMemo)(() => {
1251
+ const destinationUrls = (0, import_react9.useMemo)(() => {
1293
1252
  if (!absoluteUrl) return null;
1294
1253
  const message = `${target.name} \u2014 ${absoluteUrl}`;
1295
1254
  return {
@@ -1307,7 +1266,7 @@ function SharePopup({ target, url, onClose }) {
1307
1266
  setCopyStatus(copied ? "idle" : "failed");
1308
1267
  if (copied) setToastId((id) => id + 1);
1309
1268
  };
1310
- const dismissToast = (0, import_react8.useCallback)(() => setToastId(0), []);
1269
+ const dismissToast = (0, import_react9.useCallback)(() => setToastId(0), []);
1311
1270
  const handleNativeShare = async () => {
1312
1271
  if (!absoluteUrl || !navigator.share) return;
1313
1272
  setShareError(null);
@@ -1513,13 +1472,13 @@ var import_ui8 = require("@behindthemusictree/ui");
1513
1472
  var import_genre_tree9 = require("@behindthemusictree/app-kit/genre-tree");
1514
1473
 
1515
1474
  // src/genre-tree/ClampedText.tsx
1516
- var import_react9 = require("react");
1475
+ var import_react10 = require("react");
1517
1476
  var import_jsx_runtime22 = require("react/jsx-runtime");
1518
1477
  function ClampedText({ children, className: className2 = "" }) {
1519
- const ref = (0, import_react9.useRef)(null);
1520
- const [expanded, setExpanded] = (0, import_react9.useState)(false);
1521
- const [overflows, setOverflows] = (0, import_react9.useState)(false);
1522
- (0, import_react9.useLayoutEffect)(() => {
1478
+ const ref = (0, import_react10.useRef)(null);
1479
+ const [expanded, setExpanded] = (0, import_react10.useState)(false);
1480
+ const [overflows, setOverflows] = (0, import_react10.useState)(false);
1481
+ (0, import_react10.useLayoutEffect)(() => {
1523
1482
  const element = ref.current;
1524
1483
  if (!element || expanded) return;
1525
1484
  setOverflows(element.scrollHeight > element.clientHeight);
@@ -1540,7 +1499,7 @@ function ClampedText({ children, className: className2 = "" }) {
1540
1499
  }
1541
1500
 
1542
1501
  // src/genre-tree/GenrePlaylistTracks.tsx
1543
- var import_react10 = require("react");
1502
+ var import_react11 = require("react");
1544
1503
  var import_react_query = require("@tanstack/react-query");
1545
1504
  var import_zod = require("zod");
1546
1505
  var import_fa4 = require("react-icons/fa");
@@ -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({
@@ -1608,6 +1567,7 @@ function GenrePlaylistTracks({
1608
1567
  scope,
1609
1568
  getBackendBaseUrl,
1610
1569
  renderArtistLink,
1570
+ renderSongLink,
1611
1571
  renderSongLeading,
1612
1572
  renderSongActions,
1613
1573
  renderTracksToolbar,
@@ -1617,7 +1577,7 @@ function GenrePlaylistTracks({
1617
1577
  const { session, sessionRestored } = (0, import_auth2.useSession)();
1618
1578
  const { playerTrackObject, playState, handlePlayPauseAction } = (0, import_player4.usePlayer)();
1619
1579
  const { showPopup } = (0, import_popup5.usePopup)();
1620
- const trackNoun = (0, import_track_noun5.useTrackNoun)();
1580
+ const trackNoun = (0, import_track_noun2.useTrackNoun)();
1621
1581
  const { uuid } = genrePlaylist;
1622
1582
  const { fetch } = (0, import_transport4.useFetchWrapper)(getBackendBaseUrl);
1623
1583
  const { data, isPending, isError, refetch, hasNextPage, isFetchingNextPage, fetchNextPage } = (0, import_react_query.useInfiniteQuery)({
@@ -1638,8 +1598,8 @@ function GenrePlaylistTracks({
1638
1598
  getNextPageParam: (lastPage) => lastPage.nextPage ?? void 0,
1639
1599
  enabled: scope !== "me" || sessionRestored && !!session?.accessToken
1640
1600
  });
1641
- const sentinelRef = (0, import_react10.useRef)(null);
1642
- (0, import_react10.useEffect)(() => {
1601
+ const sentinelRef = (0, import_react11.useRef)(null);
1602
+ (0, import_react11.useEffect)(() => {
1643
1603
  const sentinel = sentinelRef.current;
1644
1604
  if (!hasNextPage || isFetchingNextPage || !sentinel) return;
1645
1605
  const observer = new IntersectionObserver((entries) => {
@@ -1662,7 +1622,7 @@ function GenrePlaylistTracks({
1662
1622
  };
1663
1623
  return /* @__PURE__ */ (0, import_jsx_runtime24.jsxs)("div", { className: "gtv-info-panel-children", children: [
1664
1624
  /* @__PURE__ */ (0, import_jsx_runtime24.jsxs)("span", { className: "gtv-info-panel-children-title", children: [
1665
- (0, import_track_noun5.capitalize)(trackNoun.plural),
1625
+ (0, import_track_noun2.capitalize)(trackNoun.plural),
1666
1626
  total !== void 0 && ` (${total})`
1667
1627
  ] }),
1668
1628
  renderTracksToolbar?.(),
@@ -1678,7 +1638,7 @@ function GenrePlaylistTracks({
1678
1638
  const isCurrent = playerTrackObject?.track.id === track.uuid;
1679
1639
  const isPlaying = isCurrent && playState === import_player5.PlayStates.PLAYING;
1680
1640
  const text = /* @__PURE__ */ (0, import_jsx_runtime24.jsxs)("span", { className: TEXT_CELL, children: [
1681
- /* @__PURE__ */ (0, import_jsx_runtime24.jsx)("span", { className: TITLE_CELL, children: (0, import_genre_tree6.isSong)(track) ? /* @__PURE__ */ (0, import_jsx_runtime24.jsx)(MusicBrainzRecordingLink, { song: track, className: "hover:underline focus-visible:underline", children: track.title }) : track.title }),
1641
+ /* @__PURE__ */ (0, import_jsx_runtime24.jsx)("span", { className: TITLE_CELL, children: renderSongLink ? renderSongLink(track) : (0, import_genre_tree6.isSong)(track) ? /* @__PURE__ */ (0, import_jsx_runtime24.jsx)(MusicBrainzRecordingLink, { song: track, className: "hover:underline focus-visible:underline", children: track.title }) : track.title }),
1682
1642
  track.artists?.length ? /* @__PURE__ */ (0, import_jsx_runtime24.jsx)("span", { className: SEPARATOR, children: " \u2014 " }) : null,
1683
1643
  /* @__PURE__ */ (0, import_jsx_runtime24.jsx)("span", { className: COLUMN_CELL, children: track.artists?.length ? /* @__PURE__ */ (0, import_jsx_runtime24.jsx)(import_genre_tree8.ArtistNames, { artists: track.artists, renderArtistLink }) : null }),
1684
1644
  track.album && /* @__PURE__ */ (0, import_jsx_runtime24.jsx)("span", { className: ALBUM_CELL, children: track.album.name })
@@ -1733,7 +1693,7 @@ function GenrePlaylistTracks({
1733
1693
  }
1734
1694
 
1735
1695
  // src/genre-tree/GenreDetailExtras.tsx
1736
- var import_track_noun6 = require("@behindthemusictree/app-kit/track-noun");
1696
+ var import_track_noun3 = require("@behindthemusictree/app-kit/track-noun");
1737
1697
  var import_si2 = require("react-icons/si");
1738
1698
  var import_jsx_runtime25 = require("react/jsx-runtime");
1739
1699
  function GenreDetailExtras({
@@ -1745,12 +1705,13 @@ function GenreDetailExtras({
1745
1705
  criteriaOverviewSchema,
1746
1706
  renderGenreDetailExtras,
1747
1707
  renderArtistLink,
1708
+ renderSongLink,
1748
1709
  renderSummary,
1749
1710
  renderEssentialTracks,
1750
1711
  family,
1751
1712
  ...trackSlots
1752
1713
  }) {
1753
- const trackNoun = (0, import_track_noun6.useTrackNoun)();
1714
+ const trackNoun = (0, import_track_noun3.useTrackNoun)();
1754
1715
  const { data: overview, isPending } = (0, import_genre_tree9.useFetchGenreOverview)(
1755
1716
  genreUuid,
1756
1717
  scope,
@@ -1822,6 +1783,7 @@ function GenreDetailExtras({
1822
1783
  scope,
1823
1784
  getBackendBaseUrl,
1824
1785
  renderArtistLink,
1786
+ renderSongLink,
1825
1787
  ...trackSlots
1826
1788
  }
1827
1789
  )
@@ -1830,7 +1792,7 @@ function GenreDetailExtras({
1830
1792
 
1831
1793
  // src/genre-tree/GenreInfoPanelBody.tsx
1832
1794
  var import_jsx_runtime26 = require("react/jsx-runtime");
1833
- var import_react11 = require("react");
1795
+ var import_react12 = require("react");
1834
1796
  function GenreNodeActionBand({ genrePlaylist, playback, ...bandProps }) {
1835
1797
  const { isPlaying, setIsPlaying } = (0, import_player6.usePlayer)();
1836
1798
  const isCurrentTrackList = playback.playingNodeId === genrePlaylist.uuid;
@@ -1867,7 +1829,7 @@ function GenreInfoPanelBody({
1867
1829
  return /* @__PURE__ */ (0, import_jsx_runtime26.jsxs)(import_jsx_runtime26.Fragment, { children: [
1868
1830
  toolbar,
1869
1831
  /* @__PURE__ */ (0, import_jsx_runtime26.jsx)(GenreNodeActionBand, { ...band, genrePlaylist, playback }),
1870
- /* @__PURE__ */ (0, import_react11.createElement)(
1832
+ /* @__PURE__ */ (0, import_react12.createElement)(
1871
1833
  GenreDetailExtras,
1872
1834
  {
1873
1835
  ...extras,
@@ -1883,66 +1845,10 @@ function GenreInfoPanelBody({
1883
1845
  ] });
1884
1846
  }
1885
1847
 
1886
- // src/genre-tree/PanelEdgeTab.tsx
1887
- var import_lucide_react12 = require("lucide-react");
1888
- var import_tailwind_merge = require("tailwind-merge");
1889
- var import_jsx_runtime27 = require("react/jsx-runtime");
1890
- 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)]";
1891
- function PanelEdgeTab({ side, edge, center, controls, className: className2 }) {
1892
- if (!edge && !center) return null;
1893
- const isLeft = side === "left";
1894
- const EdgeIcon = isLeft ? import_lucide_react12.ChevronLeft : import_lucide_react12.ChevronRight;
1895
- const CenterIcon = isLeft ? import_lucide_react12.ChevronRight : import_lucide_react12.ChevronLeft;
1896
- const renderButton = (action, icon) => /* @__PURE__ */ (0, import_jsx_runtime27.jsx)(
1897
- "button",
1898
- {
1899
- type: "button",
1900
- "aria-label": action.label,
1901
- title: action.label,
1902
- "aria-controls": controls,
1903
- "aria-expanded": action.expanded,
1904
- className: buttonClassName,
1905
- onClick: action.onClick,
1906
- children: icon
1907
- }
1908
- );
1909
- const edgeButton = edge && renderButton(edge, /* @__PURE__ */ (0, import_jsx_runtime27.jsx)(EdgeIcon, { "aria-hidden": "true", className: "h-5 w-5" }));
1910
- const centerButton = center && renderButton(center, /* @__PURE__ */ (0, import_jsx_runtime27.jsx)(CenterIcon, { "aria-hidden": "true", className: "h-5 w-5" }));
1911
- const buttons = isLeft ? [edgeButton, centerButton] : [centerButton, edgeButton];
1912
- return /* @__PURE__ */ (0, import_jsx_runtime27.jsxs)(
1913
- "div",
1914
- {
1915
- className: (0, import_tailwind_merge.twMerge)(
1916
- "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)]",
1917
- isLeft ? "rounded-r-lg" : "rounded-l-lg [clip-path:inset(-0.5rem_0_-0.5rem_-0.5rem)]",
1918
- className2
1919
- ),
1920
- children: [
1921
- buttons[0],
1922
- buttons[0] && buttons[1] && /* @__PURE__ */ (0, import_jsx_runtime27.jsx)("div", { role: "presentation", className: "h-px shrink-0 bg-[var(--color-border)]" }),
1923
- buttons[1]
1924
- ]
1925
- }
1926
- );
1927
- }
1928
-
1929
- // src/genre-tree/useLeftDetailPanelLayout.ts
1930
- var import_react12 = require("react");
1931
- function useLeftDetailPanelLayout(isPanelShown, enabled = true) {
1932
- (0, import_react12.useEffect)(() => {
1933
- if (!enabled || isPanelShown) return void 0;
1934
- const root = document.documentElement;
1935
- root.style.setProperty("--layout-left-w", "0px");
1936
- return () => {
1937
- root.style.removeProperty("--layout-left-w");
1938
- };
1939
- }, [enabled, isPanelShown]);
1940
- }
1941
-
1942
1848
  // src/genre-tree/ChangeParentPopup.tsx
1943
1849
  var import_react13 = require("react");
1944
1850
  var import_genre_tree10 = require("@behindthemusictree/app-kit/genre-tree");
1945
- var import_jsx_runtime28 = require("react/jsx-runtime");
1851
+ var import_jsx_runtime27 = require("react/jsx-runtime");
1946
1852
  function ChangeParentPopup({
1947
1853
  genrePlaylist,
1948
1854
  genrePlaylists,
@@ -1977,12 +1883,12 @@ function ChangeParentPopup({
1977
1883
  window.addEventListener("keydown", handleKeyDown);
1978
1884
  return () => window.removeEventListener("keydown", handleKeyDown);
1979
1885
  }, [onClose]);
1980
- return /* @__PURE__ */ (0, import_jsx_runtime28.jsxs)(BasePopup_default, { title: `Change parent of ${genrePlaylist.name}`, onClose, showCancelButton: true, fitContent: true, children: [
1981
- /* @__PURE__ */ (0, import_jsx_runtime28.jsxs)("p", { className: "mb-3 text-sm text-[var(--color-text-muted)]", children: [
1886
+ return /* @__PURE__ */ (0, import_jsx_runtime27.jsxs)(BasePopup_default, { title: `Change parent of ${genrePlaylist.name}`, onClose, showCancelButton: true, fitContent: true, children: [
1887
+ /* @__PURE__ */ (0, import_jsx_runtime27.jsxs)("p", { className: "mb-3 text-sm text-[var(--color-text-muted)]", children: [
1982
1888
  "Current parent: ",
1983
1889
  genrePlaylist.parent?.name ?? "none"
1984
1890
  ] }),
1985
- /* @__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)(
1891
+ /* @__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)(
1986
1892
  GenreSearch,
1987
1893
  {
1988
1894
  genrePlaylists: candidates,
@@ -1990,7 +1896,7 @@ function ChangeParentPopup({
1990
1896
  placeholder: "Search a new parent\u2026"
1991
1897
  }
1992
1898
  ) }),
1993
- 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(" ") })
1899
+ 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(" ") })
1994
1900
  ] });
1995
1901
  }
1996
1902
 
@@ -2002,12 +1908,12 @@ var import_genre_tree15 = require("@behindthemusictree/app-kit/genre-tree");
2002
1908
  var import_ui10 = require("@behindthemusictree/ui");
2003
1909
 
2004
1910
  // src/genre-tree/artist/ArtistLinks.tsx
2005
- var import_lucide_react13 = require("lucide-react");
1911
+ var import_lucide_react12 = require("lucide-react");
2006
1912
  var import_si3 = require("react-icons/si");
2007
1913
  var import_ui9 = require("@behindthemusictree/ui");
2008
- var import_jsx_runtime29 = require("react/jsx-runtime");
1914
+ var import_jsx_runtime28 = require("react/jsx-runtime");
2009
1915
  var LINK_KINDS = {
2010
- official: { label: "Official website", Icon: import_lucide_react13.Globe },
1916
+ official: { label: "Official website", Icon: import_lucide_react12.Globe },
2011
1917
  wikipedia: { label: "Wikipedia", Icon: import_si3.SiWikipedia },
2012
1918
  wikidata: { label: "Wikidata", Icon: import_si3.SiWikidata },
2013
1919
  spotify: { label: "Spotify", Icon: import_si3.SiSpotify },
@@ -2015,7 +1921,7 @@ var LINK_KINDS = {
2015
1921
  bandcamp: { label: "Bandcamp", Icon: import_si3.SiBandcamp },
2016
1922
  soundcloud: { label: "SoundCloud", Icon: import_si3.SiSoundcloud },
2017
1923
  discogs: { label: "Discogs", Icon: import_si3.SiDiscogs },
2018
- social: { label: "Social", Icon: import_lucide_react13.Share2 }
1924
+ social: { label: "Social", Icon: import_lucide_react12.Share2 }
2019
1925
  };
2020
1926
  function socialLabel(url) {
2021
1927
  try {
@@ -2042,7 +1948,7 @@ function artistLinks(artist) {
2042
1948
  return links;
2043
1949
  }
2044
1950
  function ArtistLinks({ links }) {
2045
- 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)) });
1951
+ 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)) });
2046
1952
  }
2047
1953
 
2048
1954
  // src/genre-tree/artist/formatYears.ts
@@ -2053,7 +1959,7 @@ function formatYears(beginYear, endYear, ended) {
2053
1959
  }
2054
1960
 
2055
1961
  // src/genre-tree/artist/ArtistHeader.tsx
2056
- var import_jsx_runtime30 = require("react/jsx-runtime");
1962
+ var import_jsx_runtime29 = require("react/jsx-runtime");
2057
1963
  var countryNames = new Intl.DisplayNames(["en"], { type: "region" });
2058
1964
  function ArtistHeader({ artist }) {
2059
1965
  const fields = [];
@@ -2063,35 +1969,35 @@ function ArtistHeader({ artist }) {
2063
1969
  const years = formatYears(artist.beginYear, artist.endYear, artist.ended);
2064
1970
  if (years) fields.push({ label: "Active", value: years });
2065
1971
  const links = artistLinks(artist);
2066
- if (links.length > 0) fields.push({ label: "Links", value: /* @__PURE__ */ (0, import_jsx_runtime30.jsx)(ArtistLinks, { links }) });
1972
+ if (links.length > 0) fields.push({ label: "Links", value: /* @__PURE__ */ (0, import_jsx_runtime29.jsx)(ArtistLinks, { links }) });
2067
1973
  if (fields.length === 0) return null;
2068
- return /* @__PURE__ */ (0, import_jsx_runtime30.jsx)(import_ui10.FieldList, { items: fields });
1974
+ return /* @__PURE__ */ (0, import_jsx_runtime29.jsx)(import_ui10.FieldList, { items: fields });
2069
1975
  }
2070
1976
 
2071
1977
  // src/genre-tree/artist/ArtistPhoto.tsx
2072
- var import_jsx_runtime31 = require("react/jsx-runtime");
1978
+ var import_jsx_runtime30 = require("react/jsx-runtime");
2073
1979
  function ExternalLink({ href, children }) {
2074
- return /* @__PURE__ */ (0, import_jsx_runtime31.jsx)("a", { href, target: "_blank", rel: "noopener noreferrer", className: "underline-offset-2 hover:underline", children });
1980
+ return /* @__PURE__ */ (0, import_jsx_runtime30.jsx)("a", { href, target: "_blank", rel: "noopener noreferrer", className: "underline-offset-2 hover:underline", children });
2075
1981
  }
2076
1982
  function ArtistPhoto({ image, name }) {
2077
- return /* @__PURE__ */ (0, import_jsx_runtime31.jsxs)("figure", { className: "flex flex-col gap-1.5 px-6 py-2.5", children: [
2078
- /* @__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" }),
2079
- /* @__PURE__ */ (0, import_jsx_runtime31.jsxs)("figcaption", { className: "text-xs text-[var(--color-text-muted)]", children: [
1983
+ return /* @__PURE__ */ (0, import_jsx_runtime30.jsxs)("figure", { className: "flex flex-col gap-1.5 px-6 py-2.5", children: [
1984
+ /* @__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" }),
1985
+ /* @__PURE__ */ (0, import_jsx_runtime30.jsxs)("figcaption", { className: "text-xs text-[var(--color-text-muted)]", children: [
2080
1986
  "Photo: ",
2081
- /* @__PURE__ */ (0, import_jsx_runtime31.jsx)(ExternalLink, { href: image.pageUrl, children: image.author ?? "Wikimedia Commons" }),
1987
+ /* @__PURE__ */ (0, import_jsx_runtime30.jsx)(ExternalLink, { href: image.pageUrl, children: image.author ?? "Wikimedia Commons" }),
2082
1988
  ",",
2083
1989
  " ",
2084
- image.licenseUrl ? /* @__PURE__ */ (0, import_jsx_runtime31.jsx)(ExternalLink, { href: image.licenseUrl, children: image.license }) : image.license
1990
+ image.licenseUrl ? /* @__PURE__ */ (0, import_jsx_runtime30.jsx)(ExternalLink, { href: image.licenseUrl, children: image.license }) : image.license
2085
1991
  ] })
2086
1992
  ] });
2087
1993
  }
2088
1994
 
2089
1995
  // src/genre-tree/artist/ArtistMembers.tsx
2090
1996
  var import_ui11 = require("@behindthemusictree/ui");
2091
- var import_jsx_runtime32 = require("react/jsx-runtime");
1997
+ var import_jsx_runtime31 = require("react/jsx-runtime");
2092
1998
  function MembershipList({ title, memberships, renderArtistLink }) {
2093
1999
  if (memberships.length === 0) return null;
2094
- return /* @__PURE__ */ (0, import_jsx_runtime32.jsx)(import_ui11.PanelGroup, { title, children: /* @__PURE__ */ (0, import_jsx_runtime32.jsx)("ul", { children: memberships.map((membership, index) => {
2000
+ return /* @__PURE__ */ (0, import_jsx_runtime31.jsx)(import_ui11.PanelGroup, { title, children: /* @__PURE__ */ (0, import_jsx_runtime31.jsx)("ul", { children: memberships.map((membership, index) => {
2095
2001
  const details = [
2096
2002
  formatYears(membership.beginYear, membership.endYear, membership.ended),
2097
2003
  ...membership.roles
@@ -2099,9 +2005,9 @@ function MembershipList({ title, memberships, renderArtistLink }) {
2099
2005
  const detailsText = details.filter(Boolean).join(" \xB7 ");
2100
2006
  return (
2101
2007
  // A member can have several periods in the same group.
2102
- /* @__PURE__ */ (0, import_jsx_runtime32.jsxs)("li", { className: "truncate py-0.5", title: detailsText || void 0, children: [
2103
- /* @__PURE__ */ (0, import_jsx_runtime32.jsx)("span", { className: "font-medium text-[var(--color-text)]", children: renderArtistLink(membership.artist) }),
2104
- detailsText && /* @__PURE__ */ (0, import_jsx_runtime32.jsxs)("span", { className: "text-[var(--color-text-muted)]", children: [
2008
+ /* @__PURE__ */ (0, import_jsx_runtime31.jsxs)("li", { className: "truncate py-0.5", title: detailsText || void 0, children: [
2009
+ /* @__PURE__ */ (0, import_jsx_runtime31.jsx)("span", { className: "font-medium text-[var(--color-text)]", children: renderArtistLink(membership.artist) }),
2010
+ detailsText && /* @__PURE__ */ (0, import_jsx_runtime31.jsxs)("span", { className: "text-[var(--color-text-muted)]", children: [
2105
2011
  " \u2014 ",
2106
2012
  detailsText
2107
2013
  ] })
@@ -2112,9 +2018,9 @@ function MembershipList({ title, memberships, renderArtistLink }) {
2112
2018
  function ArtistMembers({ title, memberships, renderArtistLink }) {
2113
2019
  if (memberships.length === 0) return null;
2114
2020
  const isCurrent = (membership) => !membership.ended && membership.endYear === null;
2115
- return /* @__PURE__ */ (0, import_jsx_runtime32.jsxs)(import_ui11.PanelSection, { title, children: [
2116
- /* @__PURE__ */ (0, import_jsx_runtime32.jsx)(MembershipList, { title: "Current", memberships: memberships.filter(isCurrent), renderArtistLink }),
2117
- /* @__PURE__ */ (0, import_jsx_runtime32.jsx)(
2021
+ return /* @__PURE__ */ (0, import_jsx_runtime31.jsxs)(import_ui11.PanelSection, { title, children: [
2022
+ /* @__PURE__ */ (0, import_jsx_runtime31.jsx)(MembershipList, { title: "Current", memberships: memberships.filter(isCurrent), renderArtistLink }),
2023
+ /* @__PURE__ */ (0, import_jsx_runtime31.jsx)(
2118
2024
  MembershipList,
2119
2025
  {
2120
2026
  title: "Past",
@@ -2127,7 +2033,7 @@ function ArtistMembers({ title, memberships, renderArtistLink }) {
2127
2033
 
2128
2034
  // src/genre-tree/artist/ArtistDiscography.tsx
2129
2035
  var import_ui12 = require("@behindthemusictree/ui");
2130
- var import_jsx_runtime33 = require("react/jsx-runtime");
2036
+ var import_jsx_runtime32 = require("react/jsx-runtime");
2131
2037
  var GROUPS = ["Albums", "EPs", "Singles", "Live", "Compilations", "Other"];
2132
2038
  var PRIMARY_GROUPS = { album: "Albums", ep: "EPs", single: "Singles" };
2133
2039
  function discographyGroup({ primaryType, secondaryTypes }) {
@@ -2147,12 +2053,12 @@ function byYear(a, b) {
2147
2053
  function ArtistDiscography({ albums }) {
2148
2054
  if (albums.length === 0) return null;
2149
2055
  const sorted = [...albums].sort(byYear);
2150
- return /* @__PURE__ */ (0, import_jsx_runtime33.jsx)(import_ui12.PanelSection, { title: "Discography", children: GROUPS.map((group) => {
2056
+ return /* @__PURE__ */ (0, import_jsx_runtime32.jsx)(import_ui12.PanelSection, { title: "Discography", children: GROUPS.map((group) => {
2151
2057
  const groupAlbums = sorted.filter((album) => discographyGroup(album) === group);
2152
2058
  if (groupAlbums.length === 0) return null;
2153
- 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: [
2154
- /* @__PURE__ */ (0, import_jsx_runtime33.jsx)("span", { className: "tabular-nums text-[var(--color-text-muted)]", children: album.year ?? "\u2014" }),
2155
- /* @__PURE__ */ (0, import_jsx_runtime33.jsx)("span", { className: "truncate text-[var(--color-text)]", title: album.name, children: album.name })
2059
+ 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: [
2060
+ /* @__PURE__ */ (0, import_jsx_runtime32.jsx)("span", { className: "tabular-nums text-[var(--color-text-muted)]", children: album.year ?? "\u2014" }),
2061
+ /* @__PURE__ */ (0, import_jsx_runtime32.jsx)("span", { className: "truncate text-[var(--color-text)]", title: album.name, children: album.name })
2156
2062
  ] }, album.uuid)) }) }, group);
2157
2063
  }) });
2158
2064
  }
@@ -2160,7 +2066,7 @@ function ArtistDiscography({ albums }) {
2160
2066
  // src/genre-tree/artist/ArtistSongs.tsx
2161
2067
  var import_react14 = require("react");
2162
2068
  var import_react_query2 = require("@tanstack/react-query");
2163
- var import_lucide_react14 = require("lucide-react");
2069
+ var import_lucide_react13 = require("lucide-react");
2164
2070
  var import_ui13 = require("@behindthemusictree/ui");
2165
2071
  var import_player10 = require("@behindthemusictree/app-kit/player");
2166
2072
  var import_genre_tree14 = require("@behindthemusictree/app-kit/genre-tree");
@@ -2174,7 +2080,7 @@ var import_genre_tree11 = require("@behindthemusictree/app-kit/genre-tree");
2174
2080
  var import_fa5 = require("react-icons/fa");
2175
2081
  var import_player7 = require("@behindthemusictree/app-kit/player");
2176
2082
  var import_player8 = require("@behindthemusictree/app-kit/player");
2177
- var import_jsx_runtime34 = require("react/jsx-runtime");
2083
+ var import_jsx_runtime33 = require("react/jsx-runtime");
2178
2084
  function TrackPositionPlayPause({
2179
2085
  position,
2180
2086
  uuid,
@@ -2188,14 +2094,14 @@ function TrackPositionPlayPause({
2188
2094
  const isPlaying = isThisTrack && isConfirmedPlayable && playState === import_player8.PlayStates.PLAYING;
2189
2095
  const isPaused = isThisTrack && isConfirmedPlayable && playState === import_player8.PlayStates.PAUSED;
2190
2096
  const isCurrentTrackActive = isPlaying || isPaused;
2191
- return /* @__PURE__ */ (0, import_jsx_runtime34.jsxs)(
2097
+ return /* @__PURE__ */ (0, import_jsx_runtime33.jsxs)(
2192
2098
  "div",
2193
2099
  {
2194
2100
  className: "track-position-play-pause relative flex items-center justify-center gap-2 text-lg w-16 cursor-pointer",
2195
2101
  style: { minWidth: "4rem", maxWidth: "4rem" },
2196
2102
  onClick: handlePlayPauseClick,
2197
2103
  children: [
2198
- /* @__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)(
2104
+ /* @__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)(
2199
2105
  "span",
2200
2106
  {
2201
2107
  className: "track-position-play-pause__bars",
@@ -2203,18 +2109,18 @@ function TrackPositionPlayPause({
2203
2109
  role: "img",
2204
2110
  "aria-label": isPlaying ? "Playing" : "Paused",
2205
2111
  children: [
2206
- /* @__PURE__ */ (0, import_jsx_runtime34.jsx)("span", { className: "track-position-play-pause__bar" }),
2207
- /* @__PURE__ */ (0, import_jsx_runtime34.jsx)("span", { className: "track-position-play-pause__bar" }),
2208
- /* @__PURE__ */ (0, import_jsx_runtime34.jsx)("span", { className: "track-position-play-pause__bar" })
2112
+ /* @__PURE__ */ (0, import_jsx_runtime33.jsx)("span", { className: "track-position-play-pause__bar" }),
2113
+ /* @__PURE__ */ (0, import_jsx_runtime33.jsx)("span", { className: "track-position-play-pause__bar" }),
2114
+ /* @__PURE__ */ (0, import_jsx_runtime33.jsx)("span", { className: "track-position-play-pause__bar" })
2209
2115
  ]
2210
2116
  }
2211
2117
  ) : position }),
2212
- /* @__PURE__ */ (0, import_jsx_runtime34.jsx)(
2118
+ /* @__PURE__ */ (0, import_jsx_runtime33.jsx)(
2213
2119
  "div",
2214
2120
  {
2215
2121
  className: `hidden group-hover:flex items-center justify-center ${isCurrentTrackActive ? "" : "absolute inset-0"}`,
2216
2122
  "aria-hidden": "true",
2217
- 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, {})
2123
+ 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, {})
2218
2124
  }
2219
2125
  )
2220
2126
  ]
@@ -2225,13 +2131,14 @@ function TrackPositionPlayPause({
2225
2131
  // src/genre-tree/track-list-sidebar/TrackItem.tsx
2226
2132
  var import_genre_tree12 = require("@behindthemusictree/app-kit/genre-tree");
2227
2133
  var import_genre_tree13 = require("@behindthemusictree/app-kit/genre-tree");
2228
- var import_jsx_runtime35 = require("react/jsx-runtime");
2134
+ var import_jsx_runtime34 = require("react/jsx-runtime");
2229
2135
  function TrackItem({
2230
2136
  track,
2231
2137
  position,
2232
2138
  renderDuration,
2233
2139
  renderActions,
2234
- renderArtistLink
2140
+ renderArtistLink,
2141
+ renderSongLink
2235
2142
  }) {
2236
2143
  const { handlePlayPauseAction, playerTrackObject, loadTrackForPlayer } = (0, import_player9.usePlayer)();
2237
2144
  const { trackList, selectedTrack, toTrackAtPosition, runtimeUnplayableTrackIds } = (0, import_genre_tree11.useTrackList)();
@@ -2248,12 +2155,12 @@ function TrackItem({
2248
2155
  loadTrackForPlayer(track.uuid);
2249
2156
  }
2250
2157
  };
2251
- return /* @__PURE__ */ (0, import_jsx_runtime35.jsxs)(
2158
+ return /* @__PURE__ */ (0, import_jsx_runtime34.jsxs)(
2252
2159
  "div",
2253
2160
  {
2254
2161
  className: `track-item flex h-14 text-[var(--color-text)] ${isUnplayable ? "opacity-50" : "hover:bg-[var(--color-surface-muted)] group"}`,
2255
2162
  children: [
2256
- isUnplayable ? /* @__PURE__ */ (0, import_jsx_runtime35.jsx)(
2163
+ isUnplayable ? /* @__PURE__ */ (0, import_jsx_runtime34.jsx)(
2257
2164
  "div",
2258
2165
  {
2259
2166
  className: "flex items-center justify-center text-lg w-16",
@@ -2261,7 +2168,7 @@ function TrackItem({
2261
2168
  "aria-disabled": "true",
2262
2169
  children: position
2263
2170
  }
2264
- ) : /* @__PURE__ */ (0, import_jsx_runtime35.jsx)(
2171
+ ) : /* @__PURE__ */ (0, import_jsx_runtime34.jsx)(
2265
2172
  TrackPositionPlayPause,
2266
2173
  {
2267
2174
  position,
@@ -2270,12 +2177,12 @@ function TrackItem({
2270
2177
  isSelected: selectedTrack?.uuid === track.uuid
2271
2178
  }
2272
2179
  ),
2273
- /* @__PURE__ */ (0, import_jsx_runtime35.jsxs)("div", { className: "title-artist-container flex flex-col items-start justify-center min-w-0 flex-1", children: [
2274
- /* @__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: (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 }) }),
2275
- 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 }) }) : ""
2180
+ /* @__PURE__ */ (0, import_jsx_runtime34.jsxs)("div", { className: "title-artist-container flex flex-col items-start justify-center min-w-0 flex-1", children: [
2181
+ /* @__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 }) }),
2182
+ 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 }) }) : ""
2276
2183
  ] }),
2277
- /* @__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 }) : "" }),
2278
- renderDuration && /* @__PURE__ */ (0, import_jsx_runtime35.jsx)(
2184
+ /* @__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 }) : "" }),
2185
+ renderDuration && /* @__PURE__ */ (0, import_jsx_runtime34.jsx)(
2279
2186
  "div",
2280
2187
  {
2281
2188
  className: "duration flex text-base w-16 items-center justify-center",
@@ -2283,20 +2190,20 @@ function TrackItem({
2283
2190
  children: renderDuration(track)
2284
2191
  }
2285
2192
  ),
2286
- 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) })
2193
+ 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) })
2287
2194
  ]
2288
2195
  }
2289
2196
  );
2290
2197
  }
2291
2198
 
2292
2199
  // src/genre-tree/artist/ArtistSongs.tsx
2293
- var import_jsx_runtime36 = require("react/jsx-runtime");
2200
+ var import_jsx_runtime35 = require("react/jsx-runtime");
2294
2201
  var SongPageSchema2 = (0, import_transport8.PaginatedResponseSchema)(import_genre_tree14.SongDetailedSchema);
2295
2202
  var SORTS = [
2296
2203
  { ordering: "title", label: "Title" },
2297
2204
  { ordering: "-albumYear", label: "Year" }
2298
2205
  ];
2299
- function ArtistSongs({ artist, getBackendBaseUrl, renderArtistLink }) {
2206
+ function ArtistSongs({ artist, getBackendBaseUrl, renderArtistLink, renderSongLink }) {
2300
2207
  const [page, setPage] = (0, import_react14.useState)(1);
2301
2208
  const [ordering, setOrdering] = (0, import_react14.useState)("title");
2302
2209
  const { fetch } = (0, import_transport8.useFetchWrapper)(getBackendBaseUrl);
@@ -2323,21 +2230,21 @@ function ArtistSongs({ artist, getBackendBaseUrl, renderArtistLink }) {
2323
2230
  event.stopPropagation();
2324
2231
  void play(song.uuid);
2325
2232
  };
2326
- 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: [
2327
- /* @__PURE__ */ (0, import_jsx_runtime36.jsxs)("div", { className: "flex flex-wrap items-center justify-between gap-2 mb-2", children: [
2328
- /* @__PURE__ */ (0, import_jsx_runtime36.jsxs)(
2233
+ 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: [
2234
+ /* @__PURE__ */ (0, import_jsx_runtime35.jsxs)("div", { className: "flex flex-wrap items-center justify-between gap-2 mb-2", children: [
2235
+ /* @__PURE__ */ (0, import_jsx_runtime35.jsxs)(
2329
2236
  "button",
2330
2237
  {
2331
2238
  type: "button",
2332
2239
  onClick: () => void play(),
2333
2240
  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",
2334
2241
  children: [
2335
- /* @__PURE__ */ (0, import_jsx_runtime36.jsx)(import_lucide_react14.Play, { className: "h-3.5 w-3.5", "aria-hidden": "true" }),
2242
+ /* @__PURE__ */ (0, import_jsx_runtime35.jsx)(import_lucide_react13.Play, { className: "h-3.5 w-3.5", "aria-hidden": "true" }),
2336
2243
  "Play all"
2337
2244
  ]
2338
2245
  }
2339
2246
  ),
2340
- /* @__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)(
2247
+ /* @__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)(
2341
2248
  "button",
2342
2249
  {
2343
2250
  type: "button",
@@ -2352,26 +2259,28 @@ function ArtistSongs({ artist, getBackendBaseUrl, renderArtistLink }) {
2352
2259
  sort.ordering
2353
2260
  )) })
2354
2261
  ] }),
2355
- /* @__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)(
2262
+ /* @__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)(
2356
2263
  TrackItem,
2357
2264
  {
2358
2265
  track: song,
2359
2266
  position: (page - 1) * data.pageSize + index + 1,
2360
- renderArtistLink
2267
+ renderArtistLink,
2268
+ renderSongLink
2361
2269
  }
2362
2270
  ) }, song.uuid)) }),
2363
- /* @__PURE__ */ (0, import_jsx_runtime36.jsx)(import_ui13.Pagination, { currentPage: page, totalPages: data.totalPages, onPageChange: setPage })
2271
+ /* @__PURE__ */ (0, import_jsx_runtime35.jsx)(import_ui13.Pagination, { currentPage: page, totalPages: data.totalPages, onPageChange: setPage })
2364
2272
  ] }) });
2365
2273
  }
2366
2274
 
2367
2275
  // src/genre-tree/artist/useArtistPanel.tsx
2368
- var import_jsx_runtime37 = require("react/jsx-runtime");
2276
+ var import_jsx_runtime36 = require("react/jsx-runtime");
2369
2277
  function useArtistPanel({
2370
2278
  uuid,
2371
2279
  scope,
2372
2280
  getBackendBaseUrl,
2373
2281
  getShareUrl,
2374
2282
  renderArtistLink,
2283
+ renderSongLink,
2375
2284
  expanded,
2376
2285
  onExpandedChange,
2377
2286
  onClose
@@ -2385,26 +2294,26 @@ function useArtistPanel({
2385
2294
  return {
2386
2295
  ...panel,
2387
2296
  title: "Artist",
2388
- content: /* @__PURE__ */ (0, import_jsx_runtime37.jsxs)("div", { "aria-busy": "true", className: "flex flex-col gap-2.5 px-6 py-2.5", children: [
2389
- /* @__PURE__ */ (0, import_jsx_runtime37.jsx)(import_ui14.Skeleton, { className: "h-3 w-16" }),
2390
- /* @__PURE__ */ (0, import_jsx_runtime37.jsx)(import_ui14.Skeleton, { className: "h-4 w-32" }),
2391
- /* @__PURE__ */ (0, import_jsx_runtime37.jsx)(import_ui14.Skeleton, { className: "h-3 w-16" }),
2392
- /* @__PURE__ */ (0, import_jsx_runtime37.jsx)(import_ui14.Skeleton, { className: "h-4 w-24" }),
2393
- /* @__PURE__ */ (0, import_jsx_runtime37.jsx)(import_ui14.Skeleton, { className: "h-16 mt-2.5" }),
2394
- /* @__PURE__ */ (0, import_jsx_runtime37.jsx)(import_ui14.Skeleton, { className: "h-16" })
2297
+ content: /* @__PURE__ */ (0, import_jsx_runtime36.jsxs)("div", { "aria-busy": "true", className: "flex flex-col gap-2.5 px-6 py-2.5", children: [
2298
+ /* @__PURE__ */ (0, import_jsx_runtime36.jsx)(import_ui14.Skeleton, { className: "h-3 w-16" }),
2299
+ /* @__PURE__ */ (0, import_jsx_runtime36.jsx)(import_ui14.Skeleton, { className: "h-4 w-32" }),
2300
+ /* @__PURE__ */ (0, import_jsx_runtime36.jsx)(import_ui14.Skeleton, { className: "h-3 w-16" }),
2301
+ /* @__PURE__ */ (0, import_jsx_runtime36.jsx)(import_ui14.Skeleton, { className: "h-4 w-24" }),
2302
+ /* @__PURE__ */ (0, import_jsx_runtime36.jsx)(import_ui14.Skeleton, { className: "h-16 mt-2.5" }),
2303
+ /* @__PURE__ */ (0, import_jsx_runtime36.jsx)(import_ui14.Skeleton, { className: "h-16" })
2395
2304
  ] })
2396
2305
  };
2397
2306
  case "not-found":
2398
2307
  return {
2399
2308
  ...panel,
2400
2309
  title: "Artist not found",
2401
- 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." })
2310
+ 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." })
2402
2311
  };
2403
2312
  case "error":
2404
2313
  return {
2405
2314
  ...panel,
2406
2315
  title: "Artist",
2407
- content: /* @__PURE__ */ (0, import_jsx_runtime37.jsx)("p", { className: "px-6 py-2.5 text-red-600", children: "Could not load this artist." })
2316
+ content: /* @__PURE__ */ (0, import_jsx_runtime36.jsx)("p", { className: "px-6 py-2.5 text-red-600", children: "Could not load this artist." })
2408
2317
  };
2409
2318
  case "success": {
2410
2319
  const { artist } = result;
@@ -2413,63 +2322,251 @@ function useArtistPanel({
2413
2322
  return {
2414
2323
  ...panel,
2415
2324
  title: artist.name,
2416
- content: /* @__PURE__ */ (0, import_jsx_runtime37.jsxs)(import_jsx_runtime37.Fragment, { children: [
2417
- 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 }) }) }),
2418
- artist.image && /* @__PURE__ */ (0, import_jsx_runtime37.jsx)(ArtistPhoto, { image: artist.image, name: artist.name }),
2419
- /* @__PURE__ */ (0, import_jsx_runtime37.jsx)(ArtistHeader, { artist }),
2420
- /* @__PURE__ */ (0, import_jsx_runtime37.jsx)(ArtistMembers, { title: "Members", memberships: artist.members, renderArtistLink }),
2421
- /* @__PURE__ */ (0, import_jsx_runtime37.jsx)(ArtistMembers, { title: "Member of", memberships: artist.memberOf, renderArtistLink }),
2422
- artist.tracksCount > 0 && /* @__PURE__ */ (0, import_jsx_runtime37.jsx)(
2325
+ content: /* @__PURE__ */ (0, import_jsx_runtime36.jsxs)(import_jsx_runtime36.Fragment, { children: [
2326
+ 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 }) }) }),
2327
+ artist.image && /* @__PURE__ */ (0, import_jsx_runtime36.jsx)(ArtistPhoto, { image: artist.image, name: artist.name }),
2328
+ /* @__PURE__ */ (0, import_jsx_runtime36.jsx)(ArtistHeader, { artist }),
2329
+ /* @__PURE__ */ (0, import_jsx_runtime36.jsx)(ArtistMembers, { title: "Members", memberships: artist.members, renderArtistLink }),
2330
+ /* @__PURE__ */ (0, import_jsx_runtime36.jsx)(ArtistMembers, { title: "Member of", memberships: artist.memberOf, renderArtistLink }),
2331
+ artist.tracksCount > 0 && /* @__PURE__ */ (0, import_jsx_runtime36.jsx)(
2423
2332
  ArtistSongs,
2424
2333
  {
2425
2334
  artist: { uuid: artist.uuid, name: artist.name },
2426
2335
  getBackendBaseUrl,
2427
- renderArtistLink
2336
+ renderArtistLink,
2337
+ renderSongLink
2428
2338
  }
2429
2339
  )
2430
2340
  ] }),
2431
- extraContent: artist.albums.length > 0 ? /* @__PURE__ */ (0, import_jsx_runtime37.jsx)(ArtistDiscography, { albums: artist.albums }) : void 0
2341
+ extraContent: artist.albums.length > 0 ? /* @__PURE__ */ (0, import_jsx_runtime36.jsx)(ArtistDiscography, { albums: artist.albums }) : void 0
2432
2342
  };
2433
2343
  }
2434
2344
  }
2435
2345
  }
2436
2346
 
2437
- // src/genre-tree/SizeMetricToggle.tsx
2438
- var import_md2 = require("react-icons/md");
2347
+ // src/genre-tree/song/useSongPanel.tsx
2348
+ var import_ui16 = require("@behindthemusictree/ui");
2349
+ var import_genre_tree17 = require("@behindthemusictree/app-kit/genre-tree");
2350
+
2351
+ // src/genre-tree/song/SongPanelContent.tsx
2352
+ var import_react15 = require("react");
2353
+ var import_lucide_react14 = require("lucide-react");
2439
2354
  var import_ui15 = require("@behindthemusictree/ui");
2355
+ var import_player11 = require("@behindthemusictree/app-kit/player");
2356
+ var import_genre_tree16 = require("@behindthemusictree/app-kit/genre-tree");
2357
+ var import_jsx_runtime37 = require("react/jsx-runtime");
2358
+ var numberFormat = new Intl.NumberFormat("en");
2359
+ var dateFormat = new Intl.DateTimeFormat("en", { dateStyle: "medium" });
2360
+ var externalLink = "text-[var(--color-primary)] hover:underline focus-visible:underline";
2361
+ function SongHistory({ uuid, getBackendBaseUrl }) {
2362
+ const [open, setOpen] = (0, import_react15.useState)(false);
2363
+ const history = (0, import_genre_tree16.useSongHistory)(uuid, getBackendBaseUrl, open);
2364
+ let body = null;
2365
+ if (history.status === "loading") body = /* @__PURE__ */ (0, import_jsx_runtime37.jsx)(import_ui15.Skeleton, { className: "h-10", "aria-busy": "true" });
2366
+ else if (history.status === "error") body = /* @__PURE__ */ (0, import_jsx_runtime37.jsx)("p", { className: "text-red-600", children: "Could not load the history." });
2367
+ else if (history.status === "success")
2368
+ 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: [
2369
+ /* @__PURE__ */ (0, import_jsx_runtime37.jsx)("span", { className: "font-medium", children: entry.action }),
2370
+ entry.oldValue !== null || entry.newValue !== null ? /* @__PURE__ */ (0, import_jsx_runtime37.jsxs)("span", { className: "text-[var(--color-text-muted)]", children: [
2371
+ " ",
2372
+ "\u2014 ",
2373
+ entry.oldValue ?? "none",
2374
+ " \u2192 ",
2375
+ entry.newValue ?? "none"
2376
+ ] }) : null,
2377
+ /* @__PURE__ */ (0, import_jsx_runtime37.jsxs)("span", { className: "block text-xs text-[var(--color-text-muted)]", children: [
2378
+ entry.actorPseudo ?? "Anonymous",
2379
+ " \xB7 ",
2380
+ dateFormat.format(new Date(entry.createdOn))
2381
+ ] })
2382
+ ] }, entry.uuid)) });
2383
+ return /* @__PURE__ */ (0, import_jsx_runtime37.jsxs)(
2384
+ "details",
2385
+ {
2386
+ onToggle: (event) => setOpen(event.currentTarget.open),
2387
+ className: "px-6 pb-2.5 mt-2.5 border-t border-[var(--color-border)]",
2388
+ children: [
2389
+ /* @__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" }),
2390
+ /* @__PURE__ */ (0, import_jsx_runtime37.jsx)("div", { className: "mt-1.5", children: body })
2391
+ ]
2392
+ }
2393
+ );
2394
+ }
2395
+ function SongPanelContent({
2396
+ song,
2397
+ scope,
2398
+ getBackendBaseUrl,
2399
+ shareUrl,
2400
+ renderArtistLink,
2401
+ genrePath,
2402
+ onSelectGenre
2403
+ }) {
2404
+ const { playNewTrackListFromTrackUuid } = (0, import_genre_tree16.useTrackList)();
2405
+ const { playerTrackObject, playState, handlePlayPauseAction } = (0, import_player11.usePlayer)();
2406
+ const unplayableReason = (0, import_genre_tree16.songUnplayableReason)(song);
2407
+ const isCurrent = playerTrackObject?.track.id === song.uuid;
2408
+ const isPlaying = isCurrent && playState === import_player11.PlayStates.PLAYING;
2409
+ const shareTarget = { kind: "song", uuid: song.uuid, name: song.title, scope };
2410
+ const musicBrainzUrl = (0, import_genre_tree16.musicBrainzRecordingUrl)(song);
2411
+ const fields = [];
2412
+ if (song.genre) {
2413
+ const path = genrePath.length > 0 ? genrePath : null;
2414
+ fields.push({
2415
+ label: "Genre",
2416
+ 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: [
2417
+ index > 0 && /* @__PURE__ */ (0, import_jsx_runtime37.jsx)("span", { "aria-hidden": "true", children: "\u203A " }),
2418
+ /* @__PURE__ */ (0, import_jsx_runtime37.jsx)("button", { type: "button", onClick: () => onSelectGenre(genrePlaylist), className: "hover:underline", children: genrePlaylist.name })
2419
+ ] }, genrePlaylist.uuid)) }) : song.genre.name
2420
+ });
2421
+ } else {
2422
+ fields.push({ label: "Genre", value: /* @__PURE__ */ (0, import_jsx_runtime37.jsx)("span", { className: "text-[var(--color-text-muted)]", children: "No genre" }) });
2423
+ }
2424
+ if (song.firstReleaseYear) fields.push({ label: "First released", value: song.firstReleaseYear });
2425
+ const viewCount = song.youtubeVideo?.viewCount;
2426
+ if (viewCount !== null && viewCount !== void 0)
2427
+ fields.push({ label: "YouTube views", value: numberFormat.format(viewCount) });
2428
+ fields.push({ label: "Plays", value: numberFormat.format(song.playCount) });
2429
+ const links = [];
2430
+ if (musicBrainzUrl)
2431
+ links.push(
2432
+ /* @__PURE__ */ (0, import_jsx_runtime37.jsx)("a", { href: musicBrainzUrl, target: "_blank", rel: "noopener noreferrer", className: externalLink, children: "MusicBrainz" }, "mb")
2433
+ );
2434
+ if (song.youtubeVideo)
2435
+ links.push(
2436
+ /* @__PURE__ */ (0, import_jsx_runtime37.jsx)(
2437
+ "a",
2438
+ {
2439
+ href: `https://www.youtube.com/watch?v=${encodeURIComponent(song.youtubeVideo.id)}`,
2440
+ target: "_blank",
2441
+ rel: "noopener noreferrer",
2442
+ className: externalLink,
2443
+ children: "YouTube"
2444
+ },
2445
+ "yt"
2446
+ )
2447
+ );
2448
+ if (links.length > 0)
2449
+ fields.push({
2450
+ label: "Links",
2451
+ value: /* @__PURE__ */ (0, import_jsx_runtime37.jsx)("span", { className: "flex gap-3", children: links })
2452
+ });
2453
+ return /* @__PURE__ */ (0, import_jsx_runtime37.jsxs)(import_jsx_runtime37.Fragment, { children: [
2454
+ /* @__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: [
2455
+ 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)(
2456
+ "button",
2457
+ {
2458
+ type: "button",
2459
+ onClick: () => isCurrent ? handlePlayPauseAction() : playNewTrackListFromTrackUuid(song, scope),
2460
+ className: "group flex flex-col items-center gap-1 w-16 text-xs text-center text-[var(--color-text)] focus-visible:outline-none",
2461
+ children: [
2462
+ /* @__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" }) }),
2463
+ isPlaying ? "Pause" : "Play"
2464
+ ]
2465
+ }
2466
+ ),
2467
+ shareUrl && /* @__PURE__ */ (0, import_jsx_runtime37.jsx)(ShareActionButton, { target: shareTarget, url: shareUrl })
2468
+ ] }) }),
2469
+ 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 }) }),
2470
+ /* @__PURE__ */ (0, import_jsx_runtime37.jsx)(import_ui15.FieldList, { items: fields }),
2471
+ 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)) }) }),
2472
+ /* @__PURE__ */ (0, import_jsx_runtime37.jsx)(SongHistory, { uuid: song.uuid, getBackendBaseUrl })
2473
+ ] });
2474
+ }
2475
+
2476
+ // src/genre-tree/song/useSongPanel.tsx
2440
2477
  var import_jsx_runtime38 = require("react/jsx-runtime");
2478
+ function useSongPanel({
2479
+ uuid,
2480
+ scope,
2481
+ getBackendBaseUrl,
2482
+ getShareUrl,
2483
+ renderArtistLink,
2484
+ getGenrePath,
2485
+ onSelectGenre,
2486
+ expanded,
2487
+ onExpandedChange,
2488
+ onClose
2489
+ }) {
2490
+ const result = (0, import_genre_tree17.useSong)(uuid, getBackendBaseUrl);
2491
+ const panel = { expanded, onExpandedChange, onClose };
2492
+ switch (result.status) {
2493
+ case "idle":
2494
+ return void 0;
2495
+ case "loading":
2496
+ return {
2497
+ ...panel,
2498
+ title: "Song",
2499
+ content: /* @__PURE__ */ (0, import_jsx_runtime38.jsxs)("div", { "aria-busy": "true", className: "flex flex-col gap-2.5 px-6 py-2.5", children: [
2500
+ /* @__PURE__ */ (0, import_jsx_runtime38.jsx)(import_ui16.Skeleton, { className: "h-3 w-16" }),
2501
+ /* @__PURE__ */ (0, import_jsx_runtime38.jsx)(import_ui16.Skeleton, { className: "h-4 w-32" }),
2502
+ /* @__PURE__ */ (0, import_jsx_runtime38.jsx)(import_ui16.Skeleton, { className: "h-3 w-16" }),
2503
+ /* @__PURE__ */ (0, import_jsx_runtime38.jsx)(import_ui16.Skeleton, { className: "h-4 w-24" })
2504
+ ] })
2505
+ };
2506
+ case "not-found":
2507
+ return {
2508
+ ...panel,
2509
+ title: "Song not found",
2510
+ 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." })
2511
+ };
2512
+ case "error":
2513
+ return {
2514
+ ...panel,
2515
+ title: "Song",
2516
+ content: /* @__PURE__ */ (0, import_jsx_runtime38.jsx)("p", { className: "px-6 py-2.5 text-red-600", children: "Could not load this song." })
2517
+ };
2518
+ case "success": {
2519
+ const { song } = result;
2520
+ const shareUrl = getShareUrl?.({ kind: "song", uuid: song.uuid, name: song.title, scope });
2521
+ return {
2522
+ ...panel,
2523
+ title: song.title,
2524
+ content: /* @__PURE__ */ (0, import_jsx_runtime38.jsx)(
2525
+ SongPanelContent,
2526
+ {
2527
+ song,
2528
+ scope,
2529
+ getBackendBaseUrl,
2530
+ shareUrl: shareUrl ?? void 0,
2531
+ renderArtistLink,
2532
+ genrePath: song.genre ? getGenrePath(song.genre) : [],
2533
+ onSelectGenre
2534
+ }
2535
+ )
2536
+ };
2537
+ }
2538
+ }
2539
+ }
2540
+
2541
+ // src/genre-tree/SizeMetricToggle.tsx
2542
+ var import_md2 = require("react-icons/md");
2543
+ var import_ui17 = require("@behindthemusictree/ui");
2544
+ var import_jsx_runtime39 = require("react/jsx-runtime");
2441
2545
  function SizeMetricToggle({
2442
2546
  value,
2443
2547
  onChange
2444
2548
  }) {
2445
- return /* @__PURE__ */ (0, import_jsx_runtime38.jsx)(
2446
- import_ui15.SegmentedControl,
2549
+ return /* @__PURE__ */ (0, import_jsx_runtime39.jsx)(
2550
+ import_ui17.SegmentedControl,
2447
2551
  {
2448
2552
  variant: "floating",
2449
2553
  "aria-label": "Size nodes by",
2450
2554
  value,
2451
2555
  onChange,
2452
2556
  options: [
2453
- { value: "songs", label: /* @__PURE__ */ (0, import_jsx_runtime38.jsx)(import_md2.MdMusicNote, { size: 18, "aria-hidden": true }), "aria-label": "Size by songs", title: "Node size by song count" },
2454
- { value: "views", label: /* @__PURE__ */ (0, import_jsx_runtime38.jsx)(import_md2.MdVisibility, { size: 18, "aria-hidden": true }), "aria-label": "Size by views", title: "Node size by song views" }
2557
+ { 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" },
2558
+ { 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" }
2455
2559
  ]
2456
2560
  }
2457
2561
  );
2458
2562
  }
2459
2563
 
2460
2564
  // src/genre-tree/GenreTreeView.tsx
2461
- var import_jsx_runtime39 = require("react/jsx-runtime");
2462
- var PREVIOUS_BUTTON_CSS = `
2463
- .genre-tree-view .search-overlay .gtv-info-panel-previous { display: none; }
2464
- .genre-tree-view:has(.search-overlay .gtv-info-panel-previous) .genre-search input { padding-left: 2.75rem; }
2465
- .genre-tree-view:has(.search-overlay .gtv-info-panel-previous) .genre-search-previous { display: flex; }
2466
- @keyframes genre-tree-view-previous-in { from { opacity: 0; transform: translate(-0.5rem, -50%); } to { opacity: 1; transform: translate(0, -50%); } }
2467
- @media (prefers-reduced-motion: no-preference) {
2468
- .genre-search-previous { animation: genre-tree-view-previous-in 200ms ease-out; }
2469
- }
2470
- `;
2565
+ var import_jsx_runtime40 = require("react/jsx-runtime");
2471
2566
  var NO_POP_ROOT_TITLE = "This genre tree has no 'Mainstream Pop' root yet";
2472
- 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]";
2567
+ var REGIONS_CLASS = "[--gtv-center-left:var(--layout-center-left)] [--gtv-center-right:var(--layout-center-right)] [--gtv-center-top:var(--layout-band-top)]";
2568
+ var noop = () => {
2569
+ };
2473
2570
  function GenreTreeView({
2474
2571
  scope,
2475
2572
  handleGenreCreationAction,
@@ -2502,6 +2599,13 @@ function GenreTreeView({
2502
2599
  artistSearch = true,
2503
2600
  onArtistSelect,
2504
2601
  artistSearchBaseUrl,
2602
+ songUuid: controlledSongUuid,
2603
+ onSongUuidChange,
2604
+ songExpanded: controlledSongExpanded,
2605
+ onSongExpandedChange,
2606
+ songSearch = true,
2607
+ onSongSelect,
2608
+ renderSongLink,
2505
2609
  renderSummary,
2506
2610
  renderEssentialTracks,
2507
2611
  renderSongLeading,
@@ -2516,14 +2620,21 @@ function GenreTreeView({
2516
2620
  placement = "default",
2517
2621
  ...focus
2518
2622
  }) {
2519
- const getShareUrlRef = (0, import_react15.useRef)(getShareUrl);
2623
+ const getShareUrlRef = (0, import_react16.useRef)(getShareUrl);
2520
2624
  getShareUrlRef.current = getShareUrl;
2521
- const [internalArtistUuid, setInternalArtistUuid] = (0, import_react15.useState)(null);
2625
+ const [internalArtistUuid, setInternalArtistUuid] = (0, import_react16.useState)(null);
2522
2626
  const isArtistControlled = controlledArtistUuid !== void 0;
2523
2627
  const artistUuid = isArtistControlled ? controlledArtistUuid : internalArtistUuid;
2524
- const [internalSizeMetric, setInternalSizeMetric] = (0, import_react15.useState)("songs");
2628
+ const [internalSongUuid, setInternalSongUuid] = (0, import_react16.useState)(null);
2629
+ const isSongControlled = controlledSongUuid !== void 0;
2630
+ const songUuid = isSongControlled ? controlledSongUuid : internalSongUuid;
2631
+ const changeSong = (uuid) => {
2632
+ if (!isSongControlled) setInternalSongUuid(uuid);
2633
+ onSongUuidChange?.(uuid);
2634
+ };
2635
+ const [internalSizeMetric, setInternalSizeMetric] = (0, import_react16.useState)("songs");
2525
2636
  const sizeMetric = controlledSizeMetric ?? internalSizeMetric;
2526
- const deferredSizeMetric = (0, import_react15.useDeferredValue)(sizeMetric);
2637
+ const deferredSizeMetric = (0, import_react16.useDeferredValue)(sizeMetric);
2527
2638
  const changeSizeMetric = (metric) => {
2528
2639
  if (controlledSizeMetric === void 0) setInternalSizeMetric(metric);
2529
2640
  onSizeMetricChange?.(metric);
@@ -2550,7 +2661,7 @@ function GenreTreeView({
2550
2661
  allowWheelRotation: internalAllowWheelRotation,
2551
2662
  toggleWheelRotation,
2552
2663
  onNodeHover: handleNodeHover
2553
- } = (0, import_genre_tree17.useGenreTreeViewState)({
2664
+ } = (0, import_genre_tree19.useGenreTreeViewState)({
2554
2665
  scope,
2555
2666
  getBackendBaseUrl,
2556
2667
  viewMode: controlledViewMode,
@@ -2560,7 +2671,7 @@ function GenreTreeView({
2560
2671
  artistUuid
2561
2672
  });
2562
2673
  const restoreRevision = viewportRestore?.revision;
2563
- const [stackedRestoreOwner, setStackedRestoreOwner] = (0, import_react15.useState)({ revision: restoreRevision, rootUuid: selectedGenrePlaylist?.root.uuid });
2674
+ const [stackedRestoreOwner, setStackedRestoreOwner] = (0, import_react16.useState)({ revision: restoreRevision, rootUuid: selectedGenrePlaylist?.root.uuid });
2564
2675
  let restoreRootUuid = stackedRestoreOwner.rootUuid;
2565
2676
  if (stackedRestoreOwner.revision !== restoreRevision) {
2566
2677
  restoreRootUuid = selectedGenrePlaylist?.root.uuid;
@@ -2569,14 +2680,14 @@ function GenreTreeView({
2569
2680
  restoreRootUuid = selectedGenrePlaylist.root.uuid;
2570
2681
  setStackedRestoreOwner({ revision: restoreRevision, rootUuid: restoreRootUuid });
2571
2682
  }
2572
- const isGenreUpdating = (0, import_react_query3.useIsMutating)({ mutationKey: import_genre_tree17.genreUpdateMutationKey }) > 0;
2573
- const { mutate: updateGenre } = (0, import_genre_tree17.useUpdateGenre)(scope, getBackendBaseUrl);
2683
+ const isGenreUpdating = (0, import_react_query3.useIsMutating)({ mutationKey: import_genre_tree19.genreUpdateMutationKey }) > 0;
2684
+ const { mutate: updateGenre } = (0, import_genre_tree19.useUpdateGenre)(scope, getBackendBaseUrl);
2574
2685
  const busy = deferredSizeMetric !== sizeMetric || isTreeFetching || isGenreUpdating;
2575
2686
  const isArtistFiltered = artistUuid !== null;
2576
2687
  const canSizeByViews = !isArtistFiltered && hasViewsCount(visibleGenrePlaylists);
2577
- const artistResult = (0, import_genre_tree17.useArtist)(artistUuid, artistSearchBaseUrl ?? getBackendBaseUrl);
2688
+ const artistResult = (0, import_genre_tree19.useArtist)(artistUuid, artistSearchBaseUrl ?? getBackendBaseUrl);
2578
2689
  const filterArtistName = artistResult.status === "success" ? artistResult.artist.name : null;
2579
- const filterArtist = (0, import_react15.useMemo)(
2690
+ const filterArtist = (0, import_react16.useMemo)(
2580
2691
  () => artistUuid !== null && filterArtistName !== null ? { uuid: artistUuid, name: filterArtistName } : null,
2581
2692
  [artistUuid, filterArtistName]
2582
2693
  );
@@ -2584,27 +2695,71 @@ function GenreTreeView({
2584
2695
  const playback = useGenrePlaylistPlayback({ scope, genrePlaylists: visibleGenrePlaylists, artist: filterArtist });
2585
2696
  const openArtist = (uuid) => {
2586
2697
  if (selectedNodeId !== null) clearSelection();
2698
+ if (songUuid !== null) changeSong(null);
2587
2699
  changeArtist(uuid);
2588
2700
  };
2589
- const [expandedArtistUuid, setExpandedArtistUuid] = (0, import_react15.useState)(null);
2701
+ const openSong = (uuid) => {
2702
+ if (selectedNodeId !== null) clearSelection();
2703
+ changeSong(uuid);
2704
+ };
2705
+ const openSongRef = (0, import_react16.useRef)(openSong);
2706
+ openSongRef.current = openSong;
2707
+ const resolvedRenderSongLink = (0, import_react16.useCallback)(
2708
+ (song) => renderSongLink ? renderSongLink(song) : /* @__PURE__ */ (0, import_jsx_runtime40.jsx)("button", { type: "button", onClick: () => openSongRef.current(song.uuid), className: "hover:underline", children: song.title }),
2709
+ [renderSongLink]
2710
+ );
2711
+ const [expandedSongUuid, setExpandedSongUuid] = (0, import_react16.useState)(null);
2712
+ const [expandedArtistUuid, setExpandedArtistUuid] = (0, import_react16.useState)(null);
2590
2713
  const artistBaseUrl = artistSearchBaseUrl ?? getBackendBaseUrl;
2714
+ const songBaseUrl = getBackendBaseUrl;
2591
2715
  const artistPanel = useArtistPanel({
2592
2716
  uuid: artistUuid,
2593
2717
  scope,
2594
2718
  getBackendBaseUrl: artistBaseUrl,
2595
2719
  getShareUrl: getShareUrlRef.current,
2596
- renderArtistLink: renderArtistLink ?? ((artist) => artist.isSpecialPurpose ? artist.name : /* @__PURE__ */ (0, import_jsx_runtime39.jsx)("button", { type: "button", onClick: () => openArtist(artist.uuid), className: "hover:underline", children: artist.name })),
2720
+ 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 })),
2597
2721
  expanded: controlledArtistExpanded ?? (artistUuid !== null && expandedArtistUuid === artistUuid),
2598
2722
  onExpandedChange: (on) => {
2599
2723
  if (controlledArtistExpanded === void 0) setExpandedArtistUuid(on ? artistUuid : null);
2600
2724
  onArtistExpandedChange?.(on);
2601
2725
  },
2726
+ renderSongLink: resolvedRenderSongLink,
2602
2727
  onClose: () => changeArtist(null)
2603
2728
  });
2604
- const builtInArtistPanel = overlayPanel || hasSelectedGenreNode ? void 0 : artistPanel;
2605
- const activeOverlayPanel = overlayPanel ?? (builtInArtistPanel && { ...builtInArtistPanel, hideTitle: true, hideClose: true });
2606
- const [uncontrolledCollapsed, setUncontrolledCollapsed] = (0, import_react15.useState)(false);
2607
- const [uncontrolledExpanded, setUncontrolledExpanded] = (0, import_react15.useState)(false);
2729
+ 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 }));
2730
+ const getGenrePath = (genre) => {
2731
+ const path = [];
2732
+ let current = visibleGenrePlaylists.find((playlist) => playlist.criteria?.uuid === genre.uuid);
2733
+ while (current) {
2734
+ path.unshift(current);
2735
+ const parentUuid = current.parent?.uuid;
2736
+ current = parentUuid ? visibleGenrePlaylists.find((playlist) => playlist.uuid === parentUuid) : void 0;
2737
+ }
2738
+ return path;
2739
+ };
2740
+ const songPanel = useSongPanel({
2741
+ uuid: songUuid,
2742
+ scope,
2743
+ getBackendBaseUrl: songBaseUrl,
2744
+ getShareUrl: getShareUrlRef.current,
2745
+ renderArtistLink: resolvedRenderArtistLink,
2746
+ getGenrePath,
2747
+ onSelectGenre: (genrePlaylist) => {
2748
+ changeSong(null);
2749
+ selectGenrePlaylist(genrePlaylist);
2750
+ },
2751
+ expanded: controlledSongExpanded ?? (songUuid !== null && expandedSongUuid === songUuid),
2752
+ onExpandedChange: (on) => {
2753
+ if (controlledSongExpanded === void 0) setExpandedSongUuid(on ? songUuid : null);
2754
+ onSongExpandedChange?.(on);
2755
+ },
2756
+ onClose: () => changeSong(null)
2757
+ });
2758
+ const builtInSongPanel = overlayPanel || hasSelectedGenreNode ? void 0 : songPanel;
2759
+ const builtInArtistPanel = overlayPanel || hasSelectedGenreNode || builtInSongPanel ? void 0 : artistPanel;
2760
+ const builtInPanel = builtInSongPanel ?? builtInArtistPanel;
2761
+ const [uncontrolledCollapsed, setUncontrolledCollapsed] = (0, import_react16.useState)(false);
2762
+ const [uncontrolledExpanded, setUncontrolledExpanded] = (0, import_react16.useState)(false);
2608
2763
  const isCollapsed = collapsed ?? uncontrolledCollapsed;
2609
2764
  const isExpanded = expanded ?? uncontrolledExpanded;
2610
2765
  const changeCollapsed = (next) => {
@@ -2615,54 +2770,56 @@ function GenreTreeView({
2615
2770
  setUncontrolledExpanded(next);
2616
2771
  onExpandedChange?.(next);
2617
2772
  };
2618
- (0, import_react15.useEffect)(() => {
2773
+ (0, import_react16.useEffect)(() => {
2619
2774
  setUncontrolledCollapsed(false);
2620
- }, [selectedNodeId, artistUuid]);
2621
- const isDetailHidden = isCollapsed || !hasSelectedGenreNode && !activeOverlayPanel;
2622
- useLeftDetailPanelLayout(!isDetailHidden, placement === "regions");
2623
- const detailOverlay = placement === "regions" && activeOverlayPanel?.onExpandedChange ? activeOverlayPanel : void 0;
2624
- const detailExpanded = detailOverlay ? !!detailOverlay.expanded : isExpanded;
2625
- const onDetailExpandedChange = detailOverlay ? detailOverlay.onExpandedChange : changeExpanded;
2626
- const infoPanelCuration = {
2627
- renderDangerZone,
2628
- expanded: placement === "regions" ? isExpanded : expanded,
2629
- onExpandedChange: placement === "regions" ? changeExpanded : onExpandedChange,
2630
- collapsed: placement === "regions" ? isCollapsed : void 0,
2631
- hideExpandButton: placement === "regions" ? true : void 0,
2632
- overlayPanel: detailOverlay ? { ...detailOverlay, collapsed: isCollapsed, hideExpandButton: true } : activeOverlayPanel
2633
- };
2634
- const [isWheelRevealed, setIsWheelRevealed] = (0, import_react15.useState)(false);
2775
+ }, [selectedNodeId, artistUuid, songUuid]);
2776
+ const [isWheelRevealed, setIsWheelRevealed] = (0, import_react16.useState)(false);
2635
2777
  const usesHandoff = viewMode === "wheel" || viewMode === "pop-core";
2636
2778
  const isTreeReady = !isLoading && (!usesHandoff || isWheelRevealed);
2637
- const rootRef = (0, import_react15.useRef)(null);
2638
- const handlePrevious = () => {
2639
- rootRef.current?.querySelector(".search-overlay .gtv-info-panel-previous")?.click();
2640
- };
2641
- (0, import_react15.useEffect)(() => {
2642
- if (!isTreeReady || viewMode !== "pop-core" || !hasSelectedGenreNode || !selectedGenrePlaylist) return;
2643
- const root = rootRef.current;
2644
- const centerChip = root?.querySelector(".gtv-wheel-center-node .gtv-wheel-chip");
2645
- if (!centerChip || centerChip.getAttribute("aria-label") !== selectedGenrePlaylist.name) return;
2646
- const panelTitle = root?.querySelector(".gtv-info-panel-title");
2647
- if (panelTitle?.textContent?.trim().startsWith(selectedGenrePlaylist.name)) return;
2648
- centerChip.click();
2649
- }, [hasSelectedGenreNode, isTreeReady, selectedGenrePlaylist, viewMode]);
2779
+ const [resolveStyle, setResolveStyle] = (0, import_react16.useState)(null);
2780
+ const handleNodeStyleResolverChange = (0, import_react16.useCallback)(
2781
+ (resolver) => setResolveStyle(() => resolver),
2782
+ []
2783
+ );
2784
+ const panelRef = (0, import_react16.useRef)(null);
2785
+ const obscuredInset = (0, import_ui18.useObscuredInset)(panelRef);
2786
+ const hostPanel = { obscuredInset, onNodeStyleResolverChange: handleNodeStyleResolverChange };
2787
+ const nodeMetric = isArtistFiltered || !canSizeByViews ? "songs" : deferredSizeMetric;
2788
+ const treeNodes = (0, import_react16.useMemo)(() => toTreeNodes(visibleGenrePlaylists, nodeMetric), [visibleGenrePlaylists, nodeMetric]);
2789
+ const relations = (0, import_genre_tree_view4.useGenreNodeRelations)(
2790
+ treeNodes,
2791
+ !overlayPanel && hasSelectedGenreNode ? selectedNodeId : null,
2792
+ resolveStyle
2793
+ );
2794
+ const [selectionHistory, setSelectionHistory] = (0, import_react16.useState)({
2795
+ current: selectedNodeId,
2796
+ previous: null
2797
+ });
2798
+ if (selectionHistory.current !== selectedNodeId) {
2799
+ setSelectionHistory({
2800
+ current: selectedNodeId,
2801
+ previous: selectionHistory.current ?? selectionHistory.previous
2802
+ });
2803
+ }
2804
+ const previousPlaylist = selectionHistory.previous !== null && selectionHistory.previous !== selectedNodeId ? genrePlaylistByNodeId.get(selectionHistory.previous) : void 0;
2805
+ const handlePrevious = previousPlaylist && visibleGenrePlaylists.includes(previousPlaylist) ? () => handleGenreSearchSelect(previousPlaylist) : void 0;
2650
2806
  const { showPopup, hidePopup } = (0, import_popup6.usePopup)();
2651
- const [searchText, setSearchText] = (0, import_react15.useState)("");
2652
- const artistSearchResult = (0, import_genre_tree16.useArtistSearch)(artistBaseUrl, artistSearch ? searchText : "");
2653
- const artistExtraResults = artistSearch ? {
2807
+ const [searchText, setSearchText] = (0, import_react16.useState)("");
2808
+ const artistSearchResult = (0, import_genre_tree18.useArtistSearch)(artistBaseUrl, artistSearch ? searchText : "");
2809
+ const artistGroup = artistSearch ? {
2810
+ key: "artists",
2654
2811
  label: "Artists",
2655
2812
  items: artistSearchResult.artists.map((artist) => ({
2656
2813
  key: artist.uuid,
2657
- content: /* @__PURE__ */ (0, import_jsx_runtime39.jsxs)(import_jsx_runtime39.Fragment, { children: [
2658
- /* @__PURE__ */ (0, import_jsx_runtime39.jsxs)("span", { className: "block truncate font-medium", children: [
2814
+ content: /* @__PURE__ */ (0, import_jsx_runtime40.jsxs)(import_jsx_runtime40.Fragment, { children: [
2815
+ /* @__PURE__ */ (0, import_jsx_runtime40.jsxs)("span", { className: "block truncate font-medium", children: [
2659
2816
  artist.name,
2660
- artist.disambiguation && /* @__PURE__ */ (0, import_jsx_runtime39.jsxs)("span", { className: "font-normal text-[var(--color-neutral-500)]", children: [
2817
+ artist.disambiguation && /* @__PURE__ */ (0, import_jsx_runtime40.jsxs)("span", { className: "font-normal text-[var(--color-neutral-500)]", children: [
2661
2818
  " \xB7 ",
2662
2819
  artist.disambiguation
2663
2820
  ] })
2664
2821
  ] }),
2665
- artist.matchedAlias && /* @__PURE__ */ (0, import_jsx_runtime39.jsxs)("span", { className: "block truncate text-xs text-[var(--color-neutral-500)]", children: [
2822
+ artist.matchedAlias && /* @__PURE__ */ (0, import_jsx_runtime40.jsxs)("span", { className: "block truncate text-xs text-[var(--color-neutral-500)]", children: [
2666
2823
  "also known as ",
2667
2824
  artist.matchedAlias
2668
2825
  ] })
@@ -2675,11 +2832,32 @@ function GenreTreeView({
2675
2832
  status: artistSearchResult.isError ? "Search failed" : artistSearchResult.settled && artistSearchResult.artists.length === 0 ? "No artists found" : "",
2676
2833
  loading: artistSearchResult.pending
2677
2834
  } : void 0;
2835
+ const songSearchResult = (0, import_genre_tree18.useSongSearch)(songBaseUrl, songSearch ? searchText : "");
2836
+ const songGroup = songSearch ? {
2837
+ key: "songs",
2838
+ label: "Songs",
2839
+ items: songSearchResult.songs.map((song) => ({
2840
+ key: song.uuid,
2841
+ content: /* @__PURE__ */ (0, import_jsx_runtime40.jsxs)("span", { className: (0, import_genre_tree18.isSongSearchResultPlayable)(song) ? void 0 : "opacity-60", children: [
2842
+ /* @__PURE__ */ (0, import_jsx_runtime40.jsx)("span", { className: "block truncate font-medium", children: song.title }),
2843
+ /* @__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 ") })
2844
+ ] }),
2845
+ onSelect: () => {
2846
+ openSong(song.uuid);
2847
+ onSongSelect?.(song);
2848
+ }
2849
+ })),
2850
+ status: songSearchResult.isError ? "Search failed" : songSearchResult.settled && songSearchResult.songs.length === 0 ? "No songs found" : "",
2851
+ loading: songSearchResult.pending
2852
+ } : void 0;
2853
+ const extraGroups = [artistGroup, songGroup].filter((group) => group !== void 0);
2678
2854
  const handleSelectedNodeChange = (node) => {
2855
+ if (node !== null && songUuid !== null) changeSong(null);
2679
2856
  selectNode(node);
2680
2857
  };
2681
2858
  const handleGenreSearchClear = clearSelection;
2682
2859
  const handleGenreSearchSelect = (genrePlaylist) => {
2860
+ if (songUuid !== null) changeSong(null);
2683
2861
  selectGenrePlaylist(genrePlaylist);
2684
2862
  };
2685
2863
  const selectedUuid = selectedGenrePlaylist?.uuid;
@@ -2688,36 +2866,49 @@ function GenreTreeView({
2688
2866
  const handleGenreSearchZoomIn = () => {
2689
2867
  if (selectedUuid) focus.onFocusGenreChange?.(selectedUuid, "zoom-in");
2690
2868
  };
2691
- const zoomInRef = (0, import_react15.useRef)(void 0);
2869
+ const zoomInRef = (0, import_react16.useRef)(void 0);
2692
2870
  zoomInRef.current = canZoomIn ? handleGenreSearchZoomIn : void 0;
2693
- const renderInfoPanelBody = (0, import_react15.useCallback)(
2694
- (node, { toolbar, ancestors, family }) => {
2695
- const genrePlaylist = genrePlaylistByNodeId.get(node.id);
2696
- const criteria = genrePlaylist?.criteria;
2697
- if (!genrePlaylist || !criteria) return { primary: /* @__PURE__ */ (0, import_jsx_runtime39.jsxs)(import_jsx_runtime39.Fragment, { children: [
2698
- toolbar,
2699
- family
2700
- ] }), aside: ancestors };
2701
- const genreUuid = criteria.uuid;
2702
- const parentPlaylist = visibleGenrePlaylists.find(({ uuid }) => uuid === genrePlaylist.parent?.uuid);
2703
- const canToggleSide = !readOnly && parentPlaylist !== void 0 && parentPlaylist.parent === null;
2704
- const side = criteria.side ?? "core";
2705
- const shareTarget = {
2706
- kind: "genre",
2707
- genrePlaylistUuid: genrePlaylist.uuid,
2708
- name: criteria.name,
2709
- scope,
2710
- artistUuid
2711
- };
2712
- const shareUrl = getShareUrlRef.current?.(shareTarget) ?? void 0;
2713
- const popSibling = canToggleSide && side === "core" ? visibleGenrePlaylists.find(
2714
- (sibling) => sibling.parent?.uuid === parentPlaylist.uuid && sibling.uuid !== genrePlaylist.uuid && sibling.criteria?.side === "pop"
2715
- ) : void 0;
2716
- return {
2717
- primary: /* @__PURE__ */ (0, import_jsx_runtime39.jsx)(
2871
+ const selectGenreFromPanel = (nodeId) => {
2872
+ const genrePlaylist = genrePlaylistByNodeId.get(nodeId);
2873
+ if (genrePlaylist) handleGenreSearchSelect(genrePlaylist);
2874
+ };
2875
+ const renderGenreBody = (relation) => {
2876
+ const { node } = relation;
2877
+ const family = /* @__PURE__ */ (0, import_jsx_runtime40.jsx)(
2878
+ import_genre_tree_view4.GenreNodeFamily,
2879
+ {
2880
+ node,
2881
+ fill: relation.fill,
2882
+ textColor: relation.textColor,
2883
+ parentNode: relation.parentNode,
2884
+ childNodes: relation.childNodes,
2885
+ onSelectNode: selectGenreFromPanel
2886
+ }
2887
+ );
2888
+ const ancestors = /* @__PURE__ */ (0, import_jsx_runtime40.jsx)(import_genre_tree_view4.GenreNodeAncestors, { ancestorNodes: relation.ancestorNodes, onSelectNode: selectGenreFromPanel });
2889
+ const genrePlaylist = genrePlaylistByNodeId.get(node.id);
2890
+ const criteria = genrePlaylist?.criteria;
2891
+ if (!genrePlaylist || !criteria) return { primary: family, aside: ancestors };
2892
+ const genreUuid = criteria.uuid;
2893
+ const parentPlaylist = visibleGenrePlaylists.find(({ uuid }) => uuid === genrePlaylist.parent?.uuid);
2894
+ const canToggleSide = !readOnly && parentPlaylist !== void 0 && parentPlaylist.parent === null;
2895
+ const side = criteria.side ?? "core";
2896
+ const shareTarget = {
2897
+ kind: "genre",
2898
+ genrePlaylistUuid: genrePlaylist.uuid,
2899
+ name: criteria.name,
2900
+ scope,
2901
+ artistUuid
2902
+ };
2903
+ const shareUrl = getShareUrlRef.current?.(shareTarget) ?? void 0;
2904
+ const popSibling = canToggleSide && side === "core" ? visibleGenrePlaylists.find(
2905
+ (sibling) => sibling.parent?.uuid === parentPlaylist.uuid && sibling.uuid !== genrePlaylist.uuid && sibling.criteria?.side === "pop"
2906
+ ) : void 0;
2907
+ return {
2908
+ primary: /* @__PURE__ */ (0, import_jsx_runtime40.jsxs)(import_jsx_runtime40.Fragment, { children: [
2909
+ /* @__PURE__ */ (0, import_jsx_runtime40.jsx)(
2718
2910
  GenreInfoPanelBody,
2719
2911
  {
2720
- toolbar,
2721
2912
  family,
2722
2913
  genreUuid,
2723
2914
  genrePlaylist,
@@ -2733,7 +2924,7 @@ function GenreTreeView({
2733
2924
  onAddChild: readOnly ? void 0 : () => handleGenreCreationAction(criteria),
2734
2925
  onRename: readOnly ? void 0 : () => handleGenreRenameAction(criteria),
2735
2926
  onChangeParent: readOnly ? void 0 : () => showPopup(
2736
- /* @__PURE__ */ (0, import_jsx_runtime39.jsx)(
2927
+ /* @__PURE__ */ (0, import_jsx_runtime40.jsx)(
2737
2928
  ChangeParentPopup,
2738
2929
  {
2739
2930
  genrePlaylist: { ...genrePlaylist, criteria },
@@ -2754,6 +2945,7 @@ function GenreTreeView({
2754
2945
  criteriaOverviewSchema,
2755
2946
  renderGenreDetailExtras,
2756
2947
  renderArtistLink,
2948
+ renderSongLink: resolvedRenderSongLink,
2757
2949
  artist: filterArtist,
2758
2950
  renderSummary,
2759
2951
  renderEssentialTracks,
@@ -2763,48 +2955,65 @@ function GenreTreeView({
2763
2955
  }
2764
2956
  }
2765
2957
  ),
2766
- aside: ancestors
2767
- };
2768
- },
2769
- [
2770
- genrePlaylistByNodeId,
2771
- additionalActions,
2772
- readOnly,
2773
- handleGenreCreationAction,
2774
- handleGenreRenameAction,
2775
- showPopup,
2776
- hidePopup,
2777
- onParentChanged,
2778
- visibleGenrePlaylists,
2779
- updateGenre,
2780
- playback,
2781
- filterArtist,
2782
- scope,
2783
- getBackendBaseUrl,
2784
- criteriaOverviewSchema,
2785
- renderGenreDetailExtras,
2786
- renderArtistLink,
2787
- renderSummary,
2788
- renderEssentialTracks,
2789
- renderSongLeading,
2790
- renderSongActions,
2791
- renderTracksToolbar
2792
- ]
2793
- );
2958
+ renderDangerZone?.(node)
2959
+ ] }),
2960
+ aside: ancestors
2961
+ };
2962
+ };
2794
2963
  const isRotationControlled = controlledAllowWheelRotation !== void 0;
2795
2964
  const allowWheelRotation = controlledAllowWheelRotation ?? internalAllowWheelRotation;
2796
- const detailTab = placement === "regions" && (hasSelectedGenreNode || detailOverlay) ? /* @__PURE__ */ (0, import_jsx_runtime39.jsx)(
2797
- PanelEdgeTab,
2965
+ let activePanel = null;
2966
+ if (overlayPanel) {
2967
+ activePanel = {
2968
+ title: overlayPanel.title,
2969
+ content: overlayPanel.content,
2970
+ aside: overlayPanel.extraContent,
2971
+ expanded: overlayPanel.expanded ?? false,
2972
+ onExpandedChange: overlayPanel.onExpandedChange,
2973
+ onClose: overlayPanel.onClose,
2974
+ compact: false
2975
+ };
2976
+ } else if (relations) {
2977
+ const { primary, aside } = renderGenreBody(relations);
2978
+ activePanel = {
2979
+ title: relations.node.name,
2980
+ content: primary,
2981
+ aside,
2982
+ expanded: isExpanded,
2983
+ onExpandedChange: changeExpanded,
2984
+ onClose: clearSelection,
2985
+ compact: true,
2986
+ headerAccent: { background: relations.fill, color: relations.textColor }
2987
+ };
2988
+ } else if (builtInPanel) {
2989
+ activePanel = {
2990
+ title: builtInPanel.title,
2991
+ content: builtInPanel.content,
2992
+ aside: builtInPanel.extraContent,
2993
+ expanded: builtInPanel.expanded,
2994
+ onExpandedChange: builtInPanel.onExpandedChange,
2995
+ onClose: builtInPanel.onClose,
2996
+ compact: true
2997
+ };
2998
+ }
2999
+ const isPanelShown = isTreeReady && activePanel !== null;
3000
+ const detailTab = placement === "regions" && activePanel?.onExpandedChange ? {
3001
+ edge: activePanel.expanded ? { label: "Shrink panel", onClick: () => activePanel.onExpandedChange?.(false) } : isCollapsed ? null : { label: "Hide panel", onClick: () => changeCollapsed(true) },
3002
+ center: activePanel.expanded ? null : isCollapsed ? { label: "Show panel", onClick: () => changeCollapsed(false) } : activePanel.onExpandedChange ? { label: "Extend panel", onClick: () => activePanel.onExpandedChange?.(true) } : null
3003
+ } : void 0;
3004
+ const expandButton = placement === "default" && activePanel?.onExpandedChange ? /* @__PURE__ */ (0, import_jsx_runtime40.jsx)(
3005
+ import_ui18.IconButton,
2798
3006
  {
2799
- side: "left",
2800
- edge: detailExpanded ? { label: "Shrink panel", onClick: () => onDetailExpandedChange?.(false) } : isCollapsed ? null : { label: "Hide panel", onClick: () => changeCollapsed(true) },
2801
- center: detailExpanded ? null : isCollapsed ? { label: "Show panel", onClick: () => changeCollapsed(false) } : onDetailExpandedChange ? { label: "Extend panel", onClick: () => onDetailExpandedChange(true) } : null
3007
+ "aria-label": activePanel.expanded ? "Shrink panel" : "Extend panel",
3008
+ style: activePanel.headerAccent && { color: "inherit" },
3009
+ onClick: () => activePanel.onExpandedChange?.(!activePanel.expanded),
3010
+ 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" })
2802
3011
  }
2803
3012
  ) : null;
2804
3013
  const renderArtistFilter = () => {
2805
3014
  if (isLoading || !filterArtist) return null;
2806
- return /* @__PURE__ */ (0, import_jsx_runtime39.jsxs)("span", { className: "inline-flex items-center overflow-hidden rounded-full bg-[var(--color-neutral-100)] text-sm", children: [
2807
- /* @__PURE__ */ (0, import_jsx_runtime39.jsx)(
3015
+ 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: [
3016
+ /* @__PURE__ */ (0, import_jsx_runtime40.jsx)(
2808
3017
  "button",
2809
3018
  {
2810
3019
  type: "button",
@@ -2816,7 +3025,7 @@ function GenreTreeView({
2816
3025
  children: filterArtist.name
2817
3026
  }
2818
3027
  ),
2819
- /* @__PURE__ */ (0, import_jsx_runtime39.jsx)(
3028
+ /* @__PURE__ */ (0, import_jsx_runtime40.jsx)(
2820
3029
  "button",
2821
3030
  {
2822
3031
  type: "button",
@@ -2828,23 +3037,23 @@ function GenreTreeView({
2828
3037
  )
2829
3038
  ] });
2830
3039
  };
2831
- const actions = /* @__PURE__ */ (0, import_jsx_runtime39.jsxs)(import_jsx_runtime39.Fragment, { children: [
2832
- !isLoading && /* @__PURE__ */ (0, import_jsx_runtime39.jsx)(
3040
+ const actions = /* @__PURE__ */ (0, import_jsx_runtime40.jsxs)(import_jsx_runtime40.Fragment, { children: [
3041
+ !isLoading && /* @__PURE__ */ (0, import_jsx_runtime40.jsx)(
2833
3042
  GenreSearch,
2834
3043
  {
2835
3044
  genrePlaylists: visibleGenrePlaylists,
2836
3045
  onSelect: handleGenreSearchSelect,
2837
- selectedName: selectedGenrePlaylist?.name ?? builtInArtistPanel?.title ?? null,
2838
- onClear: builtInArtistPanel ? () => changeArtist(null) : handleGenreSearchClear,
3046
+ selectedName: selectedGenrePlaylist?.name ?? (typeof builtInPanel?.title === "string" ? builtInPanel.title : null),
3047
+ onClear: builtInSongPanel ? () => changeSong(null) : builtInArtistPanel ? () => changeArtist(null) : handleGenreSearchClear,
2839
3048
  onPrevious: handlePrevious,
2840
- onQueryChange: artistSearch ? setSearchText : void 0,
2841
- extraResults: artistExtraResults
3049
+ onQueryChange: artistSearch || songSearch ? setSearchText : void 0,
3050
+ extraGroups
2842
3051
  }
2843
3052
  ),
2844
- !isLoading && filterArtist && /* @__PURE__ */ (0, import_jsx_runtime39.jsx)("div", { className: "hidden w-full justify-center max-md:flex", children: renderArtistFilter() }),
2845
- /* @__PURE__ */ (0, import_jsx_runtime39.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: [
2846
- !isLoading && !isControlled && /* @__PURE__ */ (0, import_jsx_runtime39.jsx)(
2847
- import_ui16.SegmentedControl,
3053
+ !isLoading && filterArtist && /* @__PURE__ */ (0, import_jsx_runtime40.jsx)("div", { className: "hidden w-full justify-center max-md:flex", children: renderArtistFilter() }),
3054
+ /* @__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: [
3055
+ !isLoading && !isControlled && /* @__PURE__ */ (0, import_jsx_runtime40.jsx)(
3056
+ import_ui18.SegmentedControl,
2848
3057
  {
2849
3058
  "aria-label": "Tree view mode",
2850
3059
  value: viewMode,
@@ -2857,14 +3066,14 @@ function GenreTreeView({
2857
3066
  ]
2858
3067
  }
2859
3068
  ),
2860
- !isLoading && showDisplayOptions && /* @__PURE__ */ (0, import_jsx_runtime39.jsx)(
3069
+ !isLoading && showDisplayOptions && /* @__PURE__ */ (0, import_jsx_runtime40.jsx)(
2861
3070
  "div",
2862
3071
  {
2863
3072
  className: "flex items-center gap-1",
2864
3073
  role: "group",
2865
3074
  "aria-label": "Tree display options",
2866
- children: !isRotationControlled && viewMode !== "stacked" && viewMode !== "outline" && /* @__PURE__ */ (0, import_jsx_runtime39.jsx)(
2867
- import_ui16.Button,
3075
+ children: !isRotationControlled && viewMode !== "stacked" && viewMode !== "outline" && /* @__PURE__ */ (0, import_jsx_runtime40.jsx)(
3076
+ import_ui18.Button,
2868
3077
  {
2869
3078
  variant: allowWheelRotation ? "default" : "outline",
2870
3079
  size: "sm",
@@ -2874,8 +3083,8 @@ function GenreTreeView({
2874
3083
  )
2875
3084
  }
2876
3085
  ),
2877
- !isLoading && !readOnly && showAddRoot && /* @__PURE__ */ (0, import_jsx_runtime39.jsx)(
2878
- import_ui16.IconTextButton,
3086
+ !isLoading && !readOnly && showAddRoot && /* @__PURE__ */ (0, import_jsx_runtime40.jsx)(
3087
+ import_ui18.IconTextButton,
2879
3088
  {
2880
3089
  icon: import_lucide_react15.Plus,
2881
3090
  text: "Add root",
@@ -2884,42 +3093,34 @@ function GenreTreeView({
2884
3093
  )
2885
3094
  ] })
2886
3095
  ] });
2887
- return /* @__PURE__ */ (0, import_jsx_runtime39.jsxs)(
3096
+ return /* @__PURE__ */ (0, import_jsx_runtime40.jsxs)(
2888
3097
  "div",
2889
3098
  {
2890
- ref: rootRef,
3099
+ "data-left-panel": isPanelShown && !isCollapsed ? void 0 : "hidden",
2891
3100
  className: ["genre-tree-view relative flex flex-col h-full", placement === "regions" && REGIONS_CLASS, className2].filter(Boolean).join(" "),
2892
3101
  children: [
2893
- /* @__PURE__ */ (0, import_jsx_runtime39.jsx)("style", { children: PREVIOUS_BUTTON_CSS }),
2894
- /* @__PURE__ */ (0, import_jsx_runtime39.jsx)(
3102
+ /* @__PURE__ */ (0, import_jsx_runtime40.jsx)(
2895
3103
  "div",
2896
3104
  {
2897
3105
  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"}`,
2898
3106
  children: actions
2899
3107
  }
2900
3108
  ),
2901
- !isLoading && filterArtist && /* @__PURE__ */ (0, import_jsx_runtime39.jsx)(
3109
+ !isLoading && filterArtist && /* @__PURE__ */ (0, import_jsx_runtime40.jsx)(
2902
3110
  "div",
2903
3111
  {
2904
3112
  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"}`,
2905
3113
  children: renderArtistFilter()
2906
3114
  }
2907
3115
  ),
2908
- detailTab && /* @__PURE__ */ (0, import_jsx_runtime39.jsx)(
2909
- "div",
2910
- {
2911
- 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))]"}`,
2912
- children: detailTab
2913
- }
2914
- ),
2915
- /* @__PURE__ */ (0, import_jsx_runtime39.jsx)("div", { className: "content-container flex-1 min-h-0 flex flex-row gap-4", children: /* @__PURE__ */ (0, import_jsx_runtime39.jsxs)("div", { className: "tree-view-container relative flex-1 min-w-0 flex flex-col h-full", children: [
2916
- !isLoading && viewMode === "outline" && floatingControlsLeading && /* @__PURE__ */ (0, import_jsx_runtime39.jsx)("div", { className: "absolute bottom-3 right-3 z-10", children: floatingControlsLeading }),
2917
- isLoading ? /* @__PURE__ */ (0, import_jsx_runtime39.jsx)(import_genre_tree_view4.GenreTreeViewSkeleton, { viewMode }) : viewMode === "wheel" ? (
3116
+ /* @__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: [
3117
+ !isLoading && viewMode === "outline" && floatingControlsLeading && /* @__PURE__ */ (0, import_jsx_runtime40.jsx)("div", { className: "absolute bottom-3 right-3 z-10", children: floatingControlsLeading }),
3118
+ isLoading ? /* @__PURE__ */ (0, import_jsx_runtime40.jsx)(import_genre_tree_view4.GenreTreeViewSkeleton, { viewMode }) : viewMode === "wheel" ? (
2918
3119
  // The info panel's top edge aligns with the tree's; the search bar floats `--layout-panel-inset` below it,
2919
3120
  // and the header inset (the bar's height plus that inset) extends the header's fill behind the bar,
2920
3121
  // Google Maps style; the 8px bottom padding mirrors the inset above the bar, and the hidden title's height is zeroed.
2921
3122
  // The panel's own Zoom in button is hidden: the search bar carries it.
2922
- /* @__PURE__ */ (0, import_jsx_runtime39.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_runtime39.jsx)(GenreTreeWheelHandoff, { skeleton: /* @__PURE__ */ (0, import_jsx_runtime39.jsx)(import_genre_tree_view4.GenreTreeWheelSkeleton, {}), onRevealedChange: setIsWheelRevealed, children: /* @__PURE__ */ (0, import_jsx_runtime39.jsx)(
3123
+ /* @__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)(
2923
3124
  GenrePlaylistTreeWheel,
2924
3125
  {
2925
3126
  scope,
@@ -2928,29 +3129,26 @@ function GenreTreeView({
2928
3129
  handleGenreRenameAction,
2929
3130
  additionalActions,
2930
3131
  onSelectedNodeChange: handleSelectedNodeChange,
2931
- hideInfoPanelClose: true,
2932
- hideInfoPanelTitle: true,
2933
3132
  onNodeHover: handleNodeHover,
2934
- renderInfoPanelBody,
3133
+ ...hostPanel,
2935
3134
  playback,
2936
3135
  selectedNodeId,
2937
3136
  readOnly,
2938
3137
  allowWheelRotation,
2939
3138
  sizeMetric: isArtistFiltered || !canSizeByViews ? "songs" : deferredSizeMetric,
2940
3139
  busy,
2941
- floatingControlsLeading: floatingControlsLeading || canSizeByViews ? /* @__PURE__ */ (0, import_jsx_runtime39.jsxs)("div", { className: "flex items-center gap-2", children: [
3140
+ floatingControlsLeading: floatingControlsLeading || canSizeByViews ? /* @__PURE__ */ (0, import_jsx_runtime40.jsxs)("div", { className: "flex items-center gap-2", children: [
2942
3141
  floatingControlsLeading,
2943
- canSizeByViews && /* @__PURE__ */ (0, import_jsx_runtime39.jsx)(SizeMetricToggle, { value: sizeMetric, onChange: changeSizeMetric })
3142
+ canSizeByViews && /* @__PURE__ */ (0, import_jsx_runtime40.jsx)(SizeMetricToggle, { value: sizeMetric, onChange: changeSizeMetric })
2944
3143
  ] }) : void 0,
2945
3144
  defaultViewport,
2946
3145
  viewportRestore,
2947
- onViewportChange,
2948
- ...infoPanelCuration
3146
+ onViewportChange
2949
3147
  }
2950
3148
  ) }) })
2951
3149
  ) : viewMode === "pop-core" ? (
2952
3150
  // Same as the wheel: the header inset extends its fill behind the floating search bar, Google Maps style.
2953
- /* @__PURE__ */ (0, import_jsx_runtime39.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_runtime39.jsx)(GenreTreeWheelHandoff, { skeleton: /* @__PURE__ */ (0, import_jsx_runtime39.jsx)(import_genre_tree_view4.GenreTreeWheelSkeleton, {}), onRevealedChange: setIsWheelRevealed, children: /* @__PURE__ */ (0, import_jsx_runtime39.jsx)(
3151
+ /* @__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)(
2954
3152
  GenrePlaylistTreeWheelRadialPopCore,
2955
3153
  {
2956
3154
  scope,
@@ -2959,28 +3157,25 @@ function GenreTreeView({
2959
3157
  handleGenreRenameAction,
2960
3158
  additionalActions,
2961
3159
  onSelectedNodeChange: handleSelectedNodeChange,
2962
- hideInfoPanelClose: true,
2963
- hideInfoPanelTitle: true,
2964
3160
  onNodeHover: handleNodeHover,
2965
- renderInfoPanelBody,
3161
+ ...hostPanel,
2966
3162
  playback,
2967
3163
  selectedNodeId,
2968
3164
  readOnly,
2969
3165
  allowWheelRotation,
2970
3166
  sizeMetric: isArtistFiltered || !canSizeByViews ? "songs" : deferredSizeMetric,
2971
3167
  busy,
2972
- floatingControlsLeading: floatingControlsLeading || canSizeByViews ? /* @__PURE__ */ (0, import_jsx_runtime39.jsxs)("div", { className: "flex items-center gap-2", children: [
3168
+ floatingControlsLeading: floatingControlsLeading || canSizeByViews ? /* @__PURE__ */ (0, import_jsx_runtime40.jsxs)("div", { className: "flex items-center gap-2", children: [
2973
3169
  floatingControlsLeading,
2974
- canSizeByViews && /* @__PURE__ */ (0, import_jsx_runtime39.jsx)(SizeMetricToggle, { value: sizeMetric, onChange: changeSizeMetric })
3170
+ canSizeByViews && /* @__PURE__ */ (0, import_jsx_runtime40.jsx)(SizeMetricToggle, { value: sizeMetric, onChange: changeSizeMetric })
2975
3171
  ] }) : void 0,
2976
3172
  defaultViewport,
2977
3173
  viewportRestore,
2978
3174
  onViewportChange,
2979
- ...infoPanelCuration,
2980
3175
  ...focus
2981
3176
  }
2982
3177
  ) }) })
2983
- ) : viewMode === "outline" ? /* @__PURE__ */ (0, import_jsx_runtime39.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_runtime39.jsx)(
3178
+ ) : 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)(
2984
3179
  GenrePlaylistTreeWheelRadialPopCore,
2985
3180
  {
2986
3181
  outline: true,
@@ -2990,22 +3185,19 @@ function GenreTreeView({
2990
3185
  handleGenreRenameAction,
2991
3186
  additionalActions,
2992
3187
  onSelectedNodeChange: handleSelectedNodeChange,
2993
- hideInfoPanelClose: true,
2994
- hideInfoPanelTitle: true,
2995
3188
  onNodeHover: handleNodeHover,
2996
- renderInfoPanelBody,
3189
+ ...hostPanel,
2997
3190
  playback,
2998
3191
  selectedNodeId,
2999
- readOnly,
3000
- ...infoPanelCuration
3192
+ readOnly
3001
3193
  }
3002
- ) }) : /* @__PURE__ */ (0, import_jsx_runtime39.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(
3003
- ([uuid, genrePlaylistTreePerRoot], rootIndex) => {
3004
- return /* @__PURE__ */ (0, import_jsx_runtime39.jsx)(
3194
+ ) }) : /* @__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(
3195
+ ([uuid, genrePlaylistTreePerRoot]) => {
3196
+ return /* @__PURE__ */ (0, import_jsx_runtime40.jsx)(
3005
3197
  "div",
3006
3198
  {
3007
3199
  className: "tree-per-root-container relative shrink-0 h-[28rem] mt-2 mr-16 p-2 bg-[var(--color-neutral-100)] rounded-lg",
3008
- children: /* @__PURE__ */ (0, import_jsx_runtime39.jsx)("div", { className: "graph-container relative z-10 h-full", children: /* @__PURE__ */ (0, import_jsx_runtime39.jsx)(
3200
+ children: /* @__PURE__ */ (0, import_jsx_runtime40.jsx)("div", { className: "graph-container relative z-10 h-full", children: /* @__PURE__ */ (0, import_jsx_runtime40.jsx)(
3009
3201
  GenrePlaylistTreePerRoot,
3010
3202
  {
3011
3203
  scope,
@@ -3015,26 +3207,45 @@ function GenreTreeView({
3015
3207
  handleGenreRenameAction,
3016
3208
  additionalActions,
3017
3209
  onSelectedNodeChange: handleSelectedNodeChange,
3018
- hideInfoPanelClose: true,
3019
- hideInfoPanelTitle: true,
3020
3210
  onNodeHover: handleNodeHover,
3021
- renderInfoPanelBody,
3211
+ ...hostPanel,
3022
3212
  playback,
3023
3213
  selectedNodeId: uuid === selectedGenrePlaylist?.root.uuid ? selectedNodeId : null,
3024
3214
  readOnly,
3025
3215
  wheelZoom: "modifier",
3026
3216
  defaultViewport: uuid === selectedGenrePlaylist?.root.uuid ? defaultViewport : void 0,
3027
3217
  viewportRestore: uuid === selectedGenrePlaylist?.root.uuid && uuid === restoreRootUuid ? viewportRestore : void 0,
3028
- onViewportChange: uuid === selectedGenrePlaylist?.root.uuid ? onViewportChange : void 0,
3029
- ...infoPanelCuration,
3030
- overlayPanel: rootIndex === 0 ? activeOverlayPanel : void 0
3218
+ onViewportChange: uuid === selectedGenrePlaylist?.root.uuid ? onViewportChange : void 0
3031
3219
  }
3032
3220
  ) })
3033
3221
  },
3034
3222
  uuid
3035
3223
  );
3036
3224
  }
3037
- ) })
3225
+ ) }),
3226
+ /* @__PURE__ */ (0, import_jsx_runtime40.jsx)(
3227
+ import_ui18.DetailPanel,
3228
+ {
3229
+ ref: panelRef,
3230
+ side: "left",
3231
+ title: activePanel?.title ?? "",
3232
+ hideTitle: activePanel?.compact,
3233
+ hideClose: activePanel?.compact,
3234
+ headerAccent: activePanel?.headerAccent,
3235
+ headerActions: expandButton,
3236
+ onClose: activePanel?.onClose ?? noop,
3237
+ collapsed: isCollapsed || !isTreeReady || !activePanel,
3238
+ expanded: activePanel?.expanded,
3239
+ onExpandedChange: activePanel?.onExpandedChange,
3240
+ aside: activePanel?.aside,
3241
+ edgeTab: detailTab,
3242
+ headerClassName: activePanel?.compact ? "pt-[calc(var(--layout-search-h)+var(--layout-panel-inset)+0.5rem)]" : void 0,
3243
+ children: activePanel?.aside && !activePanel.expanded ? /* @__PURE__ */ (0, import_jsx_runtime40.jsxs)(import_jsx_runtime40.Fragment, { children: [
3244
+ activePanel.content,
3245
+ activePanel.aside
3246
+ ] }) : activePanel?.content
3247
+ }
3248
+ )
3038
3249
  ] }) })
3039
3250
  ]
3040
3251
  }
@@ -3042,12 +3253,12 @@ function GenreTreeView({
3042
3253
  }
3043
3254
 
3044
3255
  // src/genre-tree/GenreDetailPanel.tsx
3045
- var import_react16 = require("react");
3256
+ var import_react17 = require("react");
3046
3257
  var import_genre_tree_view5 = require("@behindthemusictree/genre-tree-view");
3047
- var import_ui17 = require("@behindthemusictree/ui");
3048
- var import_genre_tree18 = require("@behindthemusictree/app-kit/genre-tree");
3049
- var import_jsx_runtime40 = require("react/jsx-runtime");
3050
- var ITEM_COUNT_LABEL = "Songs";
3258
+ var import_ui19 = require("@behindthemusictree/ui");
3259
+ var import_lucide_react17 = require("lucide-react");
3260
+ var import_genre_tree20 = require("@behindthemusictree/app-kit/genre-tree");
3261
+ var import_jsx_runtime41 = require("react/jsx-runtime");
3051
3262
  var RELATIVE_ITEM_COUNT = 1;
3052
3263
  function toNode(genre, parentId) {
3053
3264
  return {
@@ -3071,11 +3282,12 @@ function GenreDetailPanel({
3071
3282
  collapsed,
3072
3283
  expanded,
3073
3284
  onExpandedChange,
3285
+ edgeTab,
3074
3286
  extras
3075
3287
  }) {
3076
- const { data: detail, isPending, isError } = (0, import_genre_tree18.useFetchGenreDetail)(genreUuid, scope, getBackendBaseUrl);
3288
+ const { data: detail, isPending, isError } = (0, import_genre_tree20.useFetchGenreDetail)(genreUuid, scope, getBackendBaseUrl);
3077
3289
  const playback = useGenrePlaylistPlayback({ scope, genrePlaylists: [] });
3078
- const view = (0, import_react16.useMemo)(() => {
3290
+ const view = (0, import_react17.useMemo)(() => {
3079
3291
  if (!detail) return null;
3080
3292
  const rootColor = (0, import_genre_tree_view5.getGenreTreeColor)(detail.root.wikidataId);
3081
3293
  const relative = (genre, parentId) => ({
@@ -3083,7 +3295,7 @@ function GenreDetailPanel({
3083
3295
  fill: (0, import_genre_tree_view5.tintSurface)(rootColor),
3084
3296
  textColor: import_genre_tree_view5.TEXT_COLOR
3085
3297
  });
3086
- const ancestors = [...detail.ascendants].sort((a, b) => b.degree - a.degree);
3298
+ const ancestors2 = [...detail.ascendants].sort((a, b) => b.degree - a.degree);
3087
3299
  return {
3088
3300
  node: {
3089
3301
  id: detail.uuid,
@@ -3095,57 +3307,72 @@ function GenreDetailPanel({
3095
3307
  fill: rootColor,
3096
3308
  parentNode: detail.parent ? relative(detail.parent, null) : null,
3097
3309
  childNodes: detail.children.map((child) => relative(child, detail.uuid)),
3098
- ancestorNodes: ancestors.map(
3099
- ({ ascendant }, index) => relative(ascendant, index === 0 ? null : ancestors[index - 1].ascendant.uuid)
3310
+ ancestorNodes: ancestors2.map(
3311
+ ({ ascendant }, index) => relative(ascendant, index === 0 ? null : ancestors2[index - 1].ascendant.uuid)
3100
3312
  )
3101
3313
  };
3102
3314
  }, [detail]);
3103
3315
  if (isError) {
3104
- return /* @__PURE__ */ (0, import_jsx_runtime40.jsx)("div", { role: "alert", className: "p-4 text-sm text-[var(--color-text)]", children: "This genre could not be loaded." });
3316
+ 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." });
3105
3317
  }
3106
3318
  if (isPending || !detail || !view) {
3107
- return /* @__PURE__ */ (0, import_jsx_runtime40.jsxs)("div", { className: "flex flex-col gap-3 p-4", "aria-busy": "true", "data-testid": "genre-detail-panel-skeleton", children: [
3108
- /* @__PURE__ */ (0, import_jsx_runtime40.jsx)(import_ui17.Skeleton, { className: "h-6 w-2/3" }),
3109
- /* @__PURE__ */ (0, import_jsx_runtime40.jsx)(import_ui17.Skeleton, { className: "h-4 w-full" }),
3110
- /* @__PURE__ */ (0, import_jsx_runtime40.jsx)(import_ui17.Skeleton, { className: "h-4 w-3/4" })
3319
+ 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: [
3320
+ /* @__PURE__ */ (0, import_jsx_runtime41.jsx)(import_ui19.Skeleton, { className: "h-6 w-2/3" }),
3321
+ /* @__PURE__ */ (0, import_jsx_runtime41.jsx)(import_ui19.Skeleton, { className: "h-4 w-full" }),
3322
+ /* @__PURE__ */ (0, import_jsx_runtime41.jsx)(import_ui19.Skeleton, { className: "h-4 w-3/4" })
3111
3323
  ] });
3112
3324
  }
3113
- const renderBody = (node, sections) => ({
3114
- primary: /* @__PURE__ */ (0, import_jsx_runtime40.jsx)(
3115
- GenreInfoPanelBody,
3116
- {
3117
- toolbar: sections.toolbar,
3118
- family: sections.family,
3119
- genreUuid: detail.uuid,
3120
- genrePlaylist: { ...detail.criteriaPlaylist, tracksCount: detail.tracksCount },
3121
- wikidataId: detail.wikidataId,
3122
- scope,
3123
- getBackendBaseUrl,
3124
- playback,
3125
- band: { node, onZoomIn, shareUrl, shareTarget: shareUrl ? shareTarget : void 0 },
3126
- extras
3127
- }
3128
- ),
3129
- aside: null
3130
- });
3131
- return /* @__PURE__ */ (0, import_jsx_runtime40.jsx)(
3132
- import_genre_tree_view5.InfoPanel,
3325
+ const selectGenre = (nodeId) => onSelectGenre(nodeId);
3326
+ const family = /* @__PURE__ */ (0, import_jsx_runtime41.jsx)(
3327
+ import_genre_tree_view5.GenreNodeFamily,
3133
3328
  {
3134
3329
  node: view.node,
3135
3330
  fill: view.fill,
3136
3331
  textColor: import_genre_tree_view5.ACCENT_TEXT_COLOR,
3137
3332
  parentNode: view.parentNode,
3138
3333
  childNodes: view.childNodes,
3139
- ancestorNodes: view.ancestorNodes,
3334
+ onSelectNode: selectGenre
3335
+ }
3336
+ );
3337
+ const ancestors = /* @__PURE__ */ (0, import_jsx_runtime41.jsx)(import_genre_tree_view5.GenreNodeAncestors, { ancestorNodes: view.ancestorNodes, onSelectNode: selectGenre });
3338
+ return /* @__PURE__ */ (0, import_jsx_runtime41.jsxs)(
3339
+ import_ui19.DetailPanel,
3340
+ {
3140
3341
  side: "left",
3342
+ title: view.node.name,
3343
+ headerAccent: { background: view.fill, color: import_genre_tree_view5.ACCENT_TEXT_COLOR },
3344
+ headerActions: onExpandedChange ? /* @__PURE__ */ (0, import_jsx_runtime41.jsx)(
3345
+ import_ui19.IconButton,
3346
+ {
3347
+ "aria-label": expanded ? "Shrink panel" : "Extend panel",
3348
+ style: { color: "inherit" },
3349
+ onClick: () => onExpandedChange(!expanded),
3350
+ 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" })
3351
+ }
3352
+ ) : void 0,
3141
3353
  onClose,
3142
- onSelectNode: (nodeId) => onSelectGenre(nodeId),
3143
- itemCountLabel: ITEM_COUNT_LABEL,
3144
- renderInfoPanelBody: renderBody,
3354
+ collapsed,
3145
3355
  expanded,
3146
3356
  onExpandedChange,
3147
- collapsed,
3148
- hideExpandButton: onExpandedChange === void 0
3357
+ edgeTab,
3358
+ aside: ancestors,
3359
+ children: [
3360
+ /* @__PURE__ */ (0, import_jsx_runtime41.jsx)(
3361
+ GenreInfoPanelBody,
3362
+ {
3363
+ family,
3364
+ genreUuid: detail.uuid,
3365
+ genrePlaylist: { ...detail.criteriaPlaylist, tracksCount: detail.tracksCount },
3366
+ wikidataId: detail.wikidataId,
3367
+ scope,
3368
+ getBackendBaseUrl,
3369
+ playback,
3370
+ band: { node: view.node, onZoomIn, shareUrl, shareTarget: shareUrl ? shareTarget : void 0 },
3371
+ extras
3372
+ }
3373
+ ),
3374
+ !expanded && ancestors
3375
+ ]
3149
3376
  }
3150
3377
  );
3151
3378
  }
@@ -3154,13 +3381,13 @@ function GenreDetailPanel({
3154
3381
  var import_genre_tree_view6 = require("@behindthemusictree/genre-tree-view");
3155
3382
 
3156
3383
  // src/genre-tree/track-list-sidebar/TrackListSidebar.tsx
3157
- var import_react17 = require("react");
3158
- var import_lucide_react16 = require("lucide-react");
3159
- var import_genre_tree19 = require("@behindthemusictree/app-kit/genre-tree");
3160
- var import_genre_tree20 = require("@behindthemusictree/app-kit/genre-tree");
3161
- var import_track_noun7 = require("@behindthemusictree/app-kit/track-noun");
3162
- var import_jsx_runtime41 = require("react/jsx-runtime");
3163
- var useBrowserLayoutEffect = typeof window === "undefined" ? import_react17.useEffect : import_react17.useLayoutEffect;
3384
+ var import_react18 = require("react");
3385
+ var import_lucide_react18 = require("lucide-react");
3386
+ var import_genre_tree21 = require("@behindthemusictree/app-kit/genre-tree");
3387
+ var import_genre_tree22 = require("@behindthemusictree/app-kit/genre-tree");
3388
+ var import_track_noun4 = require("@behindthemusictree/app-kit/track-noun");
3389
+ var import_jsx_runtime42 = require("react/jsx-runtime");
3390
+ var useBrowserLayoutEffect = typeof window === "undefined" ? import_react18.useEffect : import_react18.useLayoutEffect;
3164
3391
  function TrackListSidebar({
3165
3392
  className: className2 = "",
3166
3393
  renderDuration,
@@ -3169,20 +3396,21 @@ function TrackListSidebar({
3169
3396
  isCollapsed = false,
3170
3397
  onExpand,
3171
3398
  onMinimize,
3172
- renderArtistLink
3399
+ renderArtistLink,
3400
+ renderSongLink
3173
3401
  }) {
3174
- const { trackList, loadMore, autoAdvanceScrollRequest } = (0, import_genre_tree19.useTrackList)();
3175
- const trackNoun = (0, import_track_noun7.useTrackNoun)();
3176
- const trackListRef = (0, import_react17.useRef)(null);
3177
- const previousOriginRef = (0, import_react17.useRef)(trackList?.origin ?? null);
3178
- const handledAutoAdvanceRequestIdRef = (0, import_react17.useRef)(null);
3179
- const shouldFollowPlaybackRef = (0, import_react17.useRef)(true);
3180
- const programmaticScrollTopRef = (0, import_react17.useRef)(null);
3181
- const userScrollIntentRef = (0, import_react17.useRef)(false);
3182
- const userScrollIntentTimerRef = (0, import_react17.useRef)(null);
3183
- const loadMoreSentinelRef = (0, import_react17.useRef)(null);
3402
+ const { trackList, loadMore, autoAdvanceScrollRequest } = (0, import_genre_tree21.useTrackList)();
3403
+ const trackNoun = (0, import_track_noun4.useTrackNoun)();
3404
+ const trackListRef = (0, import_react18.useRef)(null);
3405
+ const previousOriginRef = (0, import_react18.useRef)(trackList?.origin ?? null);
3406
+ const handledAutoAdvanceRequestIdRef = (0, import_react18.useRef)(null);
3407
+ const shouldFollowPlaybackRef = (0, import_react18.useRef)(true);
3408
+ const programmaticScrollTopRef = (0, import_react18.useRef)(null);
3409
+ const userScrollIntentRef = (0, import_react18.useRef)(false);
3410
+ const userScrollIntentTimerRef = (0, import_react18.useRef)(null);
3411
+ const loadMoreSentinelRef = (0, import_react18.useRef)(null);
3184
3412
  const hasMore = !!trackList && trackList.nextPage !== null;
3185
- (0, import_react17.useEffect)(() => {
3413
+ (0, import_react18.useEffect)(() => {
3186
3414
  const sentinel = loadMoreSentinelRef.current;
3187
3415
  if (!hasMore || !sentinel) return;
3188
3416
  const observer = new IntersectionObserver((entries) => {
@@ -3202,11 +3430,11 @@ function TrackListSidebar({
3202
3430
  programmaticScrollTopRef.current = null;
3203
3431
  if (trackListRef.current) trackListRef.current.scrollTop = 0;
3204
3432
  }, [trackList?.origin]);
3205
- (0, import_react17.useEffect)(() => {
3433
+ (0, import_react18.useEffect)(() => {
3206
3434
  const list = trackListRef.current;
3207
3435
  if (!isCollapsed && list?.scrollTop === 0) shouldFollowPlaybackRef.current = true;
3208
3436
  }, [isCollapsed]);
3209
- (0, import_react17.useEffect)(
3437
+ (0, import_react18.useEffect)(
3210
3438
  () => () => {
3211
3439
  if (userScrollIntentTimerRef.current !== null) clearTimeout(userScrollIntentTimerRef.current);
3212
3440
  },
@@ -3260,25 +3488,25 @@ function TrackListSidebar({
3260
3488
  }
3261
3489
  };
3262
3490
  const positionClasses = layout === "inline" ? "relative w-full flex h-full flex-col" : "fixed right-0 w-[50rem] flex flex-col";
3263
- return trackList ? /* @__PURE__ */ (0, import_jsx_runtime41.jsxs)(
3491
+ return trackList ? /* @__PURE__ */ (0, import_jsx_runtime42.jsxs)(
3264
3492
  "div",
3265
3493
  {
3266
3494
  "data-theme": "dark",
3267
3495
  className: `track-list-sidebar ${positionClasses} rounded-2xl bg-[var(--color-surface)] pb-1 ${className2}`,
3268
3496
  style: layout === "fixed" ? { top: "63.5px", maxHeight: "calc(100vh - 63.5px - 79px)" } : void 0,
3269
3497
  children: [
3270
- /* @__PURE__ */ (0, import_jsx_runtime41.jsxs)("div", { className: "header flex h-16 items-center px-4 py-2 text-[var(--color-text)]", children: [
3271
- /* @__PURE__ */ (0, import_jsx_runtime41.jsxs)("div", { className: "min-w-0 flex flex-1 flex-col justify-center", children: [
3272
- /* @__PURE__ */ (0, import_jsx_runtime41.jsxs)("div", { className: "origin flex text-xl", children: [
3273
- /* @__PURE__ */ (0, import_jsx_runtime41.jsx)("div", { className: "from h-auto flex flex-col justify-center items-center mr-2", children: "From" }),
3274
- /* @__PURE__ */ (0, import_jsx_runtime41.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_runtime41.jsx)("div", { className: "name max-w-full truncate", children: trackList.origin.label }) })
3498
+ /* @__PURE__ */ (0, import_jsx_runtime42.jsxs)("div", { className: "header flex h-16 items-center px-4 py-2 text-[var(--color-text)]", children: [
3499
+ /* @__PURE__ */ (0, import_jsx_runtime42.jsxs)("div", { className: "min-w-0 flex flex-1 flex-col justify-center", children: [
3500
+ /* @__PURE__ */ (0, import_jsx_runtime42.jsxs)("div", { className: "origin flex text-xl", children: [
3501
+ /* @__PURE__ */ (0, import_jsx_runtime42.jsx)("div", { className: "from h-auto flex flex-col justify-center items-center mr-2", children: "From" }),
3502
+ /* @__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 }) })
3275
3503
  ] }),
3276
- /* @__PURE__ */ (0, import_jsx_runtime41.jsxs)("div", { className: "info flex items-center text-sm text-[var(--color-text-muted)]", children: [
3277
- trackList.origin.type === import_genre_tree20.TrackListOriginType.GENRE_PLAYLIST ? "Genre \u2022 " : trackList.origin.type === import_genre_tree20.TrackListOriginType.ARTIST ? "Artist \u2022 " : `${trackNoun.singular} playlist \u2022 `,
3504
+ /* @__PURE__ */ (0, import_jsx_runtime42.jsxs)("div", { className: "info flex items-center text-sm text-[var(--color-text-muted)]", children: [
3505
+ 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 `,
3278
3506
  `${trackList.total} ${trackList.total > 1 ? trackNoun.plural : trackNoun.singular}`
3279
3507
  ] })
3280
3508
  ] }),
3281
- (isCollapsed && onExpand || !isCollapsed && onMinimize) && /* @__PURE__ */ (0, import_jsx_runtime41.jsx)(
3509
+ (isCollapsed && onExpand || !isCollapsed && onMinimize) && /* @__PURE__ */ (0, import_jsx_runtime42.jsx)(
3282
3510
  "button",
3283
3511
  {
3284
3512
  type: "button",
@@ -3287,11 +3515,11 @@ function TrackListSidebar({
3287
3515
  "aria-expanded": !isCollapsed,
3288
3516
  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"}`,
3289
3517
  onClick: isCollapsed ? onExpand : onMinimize,
3290
- children: /* @__PURE__ */ (0, import_jsx_runtime41.jsx)(import_lucide_react16.ListMusic, { "aria-hidden": "true", className: "h-5 w-5" })
3518
+ children: /* @__PURE__ */ (0, import_jsx_runtime42.jsx)(import_lucide_react18.ListMusic, { "aria-hidden": "true", className: "h-5 w-5" })
3291
3519
  }
3292
3520
  )
3293
3521
  ] }),
3294
- !isCollapsed && /* @__PURE__ */ (0, import_jsx_runtime41.jsxs)(
3522
+ !isCollapsed && /* @__PURE__ */ (0, import_jsx_runtime42.jsxs)(
3295
3523
  "ul",
3296
3524
  {
3297
3525
  ref: trackListRef,
@@ -3301,17 +3529,18 @@ function TrackListSidebar({
3301
3529
  onPointerDown: markUserScrollIntent,
3302
3530
  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",
3303
3531
  children: [
3304
- trackList.tracks.map((track, index) => /* @__PURE__ */ (0, import_jsx_runtime41.jsx)("li", { "data-track-uuid": track.uuid, children: /* @__PURE__ */ (0, import_jsx_runtime41.jsx)(
3532
+ trackList.tracks.map((track, index) => /* @__PURE__ */ (0, import_jsx_runtime42.jsx)("li", { "data-track-uuid": track.uuid, children: /* @__PURE__ */ (0, import_jsx_runtime42.jsx)(
3305
3533
  TrackItem,
3306
3534
  {
3307
3535
  track,
3308
3536
  position: index + 1,
3309
3537
  renderDuration,
3310
3538
  renderActions,
3311
- renderArtistLink
3539
+ renderArtistLink,
3540
+ renderSongLink
3312
3541
  }
3313
3542
  ) }, track.uuid)),
3314
- hasMore && /* @__PURE__ */ (0, import_jsx_runtime41.jsx)("li", { ref: loadMoreSentinelRef, "aria-hidden": "true", className: "h-px" })
3543
+ hasMore && /* @__PURE__ */ (0, import_jsx_runtime42.jsx)("li", { ref: loadMoreSentinelRef, "aria-hidden": "true", className: "h-px" })
3315
3544
  ]
3316
3545
  }
3317
3546
  )
@@ -3321,16 +3550,17 @@ function TrackListSidebar({
3321
3550
  }
3322
3551
 
3323
3552
  // src/genre-tree/PlayerPanel.tsx
3324
- var import_react18 = require("react");
3325
- var import_tailwind_merge2 = require("tailwind-merge");
3326
- var import_player12 = require("@behindthemusictree/app-kit/player");
3327
- var import_genre_tree22 = require("@behindthemusictree/app-kit/genre-tree");
3553
+ var import_react19 = require("react");
3554
+ var import_tailwind_merge = require("tailwind-merge");
3555
+ var import_player13 = require("@behindthemusictree/app-kit/player");
3556
+ var import_genre_tree24 = require("@behindthemusictree/app-kit/genre-tree");
3557
+ var import_ui20 = require("@behindthemusictree/ui");
3328
3558
 
3329
3559
  // src/genre-tree/PlayerControls.tsx
3330
- var import_lucide_react17 = require("lucide-react");
3331
- var import_genre_tree21 = require("@behindthemusictree/app-kit/genre-tree");
3332
- var import_player11 = require("@behindthemusictree/app-kit/player");
3333
- var import_jsx_runtime42 = require("react/jsx-runtime");
3560
+ var import_lucide_react19 = require("lucide-react");
3561
+ var import_genre_tree23 = require("@behindthemusictree/app-kit/genre-tree");
3562
+ var import_player12 = require("@behindthemusictree/app-kit/player");
3563
+ var import_jsx_runtime43 = require("react/jsx-runtime");
3334
3564
  function PlayerControls() {
3335
3565
  const {
3336
3566
  duration,
@@ -3341,23 +3571,23 @@ function PlayerControls() {
3341
3571
  seekTo,
3342
3572
  setVolume,
3343
3573
  volume
3344
- } = (0, import_player11.usePlayer)();
3345
- const { hasNextTrack, hasPreviousTrack, playNextTrack, playPreviousTrack } = (0, import_genre_tree21.useTrackList)();
3346
- const currentTime = (0, import_player11.useCurrentTime)();
3347
- const isBusy = isLoading || playState === import_player11.PlayStates.LOADING;
3574
+ } = (0, import_player12.usePlayer)();
3575
+ const { hasNextTrack, hasPreviousTrack, playNextTrack, playPreviousTrack } = (0, import_genre_tree23.useTrackList)();
3576
+ const currentTime = (0, import_player12.useCurrentTime)();
3577
+ const isBusy = isLoading || playState === import_player12.PlayStates.LOADING;
3348
3578
  const isReady = !!playerTrackObject?.isReady && !isBusy && !playerTrackObject.loadError;
3349
3579
  const safeDuration = Number.isFinite(duration) && duration > 0 ? duration : 0;
3350
3580
  const safeCurrentTime = Number.isFinite(currentTime) ? Math.max(0, Math.min(currentTime, safeDuration)) : 0;
3351
- const isPlaying = playState === import_player11.PlayStates.PLAYING;
3352
- return /* @__PURE__ */ (0, import_jsx_runtime42.jsxs)(
3581
+ const isPlaying = playState === import_player12.PlayStates.PLAYING;
3582
+ return /* @__PURE__ */ (0, import_jsx_runtime43.jsxs)(
3353
3583
  "section",
3354
3584
  {
3355
3585
  "aria-label": "Player controls",
3356
3586
  className: "player-controls flex shrink-0 flex-col gap-2 border-b border-zinc-800 px-3 py-2 text-zinc-200",
3357
3587
  children: [
3358
- /* @__PURE__ */ (0, import_jsx_runtime42.jsxs)("div", { className: "flex min-w-0 items-center gap-2 text-xs tabular-nums text-zinc-400", children: [
3359
- /* @__PURE__ */ (0, import_jsx_runtime42.jsx)("span", { "aria-label": "Elapsed time", children: (0, import_genre_tree21.formatTime)(safeCurrentTime) }),
3360
- /* @__PURE__ */ (0, import_jsx_runtime42.jsx)(
3588
+ /* @__PURE__ */ (0, import_jsx_runtime43.jsxs)("div", { className: "flex min-w-0 items-center gap-2 text-xs tabular-nums text-zinc-400", children: [
3589
+ /* @__PURE__ */ (0, import_jsx_runtime43.jsx)("span", { "aria-label": "Elapsed time", children: (0, import_genre_tree23.formatTime)(safeCurrentTime) }),
3590
+ /* @__PURE__ */ (0, import_jsx_runtime43.jsx)(
3361
3591
  "input",
3362
3592
  {
3363
3593
  "aria-label": "Seek",
@@ -3371,11 +3601,11 @@ function PlayerControls() {
3371
3601
  onChange: (event) => seekTo(Number(event.currentTarget.value))
3372
3602
  }
3373
3603
  ),
3374
- /* @__PURE__ */ (0, import_jsx_runtime42.jsx)("span", { "aria-label": "Duration", children: (0, import_genre_tree21.formatTime)(safeDuration) })
3604
+ /* @__PURE__ */ (0, import_jsx_runtime43.jsx)("span", { "aria-label": "Duration", children: (0, import_genre_tree23.formatTime)(safeDuration) })
3375
3605
  ] }),
3376
- /* @__PURE__ */ (0, import_jsx_runtime42.jsxs)("div", { className: "grid min-h-9 grid-cols-[1fr_auto_1fr] items-center gap-3", children: [
3377
- /* @__PURE__ */ (0, import_jsx_runtime42.jsxs)("div", { className: "col-start-2 flex items-center justify-center gap-2", children: [
3378
- /* @__PURE__ */ (0, import_jsx_runtime42.jsx)(
3606
+ /* @__PURE__ */ (0, import_jsx_runtime43.jsxs)("div", { className: "grid min-h-9 grid-cols-[1fr_auto_1fr] items-center gap-3", children: [
3607
+ /* @__PURE__ */ (0, import_jsx_runtime43.jsxs)("div", { className: "col-start-2 flex items-center justify-center gap-2", children: [
3608
+ /* @__PURE__ */ (0, import_jsx_runtime43.jsx)(
3379
3609
  "button",
3380
3610
  {
3381
3611
  type: "button",
@@ -3384,10 +3614,10 @@ function PlayerControls() {
3384
3614
  disabled: !isReady || !hasPreviousTrack,
3385
3615
  onClick: playPreviousTrack,
3386
3616
  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",
3387
- children: /* @__PURE__ */ (0, import_jsx_runtime42.jsx)(import_lucide_react17.SkipBack, { "aria-hidden": "true", className: "h-4 w-4" })
3617
+ children: /* @__PURE__ */ (0, import_jsx_runtime43.jsx)(import_lucide_react19.SkipBack, { "aria-hidden": "true", className: "h-4 w-4" })
3388
3618
  }
3389
3619
  ),
3390
- /* @__PURE__ */ (0, import_jsx_runtime42.jsx)(
3620
+ /* @__PURE__ */ (0, import_jsx_runtime43.jsx)(
3391
3621
  "button",
3392
3622
  {
3393
3623
  type: "button",
@@ -3396,10 +3626,10 @@ function PlayerControls() {
3396
3626
  disabled: !isReady,
3397
3627
  onClick: handlePlayPauseAction,
3398
3628
  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",
3399
- children: isPlaying ? /* @__PURE__ */ (0, import_jsx_runtime42.jsx)(import_lucide_react17.Pause, { "aria-hidden": "true", className: "h-4 w-4", fill: "currentColor" }) : /* @__PURE__ */ (0, import_jsx_runtime42.jsx)(import_lucide_react17.Play, { "aria-hidden": "true", className: "h-4 w-4", fill: "currentColor" })
3629
+ 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" })
3400
3630
  }
3401
3631
  ),
3402
- /* @__PURE__ */ (0, import_jsx_runtime42.jsx)(
3632
+ /* @__PURE__ */ (0, import_jsx_runtime43.jsx)(
3403
3633
  "button",
3404
3634
  {
3405
3635
  type: "button",
@@ -3408,14 +3638,14 @@ function PlayerControls() {
3408
3638
  disabled: !isReady || !hasNextTrack,
3409
3639
  onClick: playNextTrack,
3410
3640
  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",
3411
- children: /* @__PURE__ */ (0, import_jsx_runtime42.jsx)(import_lucide_react17.SkipForward, { "aria-hidden": "true", className: "h-4 w-4" })
3641
+ children: /* @__PURE__ */ (0, import_jsx_runtime43.jsx)(import_lucide_react19.SkipForward, { "aria-hidden": "true", className: "h-4 w-4" })
3412
3642
  }
3413
3643
  )
3414
3644
  ] }),
3415
- /* @__PURE__ */ (0, import_jsx_runtime42.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: [
3416
- volume === 0 ? /* @__PURE__ */ (0, import_jsx_runtime42.jsx)(import_lucide_react17.VolumeX, { "aria-hidden": "true", className: "h-[0.7rem] w-[0.7rem] shrink-0" }) : /* @__PURE__ */ (0, import_jsx_runtime42.jsx)(import_lucide_react17.Volume2, { "aria-hidden": "true", className: "h-[0.7rem] w-[0.7rem] shrink-0" }),
3417
- /* @__PURE__ */ (0, import_jsx_runtime42.jsx)("span", { className: "sr-only", children: "Volume" }),
3418
- /* @__PURE__ */ (0, import_jsx_runtime42.jsx)(
3645
+ /* @__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: [
3646
+ 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" }),
3647
+ /* @__PURE__ */ (0, import_jsx_runtime43.jsx)("span", { className: "sr-only", children: "Volume" }),
3648
+ /* @__PURE__ */ (0, import_jsx_runtime43.jsx)(
3419
3649
  "input",
3420
3650
  {
3421
3651
  "aria-label": "Volume",
@@ -3437,7 +3667,7 @@ function PlayerControls() {
3437
3667
  }
3438
3668
 
3439
3669
  // src/genre-tree/PlayerPanel.tsx
3440
- var import_jsx_runtime43 = require("react/jsx-runtime");
3670
+ var import_jsx_runtime44 = require("react/jsx-runtime");
3441
3671
  var PANEL_ID = "player-panel";
3442
3672
  var DEFAULT_EXTENDED_WIDTH = "calc(100vw - var(--layout-center-left) - var(--layout-gutter))";
3443
3673
  function PlayerPanel({
@@ -3446,16 +3676,17 @@ function PlayerPanel({
3446
3676
  expanded,
3447
3677
  onExpandedChange,
3448
3678
  extendedWidth = DEFAULT_EXTENDED_WIDTH,
3449
- renderArtistLink
3679
+ renderArtistLink,
3680
+ renderSongLink
3450
3681
  }) {
3451
- const { playerTrackObject } = (0, import_player12.usePlayer)();
3452
- const { toggleTrackListSidebar, isTrackListSidebarVisible } = (0, import_genre_tree22.useTrackListSidebarVisibility)();
3453
- const [lastLoadedTrackTitle, setLastLoadedTrackTitle] = (0, import_react18.useState)("");
3682
+ const { playerTrackObject } = (0, import_player13.usePlayer)();
3683
+ const { toggleTrackListSidebar, isTrackListSidebarVisible } = (0, import_genre_tree24.useTrackListSidebarVisibility)();
3684
+ const [lastLoadedTrackTitle, setLastLoadedTrackTitle] = (0, import_react19.useState)("");
3454
3685
  const loadedTrackTitle = playerTrackObject?.track.title;
3455
- (0, import_react18.useEffect)(() => {
3686
+ (0, import_react19.useEffect)(() => {
3456
3687
  if (loadedTrackTitle) setLastLoadedTrackTitle(loadedTrackTitle);
3457
3688
  }, [loadedTrackTitle]);
3458
- const [uncontrolledExtended, setUncontrolledExtended] = (0, import_react18.useState)(false);
3689
+ const [uncontrolledExtended, setUncontrolledExtended] = (0, import_react19.useState)(false);
3459
3690
  const isExtendRequested = expanded ?? uncontrolledExtended;
3460
3691
  const setIsExtendRequested = (next) => {
3461
3692
  setUncontrolledExtended(next);
@@ -3476,11 +3707,11 @@ function PlayerPanel({
3476
3707
  onClick: () => setIsExtendRequested(true),
3477
3708
  expanded: false
3478
3709
  } : null;
3479
- return /* @__PURE__ */ (0, import_jsx_runtime43.jsxs)(
3710
+ return /* @__PURE__ */ (0, import_jsx_runtime44.jsxs)(
3480
3711
  "div",
3481
3712
  {
3482
3713
  id: PANEL_ID,
3483
- className: (0, import_tailwind_merge2.twMerge)(
3714
+ className: (0, import_tailwind_merge.twMerge)(
3484
3715
  "fixed right-[var(--layout-gutter)] top-[var(--layout-band-top)] z-40 max-h-[calc(100vh-var(--layout-band-top)-3.5rem)]",
3485
3716
  !isExtended && "w-[var(--layout-right-w)]",
3486
3717
  isTrackListSidebarVisible && "h-[calc(100vh-var(--layout-band-top)-3.5rem)]",
@@ -3489,8 +3720,8 @@ function PlayerPanel({
3489
3720
  ),
3490
3721
  style: isExtended ? { width: extendedWidth } : void 0,
3491
3722
  children: [
3492
- /* @__PURE__ */ (0, import_jsx_runtime43.jsx)(
3493
- PanelEdgeTab,
3723
+ /* @__PURE__ */ (0, import_jsx_runtime44.jsx)(
3724
+ import_ui20.PanelEdgeTab,
3494
3725
  {
3495
3726
  side: "right",
3496
3727
  edge: tabEdge,
@@ -3499,34 +3730,35 @@ function PlayerPanel({
3499
3730
  className: "absolute right-full top-1/2 -translate-y-1/2"
3500
3731
  }
3501
3732
  ),
3502
- /* @__PURE__ */ (0, import_jsx_runtime43.jsxs)(
3733
+ /* @__PURE__ */ (0, import_jsx_runtime44.jsxs)(
3503
3734
  "div",
3504
3735
  {
3505
- className: (0, import_tailwind_merge2.twMerge)(
3736
+ className: (0, import_tailwind_merge.twMerge)(
3506
3737
  "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)]",
3507
3738
  isExtended ? "flex-row" : "flex-col"
3508
3739
  ),
3509
3740
  children: [
3510
- /* @__PURE__ */ (0, import_jsx_runtime43.jsxs)("div", { className: "flex w-[var(--layout-right-w)] shrink-0 flex-col", children: [
3511
- /* @__PURE__ */ (0, import_jsx_runtime43.jsx)("div", { className: "flex shrink-0 items-center justify-between gap-2 border-b border-zinc-800 p-2", children: /* @__PURE__ */ (0, import_jsx_runtime43.jsx)("span", { className: "min-w-0 truncate text-sm font-semibold", children: loadedTrackTitle || lastLoadedTrackTitle }) }),
3512
- /* @__PURE__ */ (0, import_jsx_runtime43.jsxs)("div", { className: "relative w-full shrink-0", children: [
3513
- /* @__PURE__ */ (0, import_jsx_runtime43.jsx)(import_player12.PlayerVideoSurface, { className: "aspect-video w-full bg-black" }),
3514
- playerTrackObject?.loadError && /* @__PURE__ */ (0, import_jsx_runtime43.jsx)("span", { className: "absolute bottom-2 left-2 right-2 text-xs text-red-400", children: playerTrackObject.loadError })
3741
+ /* @__PURE__ */ (0, import_jsx_runtime44.jsxs)("div", { className: "flex w-[var(--layout-right-w)] shrink-0 flex-col", children: [
3742
+ /* @__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 }) }),
3743
+ /* @__PURE__ */ (0, import_jsx_runtime44.jsxs)("div", { className: "relative w-full shrink-0", children: [
3744
+ /* @__PURE__ */ (0, import_jsx_runtime44.jsx)(import_player13.PlayerVideoSurface, { className: "aspect-video w-full bg-black" }),
3745
+ 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 })
3515
3746
  ] }),
3516
- /* @__PURE__ */ (0, import_jsx_runtime43.jsx)(PlayerControls, {})
3747
+ /* @__PURE__ */ (0, import_jsx_runtime44.jsx)(PlayerControls, {})
3517
3748
  ] }),
3518
- /* @__PURE__ */ (0, import_jsx_runtime43.jsx)(
3749
+ /* @__PURE__ */ (0, import_jsx_runtime44.jsx)(
3519
3750
  TrackListSidebar,
3520
3751
  {
3521
3752
  layout: "inline",
3522
3753
  isCollapsed: !isTrackListSidebarVisible,
3523
3754
  onExpand: toggleTrackListSidebar,
3524
3755
  onMinimize: hideSongList,
3525
- className: (0, import_tailwind_merge2.twMerge)(
3756
+ className: (0, import_tailwind_merge.twMerge)(
3526
3757
  "min-h-0 min-w-0 flex-1",
3527
3758
  isExtended && "border-l border-zinc-800"
3528
3759
  ),
3529
- renderArtistLink
3760
+ renderArtistLink,
3761
+ renderSongLink
3530
3762
  }
3531
3763
  )
3532
3764
  ]
@@ -3540,33 +3772,33 @@ function PlayerPanel({
3540
3772
  // src/genre-tree/Rating.tsx
3541
3773
  var import_uuid = require("uuid");
3542
3774
  var import_ai = require("react-icons/ai");
3543
- var import_genre_tree23 = require("@behindthemusictree/app-kit/genre-tree");
3544
- var import_jsx_runtime44 = require("react/jsx-runtime");
3775
+ var import_genre_tree25 = require("@behindthemusictree/app-kit/genre-tree");
3776
+ var import_jsx_runtime45 = require("react/jsx-runtime");
3545
3777
  function Rating({ rating, handleChange }) {
3546
3778
  const instanceId = (0, import_uuid.v4)();
3547
- return /* @__PURE__ */ (0, import_jsx_runtime44.jsx)("div", { className: "flex items-center", children: [...Array(6)].map((_, i) => /* @__PURE__ */ (0, import_jsx_runtime44.jsxs)("div", { className: `relative ${i === 0 ? "mr-4" : "mr-1"}`, children: [
3548
- /* @__PURE__ */ (0, import_jsx_runtime44.jsx)(
3779
+ 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: [
3780
+ /* @__PURE__ */ (0, import_jsx_runtime45.jsx)(
3549
3781
  "input",
3550
3782
  {
3551
3783
  id: `star-${instanceId}-${i}`,
3552
3784
  className: "w-6 h-6 opacity-0",
3553
3785
  type: "radio",
3554
3786
  name: "rating",
3555
- value: i === 0 ? import_genre_tree23.FORM_RATING_NULL_VALUE : i,
3787
+ value: i === 0 ? import_genre_tree25.FORM_RATING_NULL_VALUE : i,
3556
3788
  checked: rating === (i === 0 ? null : i),
3557
3789
  onChange: handleChange
3558
3790
  }
3559
3791
  ),
3560
- /* @__PURE__ */ (0, import_jsx_runtime44.jsx)("label", { htmlFor: `star-${instanceId}-${i}`, className: "cursor-pointer", children: i === 0 ? /* @__PURE__ */ (0, import_jsx_runtime44.jsxs)("div", { className: "flex", children: [
3561
- /* @__PURE__ */ (0, import_jsx_runtime44.jsx)(
3792
+ /* @__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: [
3793
+ /* @__PURE__ */ (0, import_jsx_runtime45.jsx)(
3562
3794
  import_ai.AiOutlineStar,
3563
3795
  {
3564
- className: `w-7 h-7 absolute top-0 left-0 ${rating === import_genre_tree23.FORM_RATING_NULL_VALUE ? "text-[var(--color-accent-rating)]" : "text-[var(--color-text-muted)]"}`
3796
+ 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)]"}`
3565
3797
  }
3566
3798
  ),
3567
- /* @__PURE__ */ (0, import_jsx_runtime44.jsx)("span", { className: "flex-grow w-8" }),
3799
+ /* @__PURE__ */ (0, import_jsx_runtime45.jsx)("span", { className: "flex-grow w-8" }),
3568
3800
  " "
3569
- ] }) : /* @__PURE__ */ (0, import_jsx_runtime44.jsx)(
3801
+ ] }) : /* @__PURE__ */ (0, import_jsx_runtime45.jsx)(
3570
3802
  import_ai.AiFillStar,
3571
3803
  {
3572
3804
  className: `w-7 h-7 absolute top-0 left-0 ${(rating ?? -1) >= i ? "text-[var(--color-accent-rating)]" : "text-[var(--color-text-muted)]"}`
@@ -3592,7 +3824,6 @@ function Rating({ rating, handleChange }) {
3592
3824
  GenreTreeWheelSkeleton,
3593
3825
  InternalErrorPopup,
3594
3826
  NetworkErrorPopup,
3595
- PanelEdgeTab,
3596
3827
  PlayerPanel,
3597
3828
  PopupButtons,
3598
3829
  PopupTitle,
@@ -3603,7 +3834,6 @@ function Rating({ rating, handleChange }) {
3603
3834
  TrackListSidebar,
3604
3835
  TrackPositionPlayPause,
3605
3836
  TrackUploadPopup,
3606
- useGenrePlaylistPlayback,
3607
- useLeftDetailPanelLayout
3837
+ useGenrePlaylistPlayback
3608
3838
  });
3609
3839
  //# sourceMappingURL=index.cjs.map