@behindthemusictree/app-kit-ui 34.0.0 → 35.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -5,14 +5,12 @@ import {
5
5
  } from "./chunk-DKQQLLMY.js";
6
6
 
7
7
  // src/genre-tree/GenreTreeView.tsx
8
- import { useCallback as useCallback6, useDeferredValue, useEffect as useEffect5, useMemo as useMemo7, useRef as useRef3, useState as useState8 } from "react";
8
+ import { useCallback as useCallback6, useDeferredValue as useDeferredValue2, useEffect as useEffect5, useMemo as useMemo8, useRef as useRef3, useState as useState9 } from "react";
9
9
  import { useIsMutating } from "@tanstack/react-query";
10
10
  import { Plus as Plus2 } from "lucide-react";
11
- import { Maximize2, Minimize2 } from "lucide-react";
12
- import { IconTextButton, IconButton, Button, SegmentedControl as SegmentedControl2, DetailPanel, useObscuredInset } from "@behindthemusictree/ui";
11
+ import { Maximize2 as Maximize22, Minimize2 as Minimize22 } from "lucide-react";
12
+ import { IconTextButton, IconButton as IconButton2, Button, SegmentedControl as SegmentedControl2, DetailPanel as DetailPanel2, useObscuredInset } from "@behindthemusictree/ui";
13
13
  import {
14
- GenreNodeAncestors,
15
- GenreNodeFamily,
16
14
  GenreTreeWheelSkeleton,
17
15
  GenreTreeViewSkeleton,
18
16
  useGenreNodeRelations
@@ -465,6 +463,9 @@ function GenreTreeWheelHandoff({ skeleton, children, onRevealedChange }) {
465
463
  ] });
466
464
  }
467
465
 
466
+ // src/genre-tree/GenreTreeSearch.tsx
467
+ import { useDeferredValue, useState as useState3 } from "react";
468
+
468
469
  // src/genre-tree/GenreSearch.tsx
469
470
  import { ArrowLeft } from "lucide-react";
470
471
  import { SEARCH_LEADING_BUTTON_CLASS } from "@behindthemusictree/ui";
@@ -670,6 +671,66 @@ function GenreSearch({
670
671
  ) });
671
672
  }
672
673
 
674
+ // src/genre-tree/GenreTreeSearch.tsx
675
+ import { jsx as jsx9 } from "react/jsx-runtime";
676
+ function GenreTreeSearch({
677
+ artistBaseUrl,
678
+ songBaseUrl,
679
+ onArtistSelect,
680
+ onSongSelect,
681
+ artistSearch,
682
+ songSearch,
683
+ SearchGroups,
684
+ onSelectGenre,
685
+ extraGroups = [],
686
+ onQueryChange,
687
+ onFocus,
688
+ ...props
689
+ }) {
690
+ const [query, setQuery] = useState3("");
691
+ const [focused, setFocused] = useState3(false);
692
+ const deferredQuery = useDeferredValue(query);
693
+ const artistGroup = useArtistSearchGroup(artistBaseUrl, deferredQuery, onArtistSelect, artistSearch);
694
+ const songGroup = useSongSearchGroup(songBaseUrl, deferredQuery, onSongSelect, songSearch);
695
+ const render = (hostGroups) => /* @__PURE__ */ jsx9(
696
+ GenreSearch,
697
+ {
698
+ ...props,
699
+ onQueryChange: (text) => {
700
+ setQuery(text);
701
+ onQueryChange?.(text);
702
+ },
703
+ onFocus: () => {
704
+ setFocused(true);
705
+ onFocus?.();
706
+ },
707
+ extraGroups: [...extraGroups, ...hostGroups, artistGroup, songGroup].filter((group) => group !== void 0)
708
+ }
709
+ );
710
+ return SearchGroups ? /* @__PURE__ */ jsx9(SearchGroups, { query: deferredQuery, focused, onSelectGenre, children: render }) : render([]);
711
+ }
712
+
713
+ // src/genre-tree/GenreDetailPanel.tsx
714
+ import { useMemo as useMemo6 } from "react";
715
+ import {
716
+ ACCENT_TEXT_COLOR,
717
+ GenreNodeAncestors,
718
+ GenreNodeFamily,
719
+ getGenreTreeColor as getGenreTreeColor2,
720
+ TEXT_COLOR,
721
+ tintSurface
722
+ } from "@behindthemusictree/genre-tree-view";
723
+ import { DetailPanel, IconButton, Skeleton as Skeleton3 } from "@behindthemusictree/ui";
724
+ import { Maximize2, Minimize2 } from "lucide-react";
725
+ import {
726
+ useFetchGenreDetail,
727
+ genreMusicCount as genreMusicCount2,
728
+ GenrePlaylistSummarySchema,
729
+ GenreCatalogueCountsSchema as GenreCatalogueCountsSchema3,
730
+ CriteriaMinimumSchema,
731
+ GenreUuidSchema
732
+ } from "@behindthemusictree/app-kit/genre-tree";
733
+
673
734
  // src/genre-tree/GenreInfoPanelBody.tsx
674
735
  import {
675
736
  genreMusicCount,
@@ -687,18 +748,18 @@ import { Share2 as Share22 } from "lucide-react";
687
748
  import { usePopup as usePopup3 } from "@behindthemusictree/app-kit/popup";
688
749
 
689
750
  // src/genre-tree/SharePopup.tsx
690
- import { useCallback as useCallback5, useEffect as useEffect2, useMemo as useMemo5, useState as useState3 } from "react";
751
+ import { useCallback as useCallback5, useEffect as useEffect2, useMemo as useMemo5, useState as useState4 } from "react";
691
752
  import { Link2, Share2 } from "lucide-react";
692
753
  import { FaWhatsapp } from "react-icons/fa";
693
754
  import { SiGmail } from "react-icons/si";
694
755
 
695
756
  // src/genre-tree/ShareDestinationButton.tsx
696
- import { Fragment as Fragment4, jsx as jsx9, jsxs as jsxs6 } from "react/jsx-runtime";
757
+ import { Fragment as Fragment4, jsx as jsx10, jsxs as jsxs6 } from "react/jsx-runtime";
697
758
  var className = "group flex min-w-0 flex-1 flex-col items-center justify-center gap-1.5 rounded-xl px-2 py-2 text-xs text-[var(--color-text)] transition-colors hover:bg-[var(--color-surface-muted)] focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[var(--color-focus-ring)] disabled:cursor-not-allowed disabled:opacity-50";
698
759
  function Contents({ icon, label }) {
699
760
  return /* @__PURE__ */ jsxs6(Fragment4, { children: [
700
- /* @__PURE__ */ jsx9("span", { className: "flex h-12 w-12 items-center justify-center rounded-full bg-[var(--color-surface-muted)] transition-colors group-hover:bg-[var(--color-border)]", children: icon }),
701
- /* @__PURE__ */ jsx9("span", { className: "max-w-full truncate", children: label })
761
+ /* @__PURE__ */ jsx10("span", { className: "flex h-12 w-12 items-center justify-center rounded-full bg-[var(--color-surface-muted)] transition-colors group-hover:bg-[var(--color-border)]", children: icon }),
762
+ /* @__PURE__ */ jsx10("span", { className: "max-w-full truncate", children: label })
702
763
  ] });
703
764
  }
704
765
  function ShareDestinationButton({
@@ -710,29 +771,29 @@ function ShareDestinationButton({
710
771
  ariaLabel
711
772
  }) {
712
773
  if (href) {
713
- return /* @__PURE__ */ jsx9("a", { href, target: "_blank", rel: "noopener noreferrer", "aria-label": ariaLabel, className, children: /* @__PURE__ */ jsx9(Contents, { icon, label }) });
774
+ return /* @__PURE__ */ jsx10("a", { href, target: "_blank", rel: "noopener noreferrer", "aria-label": ariaLabel, className, children: /* @__PURE__ */ jsx10(Contents, { icon, label }) });
714
775
  }
715
- return /* @__PURE__ */ jsx9("button", { type: "button", onClick, disabled, "aria-label": ariaLabel, className, children: /* @__PURE__ */ jsx9(Contents, { icon, label }) });
776
+ return /* @__PURE__ */ jsx10("button", { type: "button", onClick, disabled, "aria-label": ariaLabel, className, children: /* @__PURE__ */ jsx10(Contents, { icon, label }) });
716
777
  }
717
778
 
718
779
  // src/genre-tree/SharePreviewCard.tsx
719
780
  import { Music, Music2, UserRound } from "lucide-react";
720
- import { jsx as jsx10, jsxs as jsxs7 } from "react/jsx-runtime";
781
+ import { jsx as jsx11, jsxs as jsxs7 } from "react/jsx-runtime";
721
782
  function SharePreviewCard({ target, url }) {
722
783
  const Icon = { genre: Music2, artist: UserRound, song: Music }[target.kind];
723
784
  const typeLabel = { genre: "Genre", artist: "Artist", song: "Song" }[target.kind];
724
- return /* @__PURE__ */ jsx10("div", { className: "rounded-3xl bg-[var(--color-surface-muted)] p-4", children: /* @__PURE__ */ jsxs7("div", { className: "flex min-h-32 overflow-hidden rounded-2xl bg-[var(--color-surface)] shadow-lg", children: [
725
- /* @__PURE__ */ jsx10("div", { className: "flex w-28 shrink-0 items-center justify-center bg-[var(--color-primary)]/15 text-[var(--color-primary)]", children: /* @__PURE__ */ jsx10(Icon, { "aria-hidden": "true", className: "h-10 w-10" }) }),
785
+ return /* @__PURE__ */ jsx11("div", { className: "rounded-3xl bg-[var(--color-surface-muted)] p-4", children: /* @__PURE__ */ jsxs7("div", { className: "flex min-h-32 overflow-hidden rounded-2xl bg-[var(--color-surface)] shadow-lg", children: [
786
+ /* @__PURE__ */ jsx11("div", { className: "flex w-28 shrink-0 items-center justify-center bg-[var(--color-primary)]/15 text-[var(--color-primary)]", children: /* @__PURE__ */ jsx11(Icon, { "aria-hidden": "true", className: "h-10 w-10" }) }),
726
787
  /* @__PURE__ */ jsxs7("div", { className: "flex min-w-0 flex-1 flex-col justify-center gap-1 p-4", children: [
727
- /* @__PURE__ */ jsx10("span", { className: "text-xs font-medium uppercase tracking-wide text-[var(--color-text-muted)]", children: typeLabel }),
728
- /* @__PURE__ */ jsx10("span", { className: "truncate text-base font-semibold text-[var(--color-text)]", title: target.name, children: target.name }),
729
- /* @__PURE__ */ jsx10("span", { className: "truncate text-sm text-[var(--color-primary)]", title: url, children: url })
788
+ /* @__PURE__ */ jsx11("span", { className: "text-xs font-medium uppercase tracking-wide text-[var(--color-text-muted)]", children: typeLabel }),
789
+ /* @__PURE__ */ jsx11("span", { className: "truncate text-base font-semibold text-[var(--color-text)]", title: target.name, children: target.name }),
790
+ /* @__PURE__ */ jsx11("span", { className: "truncate text-sm text-[var(--color-primary)]", title: url, children: url })
730
791
  ] })
731
792
  ] }) });
732
793
  }
733
794
 
734
795
  // src/genre-tree/SharePopup.tsx
735
- import { Fragment as Fragment5, jsx as jsx11, jsxs as jsxs8 } from "react/jsx-runtime";
796
+ import { Fragment as Fragment5, jsx as jsx12, jsxs as jsxs8 } from "react/jsx-runtime";
736
797
  function resolveAbsoluteUrl(url) {
737
798
  try {
738
799
  const parsed = new URL(url, window.location.href);
@@ -765,11 +826,11 @@ async function copyText(text) {
765
826
  }
766
827
  }
767
828
  function SharePopup({ target, url, onClose }) {
768
- const [absoluteUrl, setAbsoluteUrl] = useState3(url);
769
- const [copyStatus, setCopyStatus] = useState3("idle");
770
- const [canUseNativeShare, setCanUseNativeShare] = useState3(false);
771
- const [shareError, setShareError] = useState3(null);
772
- const [toastId, setToastId] = useState3(0);
829
+ const [absoluteUrl, setAbsoluteUrl] = useState4(url);
830
+ const [copyStatus, setCopyStatus] = useState4("idle");
831
+ const [canUseNativeShare, setCanUseNativeShare] = useState4(false);
832
+ const [shareError, setShareError] = useState4(null);
833
+ const [toastId, setToastId] = useState4(0);
773
834
  useEffect2(() => {
774
835
  setAbsoluteUrl(resolveAbsoluteUrl(url) ?? "");
775
836
  setCopyStatus("idle");
@@ -808,7 +869,7 @@ function SharePopup({ target, url, onClose }) {
808
869
  }
809
870
  };
810
871
  return /* @__PURE__ */ jsxs8(Fragment5, { children: [
811
- /* @__PURE__ */ jsx11(
872
+ /* @__PURE__ */ jsx12(
812
873
  BasePopup_default,
813
874
  {
814
875
  title: "Share",
@@ -824,64 +885,64 @@ function SharePopup({ target, url, onClose }) {
824
885
  closeButtonClassName: "!h-10 !w-10 !rounded-none !border-0 !bg-transparent !p-0 !text-[var(--color-text)] hover:!bg-transparent hover:!text-[var(--color-text)]",
825
886
  contentClassName: "!rounded-none !bg-transparent !px-6 !pb-6 !pt-2",
826
887
  children: /* @__PURE__ */ jsxs8("div", { className: "flex flex-col gap-4", children: [
827
- /* @__PURE__ */ jsx11(SharePreviewCard, { target, url: absoluteUrl || url }),
888
+ /* @__PURE__ */ jsx12(SharePreviewCard, { target, url: absoluteUrl || url }),
828
889
  /* @__PURE__ */ jsxs8("div", { className: "grid grid-cols-3 gap-1 rounded-3xl bg-[var(--color-surface)] p-3 sm:grid-cols-4", children: [
829
- /* @__PURE__ */ jsx11(
890
+ /* @__PURE__ */ jsx12(
830
891
  ShareDestinationButton,
831
892
  {
832
893
  label: "Copy Link",
833
- icon: /* @__PURE__ */ jsx11(Link2, { "aria-hidden": "true", className: "h-6 w-6" }),
894
+ icon: /* @__PURE__ */ jsx12(Link2, { "aria-hidden": "true", className: "h-6 w-6" }),
834
895
  onClick: () => void handleCopy(),
835
896
  ariaLabel: "Copy link",
836
897
  disabled: !absoluteUrl
837
898
  }
838
899
  ),
839
- /* @__PURE__ */ jsx11(
900
+ /* @__PURE__ */ jsx12(
840
901
  ShareDestinationButton,
841
902
  {
842
903
  label: "WhatsApp",
843
- icon: /* @__PURE__ */ jsx11(FaWhatsapp, { "aria-hidden": "true", className: "h-9 w-9 text-[#25D366]" }),
904
+ icon: /* @__PURE__ */ jsx12(FaWhatsapp, { "aria-hidden": "true", className: "h-9 w-9 text-[#25D366]" }),
844
905
  href: destinationUrls?.whatsapp,
845
906
  ariaLabel: `Share ${target.name} via WhatsApp`,
846
907
  disabled: !absoluteUrl
847
908
  }
848
909
  ),
849
- /* @__PURE__ */ jsx11(
910
+ /* @__PURE__ */ jsx12(
850
911
  ShareDestinationButton,
851
912
  {
852
913
  label: "Gmail",
853
- icon: /* @__PURE__ */ jsx11(SiGmail, { "aria-hidden": "true", className: "h-8 w-8 text-[#EA4335]" }),
914
+ icon: /* @__PURE__ */ jsx12(SiGmail, { "aria-hidden": "true", className: "h-8 w-8 text-[#EA4335]" }),
854
915
  href: destinationUrls?.gmail,
855
916
  ariaLabel: `Email ${target.name} with Gmail`,
856
917
  disabled: !absoluteUrl
857
918
  }
858
919
  ),
859
- canUseNativeShare && /* @__PURE__ */ jsx11(
920
+ canUseNativeShare && /* @__PURE__ */ jsx12(
860
921
  ShareDestinationButton,
861
922
  {
862
923
  label: "More",
863
- icon: /* @__PURE__ */ jsx11(Share2, { "aria-hidden": "true", className: "h-6 w-6" }),
924
+ icon: /* @__PURE__ */ jsx12(Share2, { "aria-hidden": "true", className: "h-6 w-6" }),
864
925
  onClick: () => void handleNativeShare(),
865
926
  ariaLabel: "More sharing options",
866
927
  disabled: !absoluteUrl
867
928
  }
868
929
  )
869
930
  ] }),
870
- copyStatus === "failed" && /* @__PURE__ */ jsx11("p", { role: "alert", className: "text-center text-sm text-[var(--color-red-500)]", children: "Could not copy the link." }),
871
- shareError && /* @__PURE__ */ jsx11("p", { role: "alert", className: "text-center text-sm text-[var(--color-red-500)]", children: shareError })
931
+ copyStatus === "failed" && /* @__PURE__ */ jsx12("p", { role: "alert", className: "text-center text-sm text-[var(--color-red-500)]", children: "Could not copy the link." }),
932
+ shareError && /* @__PURE__ */ jsx12("p", { role: "alert", className: "text-center text-sm text-[var(--color-red-500)]", children: shareError })
872
933
  ] })
873
934
  }
874
935
  ),
875
- toastId > 0 && /* @__PURE__ */ jsx11(Toast, { message: "Link copied", onClose: dismissToast }, toastId)
936
+ toastId > 0 && /* @__PURE__ */ jsx12(Toast, { message: "Link copied", onClose: dismissToast }, toastId)
876
937
  ] });
877
938
  }
878
939
 
879
940
  // src/genre-tree/ShareActionButton.tsx
880
- import { jsx as jsx12, jsxs as jsxs9 } from "react/jsx-runtime";
941
+ import { jsx as jsx13, jsxs as jsxs9 } from "react/jsx-runtime";
881
942
  function ShareActionButton({ target, url }) {
882
943
  const { showPopup, hidePopup } = usePopup3();
883
944
  const openSharePopup = () => {
884
- showPopup(/* @__PURE__ */ jsx12(SharePopup, { target, url, onClose: hidePopup }));
945
+ showPopup(/* @__PURE__ */ jsx13(SharePopup, { target, url, onClose: hidePopup }));
885
946
  };
886
947
  return /* @__PURE__ */ jsxs9(
887
948
  "button",
@@ -892,7 +953,7 @@ function ShareActionButton({ target, url }) {
892
953
  "aria-label": `Share ${target.name}`,
893
954
  title: `Share ${target.name}`,
894
955
  children: [
895
- /* @__PURE__ */ jsx12("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: /* @__PURE__ */ jsx12(Share22, { "aria-hidden": "true", className: "w-5 h-5" }) }),
956
+ /* @__PURE__ */ jsx13("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: /* @__PURE__ */ jsx13(Share22, { "aria-hidden": "true", className: "w-5 h-5" }) }),
896
957
  "Share"
897
958
  ]
898
959
  }
@@ -900,7 +961,7 @@ function ShareActionButton({ target, url }) {
900
961
  }
901
962
 
902
963
  // src/genre-tree/GenreActionBand.tsx
903
- import { jsx as jsx13, jsxs as jsxs10 } from "react/jsx-runtime";
964
+ import { jsx as jsx14, jsxs as jsxs10 } from "react/jsx-runtime";
904
965
  function GenreActionBand({
905
966
  node,
906
967
  onPlayPause,
@@ -922,7 +983,7 @@ function GenreActionBand({
922
983
  items.push({
923
984
  key: "play-pause",
924
985
  label: isPlaying ? "Pause" : "Play",
925
- icon: isPlaying ? /* @__PURE__ */ jsx13(Pause, { "aria-hidden": "true", className: "w-5 h-5" }) : /* @__PURE__ */ jsx13(Play, { "aria-hidden": "true", className: "w-5 h-5" }),
986
+ icon: isPlaying ? /* @__PURE__ */ jsx14(Pause, { "aria-hidden": "true", className: "w-5 h-5" }) : /* @__PURE__ */ jsx14(Play, { "aria-hidden": "true", className: "w-5 h-5" }),
926
987
  onClick: onPlayPause
927
988
  });
928
989
  }
@@ -930,24 +991,24 @@ function GenreActionBand({
930
991
  items.push({
931
992
  key: "zoom-in",
932
993
  label: "Focus",
933
- icon: /* @__PURE__ */ jsx13(ZoomIn, { "aria-hidden": "true", className: "w-5 h-5" }),
994
+ icon: /* @__PURE__ */ jsx14(ZoomIn, { "aria-hidden": "true", className: "w-5 h-5" }),
934
995
  onClick: onZoomIn
935
996
  });
936
997
  }
937
998
  if (onAddChild) {
938
- adminItems.push({ key: "add-child", label: "Add", icon: /* @__PURE__ */ jsx13(Plus, { "aria-hidden": "true", className: "w-5 h-5" }), onClick: onAddChild });
999
+ adminItems.push({ key: "add-child", label: "Add", icon: /* @__PURE__ */ jsx14(Plus, { "aria-hidden": "true", className: "w-5 h-5" }), onClick: onAddChild });
939
1000
  }
940
1001
  if (onRename) {
941
- adminItems.push({ key: "rename", label: "Rename", icon: /* @__PURE__ */ jsx13(Pencil, { "aria-hidden": "true", className: "w-5 h-5" }), onClick: onRename });
1002
+ adminItems.push({ key: "rename", label: "Rename", icon: /* @__PURE__ */ jsx14(Pencil, { "aria-hidden": "true", className: "w-5 h-5" }), onClick: onRename });
942
1003
  }
943
1004
  if (onChangeParent) {
944
- adminItems.push({ key: "change-parent", label: "Parent", icon: /* @__PURE__ */ jsx13(MoveUpRight, { "aria-hidden": "true", className: "w-5 h-5" }), onClick: onChangeParent });
1005
+ adminItems.push({ key: "change-parent", label: "Parent", icon: /* @__PURE__ */ jsx14(MoveUpRight, { "aria-hidden": "true", className: "w-5 h-5" }), onClick: onChangeParent });
945
1006
  }
946
1007
  if (onToggleSide) {
947
1008
  adminItems.push({
948
1009
  key: "toggle-side",
949
1010
  label: side === "pop" ? "Make core" : "Make pop",
950
- icon: /* @__PURE__ */ jsx13(ArrowLeftRight, { "aria-hidden": "true", className: "w-5 h-5" }),
1011
+ icon: /* @__PURE__ */ jsx14(ArrowLeftRight, { "aria-hidden": "true", className: "w-5 h-5" }),
951
1012
  onClick: onToggleSide,
952
1013
  disabled: !!toggleSideDisabledReason,
953
1014
  title: toggleSideDisabledReason
@@ -972,7 +1033,7 @@ function GenreActionBand({
972
1033
  onClick: item.onClick,
973
1034
  className: "group flex flex-col items-center gap-1 w-16 text-xs text-center text-[var(--color-text)] focus-visible:outline-none disabled:opacity-50",
974
1035
  children: [
975
- /* @__PURE__ */ jsx13("span", { className: "flex items-center justify-center w-10 h-10 rounded-full group-focus-visible:ring-2 group-focus-visible:ring-[var(--color-focus-ring)] bg-[var(--color-surface-muted)] transition-colors group-hover:bg-[var(--color-border)] group-disabled:group-hover:bg-[var(--color-surface-muted)]", children: item.icon }),
1036
+ /* @__PURE__ */ jsx14("span", { className: "flex items-center justify-center w-10 h-10 rounded-full group-focus-visible:ring-2 group-focus-visible:ring-[var(--color-focus-ring)] bg-[var(--color-surface-muted)] transition-colors group-hover:bg-[var(--color-border)] group-disabled:group-hover:bg-[var(--color-surface-muted)]", children: item.icon }),
976
1037
  item.label
977
1038
  ]
978
1039
  },
@@ -981,7 +1042,7 @@ function GenreActionBand({
981
1042
  return /* @__PURE__ */ jsxs10("div", { className: "genre-action-band border-b border-[var(--color-border)]", children: [
982
1043
  (items.length > 0 || shareTarget && shareUrl) && /* @__PURE__ */ jsxs10("div", { className: "flex flex-wrap gap-1 px-3.5 py-3", children: [
983
1044
  renderItems(items),
984
- shareTarget && shareUrl && /* @__PURE__ */ jsx13(ShareActionButton, { target: shareTarget, url: shareUrl })
1045
+ shareTarget && shareUrl && /* @__PURE__ */ jsx14(ShareActionButton, { target: shareTarget, url: shareUrl })
985
1046
  ] }),
986
1047
  adminItems.length > 0 && /* @__PURE__ */ jsxs10(
987
1048
  "section",
@@ -989,8 +1050,8 @@ function GenreActionBand({
989
1050
  "aria-label": "Admin actions",
990
1051
  className: `px-3.5 py-3 ${items.length > 0 ? "border-t border-[var(--color-border)]" : ""}`,
991
1052
  children: [
992
- /* @__PURE__ */ jsx13("h3", { className: "mb-2 text-xs font-semibold uppercase tracking-wide text-[var(--color-text-muted)]", children: "Admin" }),
993
- /* @__PURE__ */ jsx13("div", { className: "flex flex-wrap gap-1", children: renderItems(adminItems) })
1053
+ /* @__PURE__ */ jsx14("h3", { className: "mb-2 text-xs font-semibold uppercase tracking-wide text-[var(--color-text-muted)]", children: "Admin" }),
1054
+ /* @__PURE__ */ jsx14("div", { className: "flex flex-wrap gap-1", children: renderItems(adminItems) })
994
1055
  ]
995
1056
  }
996
1057
  )
@@ -1002,20 +1063,20 @@ import { PanelSection as PanelSection2, Skeleton as Skeleton2 } from "@behindthe
1002
1063
  import { useFetchGenreOverview } from "@behindthemusictree/app-kit/genre-tree";
1003
1064
 
1004
1065
  // src/genre-tree/ClampedText.tsx
1005
- import { useLayoutEffect, useRef as useRef2, useState as useState4 } from "react";
1006
- import { Fragment as Fragment6, jsx as jsx14, jsxs as jsxs11 } from "react/jsx-runtime";
1066
+ import { useLayoutEffect, useRef as useRef2, useState as useState5 } from "react";
1067
+ import { Fragment as Fragment6, jsx as jsx15, jsxs as jsxs11 } from "react/jsx-runtime";
1007
1068
  function ClampedText({ children, className: className2 = "" }) {
1008
1069
  const ref = useRef2(null);
1009
- const [expanded, setExpanded] = useState4(false);
1010
- const [overflows, setOverflows] = useState4(false);
1070
+ const [expanded, setExpanded] = useState5(false);
1071
+ const [overflows, setOverflows] = useState5(false);
1011
1072
  useLayoutEffect(() => {
1012
1073
  const element = ref.current;
1013
1074
  if (!element || expanded) return;
1014
1075
  setOverflows(element.scrollHeight > element.clientHeight);
1015
1076
  }, [children, expanded]);
1016
1077
  return /* @__PURE__ */ jsxs11(Fragment6, { children: [
1017
- /* @__PURE__ */ jsx14("p", { ref, className: `${className2} ${expanded ? "" : "line-clamp-3"}`, children }),
1018
- (overflows || expanded) && /* @__PURE__ */ jsx14(
1078
+ /* @__PURE__ */ jsx15("p", { ref, className: `${className2} ${expanded ? "" : "line-clamp-3"}`, children }),
1079
+ (overflows || expanded) && /* @__PURE__ */ jsx15(
1019
1080
  "button",
1020
1081
  {
1021
1082
  type: "button",
@@ -1029,7 +1090,7 @@ function ClampedText({ children, className: className2 = "" }) {
1029
1090
  }
1030
1091
 
1031
1092
  // src/genre-tree/GenrePlaylistTracks.tsx
1032
- import { useEffect as useEffect3, useState as useState5 } from "react";
1093
+ import { useEffect as useEffect3, useState as useState6 } from "react";
1033
1094
  import { useInfiniteQuery } from "@tanstack/react-query";
1034
1095
  import { z } from "zod";
1035
1096
  import { FaPlay, FaPause } from "react-icons/fa";
@@ -1050,15 +1111,15 @@ import { useTrackList as useTrackList2 } from "@behindthemusictree/app-kit/genre
1050
1111
 
1051
1112
  // src/genre-tree/MusicBrainzRecordingLink.tsx
1052
1113
  import { musicBrainzRecordingUrl } from "@behindthemusictree/app-kit/genre-tree";
1053
- import { Fragment as Fragment7, jsx as jsx15 } from "react/jsx-runtime";
1114
+ import { Fragment as Fragment7, jsx as jsx16 } from "react/jsx-runtime";
1054
1115
  function MusicBrainzRecordingLink({
1055
1116
  song,
1056
1117
  children,
1057
1118
  className: className2 = ""
1058
1119
  }) {
1059
1120
  const url = musicBrainzRecordingUrl(song);
1060
- if (!url) return /* @__PURE__ */ jsx15(Fragment7, { children });
1061
- return /* @__PURE__ */ jsx15(
1121
+ if (!url) return /* @__PURE__ */ jsx16(Fragment7, { children });
1122
+ return /* @__PURE__ */ jsx16(
1062
1123
  "a",
1063
1124
  {
1064
1125
  href: url,
@@ -1075,7 +1136,7 @@ function MusicBrainzRecordingLink({
1075
1136
  // src/genre-tree/GenrePlaylistTracks.tsx
1076
1137
  import { ArtistNames } from "@behindthemusictree/app-kit/genre-tree";
1077
1138
  import { capitalize, useTrackNoun } from "@behindthemusictree/app-kit/track-noun";
1078
- import { jsx as jsx16, jsxs as jsxs12 } from "react/jsx-runtime";
1139
+ import { jsx as jsx17, jsxs as jsxs12 } from "react/jsx-runtime";
1079
1140
  var CATALOGUE_PAGE_SIZE = 100;
1080
1141
  var SongPageSchema = PaginatedResponseSchema(SongDetailedSchema).extend({
1081
1142
  overallTotal: z.number().nullable(),
@@ -1095,6 +1156,7 @@ var nextCursor = (next) => {
1095
1156
  function GenrePlaylistTracks({
1096
1157
  genreUuid,
1097
1158
  genrePlaylist,
1159
+ genreName: selectedGenreName,
1098
1160
  selection,
1099
1161
  scope,
1100
1162
  getBackendBaseUrl,
@@ -1110,13 +1172,17 @@ function GenrePlaylistTracks({
1110
1172
  const { playerTrackObject, playState, handlePlayPauseAction } = usePlayer4();
1111
1173
  const { showPopup } = usePopup4();
1112
1174
  const trackNoun = useTrackNoun();
1113
- const { uuid } = genrePlaylist;
1175
+ const uuid = genrePlaylist?.uuid;
1176
+ const genreName = selectedGenreName ?? genrePlaylist?.name ?? "Genre";
1114
1177
  const { fetch } = useFetchWrapper(getBackendBaseUrl);
1115
1178
  const playback = usePlaybackContext();
1116
1179
  const { data, isPending, isError, refetch, hasNextPage, isFetchingNextPage, fetchNextPage } = useInfiniteQuery({
1117
- queryKey: [...scope === "me" ? genrePlaylistQueryKeys.me.tracks(uuid) : artist ? [...libraryQueryKeys.reference.songs.byGenre(getBackendBaseUrl(), genreUuid, selection), "artist", artist.uuid] : libraryQueryKeys.reference.songs.byGenre(getBackendBaseUrl(), genreUuid, selection), ...playback.key ? [playback.key] : []],
1180
+ queryKey: [...scope === "me" ? uuid ? genrePlaylistQueryKeys.me.tracks(uuid) : ["missing-personal-genre-playlist", genreUuid] : artist ? [...libraryQueryKeys.reference.songs.byGenre(getBackendBaseUrl(), genreUuid, selection), "artist", artist.uuid] : libraryQueryKeys.reference.songs.byGenre(getBackendBaseUrl(), genreUuid, selection), ...playback.key ? [playback.key] : []],
1118
1181
  queryFn: async ({ pageParam }) => {
1119
- if (scope === "me") return fetchGenrePlaylistTracksPage(uuid, scope, Number(pageParam));
1182
+ if (scope === "me") {
1183
+ if (!uuid) throw new Error("Personal genre has no playlist");
1184
+ return fetchGenrePlaylistTracksPage(uuid, scope, Number(pageParam));
1185
+ }
1120
1186
  const response = await fetch(
1121
1187
  libraryEndpoints.reference.songs.list(),
1122
1188
  true,
@@ -1129,9 +1195,9 @@ function GenrePlaylistTracks({
1129
1195
  },
1130
1196
  initialPageParam: scope === "me" ? 1 : "",
1131
1197
  getNextPageParam: (lastPage) => lastPage.nextPage ?? void 0,
1132
- enabled: playback.ready && (scope !== "me" || sessionRestored && !!session?.accessToken)
1198
+ enabled: playback.ready && (scope !== "me" || uuid !== void 0 && sessionRestored && !!session?.accessToken)
1133
1199
  });
1134
- const [sentinel, setSentinel] = useState5(null);
1200
+ const [sentinel, setSentinel] = useState6(null);
1135
1201
  useEffect3(() => {
1136
1202
  if (!hasNextPage || isFetchingNextPage || !sentinel) return;
1137
1203
  const observer = new IntersectionObserver((entries) => {
@@ -1151,30 +1217,30 @@ function GenrePlaylistTracks({
1151
1217
  ordering: "title",
1152
1218
  genreUuid,
1153
1219
  ...selection && { selection }
1154
- }) : scope === "reference" ? playNewTrackListFromGenreCatalogue({ uuid: genreUuid, name: genrePlaylist.name }, scope, { startSongUuid, ...selection && { selection } }) : playNewTrackListFromGenrePlaylist(genrePlaylist, scope, { startSongUuid });
1220
+ }) : scope === "reference" ? playNewTrackListFromGenreCatalogue({ uuid: genreUuid, name: genreName }, scope, { startSongUuid, ...selection && { selection } }) : genrePlaylist ? playNewTrackListFromGenrePlaylist(genrePlaylist, scope, { startSongUuid }) : Promise.reject(new Error("Personal genre has no playlist"));
1155
1221
  started.catch((error) => {
1156
1222
  console.error("Failed to play genre playlist:", error);
1157
- showPopup(/* @__PURE__ */ jsx16(InternalErrorPopup, { errorCode: ErrorCode3.CLIENT_UNKNOWN }));
1223
+ showPopup(/* @__PURE__ */ jsx17(InternalErrorPopup, { errorCode: ErrorCode3.CLIENT_UNKNOWN }));
1158
1224
  });
1159
1225
  };
1160
- return /* @__PURE__ */ jsx16(PanelSection, { title: `${capitalize(trackNoun.plural)}${total !== void 0 ? ` (${total})` : ""}`, children: /* @__PURE__ */ jsxs12("div", { children: [
1226
+ return /* @__PURE__ */ jsx17(PanelSection, { title: `${capitalize(trackNoun.plural)}${total !== void 0 ? ` (${total})` : ""}`, children: /* @__PURE__ */ jsxs12("div", { children: [
1161
1227
  renderTracksToolbar?.(),
1162
- isPending ? /* @__PURE__ */ jsx16(Skeleton, { className: "h-4 w-3/4", "aria-busy": "true" }) : isError && tracks.length === 0 ? /* @__PURE__ */ jsxs12("p", { role: "alert", children: [
1228
+ scope === "me" && !genrePlaylist ? /* @__PURE__ */ jsx17("p", { children: "\u2014" }) : isPending ? /* @__PURE__ */ jsx17(Skeleton, { className: "h-4 w-3/4", "aria-busy": "true" }) : isError && tracks.length === 0 ? /* @__PURE__ */ jsxs12("p", { role: "alert", children: [
1163
1229
  "Couldn't load ",
1164
1230
  trackNoun.plural,
1165
1231
  ".",
1166
1232
  " ",
1167
- /* @__PURE__ */ jsx16("button", { type: "button", className: "underline", onClick: () => void refetch(), children: "Retry" })
1168
- ] }) : tracks.length === 0 ? /* @__PURE__ */ jsx16("p", { children: "\u2014" }) : /* @__PURE__ */ jsxs12("ul", { className: "mt-1 max-h-64 overflow-y-auto text-[0.8125rem] leading-[1.45] [container-type:inline-size]", "aria-label": `${genrePlaylist.name} ${trackNoun.plural}`, children: [
1233
+ /* @__PURE__ */ jsx17("button", { type: "button", className: "underline", onClick: () => void refetch(), children: "Retry" })
1234
+ ] }) : tracks.length === 0 ? /* @__PURE__ */ jsx17("p", { children: "\u2014" }) : /* @__PURE__ */ jsxs12("ul", { className: "mt-1 max-h-64 overflow-y-auto text-[0.8125rem] leading-[1.45] [container-type:inline-size]", "aria-label": `${genreName} ${trackNoun.plural}`, children: [
1169
1235
  tracks.map((track, index) => {
1170
1236
  const label = track.artists?.length ? `${track.title} \u2014 ${track.artists.map((artist2) => artist2.name).join(", ")}` : track.title;
1171
1237
  const isCurrent = playerTrackObject?.track.id === track.uuid;
1172
1238
  const isPlaying = isCurrent && playState === PlayStates.PLAYING;
1173
1239
  const text = /* @__PURE__ */ jsxs12("span", { className: TEXT_CELL, children: [
1174
- /* @__PURE__ */ jsx16("span", { className: TITLE_CELL, children: renderSongLink ? renderSongLink(track) : isSong(track) ? /* @__PURE__ */ jsx16(MusicBrainzRecordingLink, { song: track, className: "hover:underline focus-visible:underline", children: track.title }) : track.title }),
1175
- track.artists?.length ? /* @__PURE__ */ jsx16("span", { className: SEPARATOR, children: " \u2014 " }) : null,
1176
- /* @__PURE__ */ jsx16("span", { className: COLUMN_CELL, children: track.artists?.length ? /* @__PURE__ */ jsx16(ArtistNames, { artists: track.artists, renderArtistLink }) : null }),
1177
- track.album && /* @__PURE__ */ jsx16("span", { className: ALBUM_CELL, children: track.album.name })
1240
+ /* @__PURE__ */ jsx17("span", { className: TITLE_CELL, children: renderSongLink ? renderSongLink(track) : isSong(track) ? /* @__PURE__ */ jsx17(MusicBrainzRecordingLink, { song: track, className: "hover:underline focus-visible:underline", children: track.title }) : track.title }),
1241
+ track.artists?.length ? /* @__PURE__ */ jsx17("span", { className: SEPARATOR, children: " \u2014 " }) : null,
1242
+ /* @__PURE__ */ jsx17("span", { className: COLUMN_CELL, children: track.artists?.length ? /* @__PURE__ */ jsx17(ArtistNames, { artists: track.artists, renderArtistLink }) : null }),
1243
+ track.album && /* @__PURE__ */ jsx17("span", { className: ALBUM_CELL, children: track.album.name })
1178
1244
  ] });
1179
1245
  const leading = renderSongLeading?.(track, index);
1180
1246
  const actions = renderSongActions?.(track);
@@ -1182,7 +1248,7 @@ function GenrePlaylistTracks({
1182
1248
  if (isUnplayable) {
1183
1249
  return /* @__PURE__ */ jsxs12("li", { className: "flex min-h-8 items-center gap-1 px-1 opacity-50", children: [
1184
1250
  leading,
1185
- /* @__PURE__ */ jsx16(
1251
+ /* @__PURE__ */ jsx17(
1186
1252
  "button",
1187
1253
  {
1188
1254
  type: "button",
@@ -1190,7 +1256,7 @@ function GenrePlaylistTracks({
1190
1256
  "aria-disabled": "true",
1191
1257
  "aria-label": label,
1192
1258
  className: "flex h-6 w-6 shrink-0 items-center justify-center rounded-full opacity-0 cursor-not-allowed",
1193
- children: /* @__PURE__ */ jsx16(FaPlay, { size: 10 })
1259
+ children: /* @__PURE__ */ jsx17(FaPlay, { size: 10 })
1194
1260
  }
1195
1261
  ),
1196
1262
  text,
@@ -1203,14 +1269,14 @@ function GenrePlaylistTracks({
1203
1269
  className: `group flex min-h-8 items-center gap-1 rounded px-1 hover:bg-[var(--color-neutral-100)] ${isCurrent ? "font-medium text-[var(--color-primary)]" : ""}`,
1204
1270
  children: [
1205
1271
  leading,
1206
- /* @__PURE__ */ jsx16(
1272
+ /* @__PURE__ */ jsx17(
1207
1273
  "button",
1208
1274
  {
1209
1275
  type: "button",
1210
1276
  "aria-label": `${isPlaying ? "Pause" : "Play"} ${label}`,
1211
1277
  className: `flex h-6 w-6 shrink-0 items-center justify-center rounded-full cursor-pointer group-hover:opacity-100 focus-visible:opacity-100 ${isCurrent && playState !== PlayStates.STOPPED ? "opacity-100" : "opacity-0 [@media(pointer:coarse)]:opacity-100"}`,
1212
1278
  onClick: () => isCurrent ? handlePlayPauseAction() : play(track.uuid),
1213
- children: isCurrent && playState === PlayStates.LOADING ? /* @__PURE__ */ jsx16(RingLoader, { size: 10 }) : isPlaying ? /* @__PURE__ */ jsx16(FaPause, { size: 10 }) : /* @__PURE__ */ jsx16(FaPlay, { size: 10 })
1279
+ children: isCurrent && playState === PlayStates.LOADING ? /* @__PURE__ */ jsx17(RingLoader, { size: 10 }) : isPlaying ? /* @__PURE__ */ jsx17(FaPause, { size: 10 }) : /* @__PURE__ */ jsx17(FaPlay, { size: 10 })
1214
1280
  }
1215
1281
  ),
1216
1282
  text,
@@ -1220,7 +1286,7 @@ function GenrePlaylistTracks({
1220
1286
  track.uuid
1221
1287
  );
1222
1288
  }),
1223
- hasNextPage && /* @__PURE__ */ jsx16("li", { ref: setSentinel, "aria-hidden": "true", className: "h-4" })
1289
+ hasNextPage && /* @__PURE__ */ jsx17("li", { ref: setSentinel, "aria-hidden": "true", className: "h-4" })
1224
1290
  ] })
1225
1291
  ] }) });
1226
1292
  }
@@ -1228,10 +1294,11 @@ function GenrePlaylistTracks({
1228
1294
  // src/genre-tree/GenreDetailExtras.tsx
1229
1295
  import { useTrackNoun as useTrackNoun2 } from "@behindthemusictree/app-kit/track-noun";
1230
1296
  import { SiWikidata, SiWikipedia } from "react-icons/si";
1231
- import { Fragment as Fragment8, jsx as jsx17, jsxs as jsxs13 } from "react/jsx-runtime";
1297
+ import { Fragment as Fragment8, jsx as jsx18, jsxs as jsxs13 } from "react/jsx-runtime";
1232
1298
  function GenreDetailExtras({
1233
1299
  genreUuid,
1234
1300
  genrePlaylist,
1301
+ genreName,
1235
1302
  selection,
1236
1303
  wikidataId,
1237
1304
  scope,
@@ -1254,15 +1321,15 @@ function GenreDetailExtras({
1254
1321
  );
1255
1322
  if (isPending) {
1256
1323
  return /* @__PURE__ */ jsxs13(Fragment8, { children: [
1257
- /* @__PURE__ */ jsx17(PanelSection2, { title: "Summary", children: /* @__PURE__ */ jsx17("div", { "aria-busy": "true", "data-testid": "genre-detail-extras-skeleton", children: /* @__PURE__ */ jsx17(Skeleton2, { className: "h-4 w-3/4" }) }) }),
1324
+ /* @__PURE__ */ jsx18(PanelSection2, { title: "Summary", children: /* @__PURE__ */ jsx18("div", { "aria-busy": "true", "data-testid": "genre-detail-extras-skeleton", children: /* @__PURE__ */ jsx18(Skeleton2, { className: "h-4 w-3/4" }) }) }),
1258
1325
  family
1259
1326
  ] });
1260
1327
  }
1261
- if (!overview) return /* @__PURE__ */ jsx17(Fragment8, { children: family });
1328
+ if (!overview) return /* @__PURE__ */ jsx18(Fragment8, { children: family });
1262
1329
  const { summary, summarySourceUrl, essentialSongs } = overview;
1263
1330
  const wikidataUrl = wikidataId ? `https://www.wikidata.org/wiki/${encodeURIComponent(wikidataId)}` : null;
1264
1331
  return /* @__PURE__ */ jsxs13(Fragment8, { children: [
1265
- (summarySourceUrl || wikidataUrl) && /* @__PURE__ */ jsx17(PanelSection2, { title: "Links", children: /* @__PURE__ */ jsxs13("div", { className: "flex flex-wrap gap-x-3 gap-y-1 text-xs text-[var(--color-text)]", children: [
1332
+ (summarySourceUrl || wikidataUrl) && /* @__PURE__ */ jsx18(PanelSection2, { title: "Links", children: /* @__PURE__ */ jsxs13("div", { className: "flex flex-wrap gap-x-3 gap-y-1 text-xs text-[var(--color-text)]", children: [
1266
1333
  summarySourceUrl && /* @__PURE__ */ jsxs13(
1267
1334
  "a",
1268
1335
  {
@@ -1271,7 +1338,7 @@ function GenreDetailExtras({
1271
1338
  rel: "noopener noreferrer",
1272
1339
  className: "inline-flex items-center gap-1.5 text-[var(--color-text-muted)] hover:text-[var(--color-text)]",
1273
1340
  children: [
1274
- /* @__PURE__ */ jsx17(SiWikipedia, { "aria-hidden": "true", className: "h-3.5 w-3.5" }),
1341
+ /* @__PURE__ */ jsx18(SiWikipedia, { "aria-hidden": "true", className: "h-3.5 w-3.5" }),
1275
1342
  "Wikipedia"
1276
1343
  ]
1277
1344
  }
@@ -1284,21 +1351,22 @@ function GenreDetailExtras({
1284
1351
  rel: "noopener noreferrer",
1285
1352
  className: "inline-flex items-center gap-1.5 text-[var(--color-text-muted)] hover:text-[var(--color-text)]",
1286
1353
  children: [
1287
- /* @__PURE__ */ jsx17(SiWikidata, { "aria-hidden": "true", className: "h-3.5 w-3.5" }),
1354
+ /* @__PURE__ */ jsx18(SiWikidata, { "aria-hidden": "true", className: "h-3.5 w-3.5" }),
1288
1355
  "Wikidata"
1289
1356
  ]
1290
1357
  }
1291
1358
  )
1292
1359
  ] }) }),
1293
- /* @__PURE__ */ jsx17(PanelSection2, { title: "Summary", children: renderSummary ? renderSummary(overview) : /* @__PURE__ */ jsx17(ClampedText, { className: "text-[0.8125rem] leading-[1.45] text-[var(--color-text)]", children: summary ?? "\u2014" }, genreUuid) }),
1360
+ /* @__PURE__ */ jsx18(PanelSection2, { title: "Summary", children: renderSummary ? renderSummary(overview) : /* @__PURE__ */ jsx18(ClampedText, { className: "text-[0.8125rem] leading-[1.45] text-[var(--color-text)]", children: summary ?? "\u2014" }, genreUuid) }),
1294
1361
  family,
1295
1362
  renderGenreDetailExtras?.(overview),
1296
- (renderEssentialTracks || essentialSongs) && /* @__PURE__ */ jsx17(PanelSection2, { title: `Essential ${trackNoun.plural}`, children: renderEssentialTracks ? renderEssentialTracks(overview) : essentialSongs && essentialSongs.length > 0 ? /* @__PURE__ */ jsx17("ul", { className: "text-[0.8125rem] leading-[1.45] text-[var(--color-text)]", children: essentialSongs.map((track) => /* @__PURE__ */ jsx17("li", { className: "py-1", children: track.title }, track.uuid)) }) : /* @__PURE__ */ jsx17("p", { className: "text-[0.8125rem] leading-[1.45] text-[var(--color-text)]", children: "\u2014" }) }),
1297
- /* @__PURE__ */ jsx17(
1363
+ (renderEssentialTracks || essentialSongs) && /* @__PURE__ */ jsx18(PanelSection2, { title: `Essential ${trackNoun.plural}`, children: renderEssentialTracks ? renderEssentialTracks(overview) : essentialSongs && essentialSongs.length > 0 ? /* @__PURE__ */ jsx18("ul", { className: "text-[0.8125rem] leading-[1.45] text-[var(--color-text)]", children: essentialSongs.map((track) => /* @__PURE__ */ jsx18("li", { className: "py-1", children: track.title }, track.uuid)) }) : /* @__PURE__ */ jsx18("p", { className: "text-[0.8125rem] leading-[1.45] text-[var(--color-text)]", children: "\u2014" }) }),
1364
+ /* @__PURE__ */ jsx18(
1298
1365
  GenrePlaylistTracks,
1299
1366
  {
1300
1367
  genreUuid,
1301
1368
  genrePlaylist,
1369
+ genreName,
1302
1370
  selection,
1303
1371
  scope,
1304
1372
  getBackendBaseUrl,
@@ -1311,7 +1379,7 @@ function GenreDetailExtras({
1311
1379
  }
1312
1380
 
1313
1381
  // src/genre-tree/GenreInfoPanelBody.tsx
1314
- import { Fragment as Fragment9, jsx as jsx18, jsxs as jsxs14 } from "react/jsx-runtime";
1382
+ import { Fragment as Fragment9, jsx as jsx19, jsxs as jsxs14 } from "react/jsx-runtime";
1315
1383
  import { createElement } from "react";
1316
1384
  function GenreNodeActionBand({ genrePlaylist, playback, ...bandProps }) {
1317
1385
  const { isPlaying, setIsPlaying } = usePlayer5();
@@ -1325,7 +1393,7 @@ function GenreNodeActionBand({ genrePlaylist, playback, ...bandProps }) {
1325
1393
  console.error("Failed to load genre playlist tracks:", error);
1326
1394
  });
1327
1395
  };
1328
- return /* @__PURE__ */ jsx18(
1396
+ return /* @__PURE__ */ jsx19(
1329
1397
  GenreActionBand,
1330
1398
  {
1331
1399
  ...bandProps,
@@ -1340,6 +1408,7 @@ function GenreInfoPanelBody({
1340
1408
  genreUuid,
1341
1409
  genrePlaylist,
1342
1410
  catalogue,
1411
+ countsContext,
1343
1412
  wikidataId,
1344
1413
  scope,
1345
1414
  getBackendBaseUrl,
@@ -1348,7 +1417,7 @@ function GenreInfoPanelBody({
1348
1417
  extras
1349
1418
  }) {
1350
1419
  const noun = useTrackNoun3();
1351
- const summary = scope === "reference" ? GenreCatalogueCountsSchema2.parse(genrePlaylist) : void 0;
1420
+ const summary = scope === "reference" ? GenreCatalogueCountsSchema2.parse(genrePlaylist ?? catalogue) : void 0;
1352
1421
  const selection = summary ? defaultGenreMusicSelection2(summary) : void 0;
1353
1422
  const describeCounts = (counts) => {
1354
1423
  const scopes = counts.genreAndSubgenresTracksCount;
@@ -1356,17 +1425,19 @@ function GenreInfoPanelBody({
1356
1425
  };
1357
1426
  return /* @__PURE__ */ jsxs14(Fragment9, { children: [
1358
1427
  toolbar,
1359
- /* @__PURE__ */ jsx18(GenreNodeActionBand, { ...band, genrePlaylist, playback }),
1428
+ genrePlaylist ? /* @__PURE__ */ jsx19(GenreNodeActionBand, { ...band, genrePlaylist, playback }) : /* @__PURE__ */ jsx19(GenreActionBand, { ...band }),
1360
1429
  summary && catalogue && /* @__PURE__ */ jsxs14("div", { className: "px-3.5 py-3 text-sm text-[var(--color-text)]", children: [
1361
1430
  /* @__PURE__ */ jsxs14("p", { children: [
1362
1431
  "Catalogue ",
1363
1432
  noun.plural,
1433
+ countsContext ? ` (${countsContext})` : "",
1364
1434
  ": ",
1365
1435
  describeCounts(catalogue)
1366
1436
  ] }),
1367
- /* @__PURE__ */ jsxs14("p", { children: [
1437
+ genrePlaylist && /* @__PURE__ */ jsxs14("p", { children: [
1368
1438
  "Playlist ",
1369
1439
  noun.plural,
1440
+ countsContext ? ` (${countsContext})` : "",
1370
1441
  ": ",
1371
1442
  describeCounts(summary)
1372
1443
  ] })
@@ -1379,6 +1450,7 @@ function GenreInfoPanelBody({
1379
1450
  family,
1380
1451
  genreUuid,
1381
1452
  genrePlaylist,
1453
+ genreName: band.node.name,
1382
1454
  selection,
1383
1455
  wikidataId,
1384
1456
  scope,
@@ -1388,13 +1460,163 @@ function GenreInfoPanelBody({
1388
1460
  ] });
1389
1461
  }
1390
1462
 
1463
+ // src/genre-tree/GenreDetailPanel.tsx
1464
+ import { Fragment as Fragment10, jsx as jsx20, jsxs as jsxs15 } from "react/jsx-runtime";
1465
+ var RELATIVE_ITEM_COUNT = 1;
1466
+ function toNode(genre, parentId) {
1467
+ return {
1468
+ id: genre.uuid,
1469
+ parentId,
1470
+ name: genre.name,
1471
+ itemCount: RELATIVE_ITEM_COUNT,
1472
+ side: genre.side ?? void 0,
1473
+ wikidataId: genre.wikidataId
1474
+ };
1475
+ }
1476
+ function useGenreDetailContent({
1477
+ genreUuid,
1478
+ scope,
1479
+ getBackendBaseUrl,
1480
+ onSelectGenre,
1481
+ extras,
1482
+ band,
1483
+ playback: hostPlayback,
1484
+ trailing,
1485
+ genrePlaylist: hostPlaylist,
1486
+ expanded
1487
+ }) {
1488
+ const { data: detail, isPending, isError } = useFetchGenreDetail(genreUuid, scope, getBackendBaseUrl);
1489
+ const defaultPlayback = useGenrePlaylistPlayback({ scope, genrePlaylists: [], artist: extras?.artist });
1490
+ const playback = hostPlayback ?? defaultPlayback;
1491
+ const view = useMemo6(() => {
1492
+ if (!detail) return null;
1493
+ const rootColor = getGenreTreeColor2(detail.root.wikidataId);
1494
+ const relative = (genre, parentId) => ({
1495
+ node: toNode(genre, parentId),
1496
+ fill: tintSurface(rootColor),
1497
+ textColor: TEXT_COLOR
1498
+ });
1499
+ const ancestors2 = [...detail.ascendants].sort((a, b) => b.degree - a.degree);
1500
+ return {
1501
+ node: {
1502
+ id: detail.uuid,
1503
+ parentId: detail.parent?.uuid ?? null,
1504
+ name: detail.name,
1505
+ itemCount: scope === "reference" ? genreMusicCount2(detail.criteriaPlaylist ? GenrePlaylistSummarySchema.parse(detail.criteriaPlaylist) : GenreCatalogueCountsSchema3.parse(detail.catalogue)) : detail.tracksCount,
1506
+ wikidataId: detail.wikidataId
1507
+ },
1508
+ fill: rootColor,
1509
+ parentNode: detail.parent ? relative(detail.parent, null) : null,
1510
+ childNodes: detail.children.map((child) => relative(child, detail.uuid)),
1511
+ ancestorNodes: ancestors2.map(
1512
+ ({ ascendant }, index) => relative(ascendant, index === 0 ? null : ancestors2[index - 1].ascendant.uuid)
1513
+ )
1514
+ };
1515
+ }, [detail, scope]);
1516
+ if (isError) return {
1517
+ title: "Genre unavailable",
1518
+ content: /* @__PURE__ */ jsx20("p", { role: "alert", className: "p-4 text-sm text-[var(--color-text)]", children: "This genre could not be loaded." })
1519
+ };
1520
+ if (isPending || !detail || !view) return {
1521
+ title: "Loading genre\u2026",
1522
+ content: /* @__PURE__ */ jsxs15("div", { className: "flex flex-col gap-3 p-4", "aria-busy": "true", "data-testid": "genre-detail-panel-skeleton", children: [
1523
+ /* @__PURE__ */ jsx20(Skeleton3, { className: "h-6 w-2/3" }),
1524
+ /* @__PURE__ */ jsx20(Skeleton3, { className: "h-4 w-full" }),
1525
+ /* @__PURE__ */ jsx20(Skeleton3, { className: "h-4 w-3/4" })
1526
+ ] })
1527
+ };
1528
+ const selectGenre = (nodeId) => onSelectGenre(GenreUuidSchema.parse(nodeId));
1529
+ const family = /* @__PURE__ */ jsx20(
1530
+ GenreNodeFamily,
1531
+ {
1532
+ node: view.node,
1533
+ fill: view.fill,
1534
+ textColor: ACCENT_TEXT_COLOR,
1535
+ parentNode: view.parentNode,
1536
+ childNodes: view.childNodes,
1537
+ onSelectNode: selectGenre
1538
+ }
1539
+ );
1540
+ const ancestors = /* @__PURE__ */ jsx20(GenreNodeAncestors, { ancestorNodes: view.ancestorNodes, onSelectNode: selectGenre });
1541
+ return {
1542
+ title: detail.name,
1543
+ headerAccent: { background: view.fill, color: ACCENT_TEXT_COLOR },
1544
+ aside: expanded ? ancestors : void 0,
1545
+ content: /* @__PURE__ */ jsxs15(Fragment10, { children: [
1546
+ /* @__PURE__ */ jsx20(
1547
+ GenreInfoPanelBody,
1548
+ {
1549
+ family: /* @__PURE__ */ jsxs15(Fragment10, { children: [
1550
+ family,
1551
+ !expanded && ancestors
1552
+ ] }),
1553
+ genreUuid: detail.uuid,
1554
+ genrePlaylist: hostPlaylist ?? (detail.criteriaPlaylist === null ? null : scope === "reference" ? { ...GenrePlaylistSummarySchema.parse(detail.criteriaPlaylist), criteria: CriteriaMinimumSchema.parse(detail) } : { ...detail.criteriaPlaylist, criteria: CriteriaMinimumSchema.parse(detail), tracksCount: detail.tracksCount }),
1555
+ catalogue: detail.catalogue,
1556
+ countsContext: !hostPlaylist && extras?.artist ? "all artists" : void 0,
1557
+ wikidataId: detail.wikidataId,
1558
+ scope,
1559
+ getBackendBaseUrl,
1560
+ playback,
1561
+ band: { node: view.node, ...band, shareTarget: band?.shareTarget?.kind === "genre" ? { ...band.shareTarget, name: detail.name } : band?.shareTarget },
1562
+ extras
1563
+ }
1564
+ ),
1565
+ trailing
1566
+ ] })
1567
+ };
1568
+ }
1569
+ function GenreDetailPanel({
1570
+ genreUuid,
1571
+ scope,
1572
+ getBackendBaseUrl,
1573
+ onClose,
1574
+ onSelectGenre,
1575
+ onZoomIn,
1576
+ shareUrl,
1577
+ shareTarget,
1578
+ collapsed,
1579
+ expanded,
1580
+ onExpandedChange,
1581
+ edgeTab,
1582
+ extras,
1583
+ headerClassName
1584
+ }) {
1585
+ const panel = useGenreDetailContent({
1586
+ genreUuid,
1587
+ scope,
1588
+ getBackendBaseUrl,
1589
+ onSelectGenre,
1590
+ extras,
1591
+ expanded,
1592
+ band: { onZoomIn, shareUrl, shareTarget: shareUrl ? shareTarget : void 0 }
1593
+ });
1594
+ return /* @__PURE__ */ jsx20(
1595
+ DetailPanel,
1596
+ {
1597
+ side: "left",
1598
+ title: panel.title,
1599
+ headerAccent: panel.headerAccent,
1600
+ onClose,
1601
+ collapsed,
1602
+ expanded,
1603
+ onExpandedChange,
1604
+ edgeTab,
1605
+ aside: panel.aside,
1606
+ headerClassName,
1607
+ headerActions: onExpandedChange ? /* @__PURE__ */ jsx20(IconButton, { "aria-label": expanded ? "Shrink panel" : "Extend panel", onClick: () => onExpandedChange(!expanded), children: expanded ? /* @__PURE__ */ jsx20(Minimize2, { "aria-hidden": "true", className: "h-4 w-4" }) : /* @__PURE__ */ jsx20(Maximize2, { "aria-hidden": "true", className: "h-4 w-4" }) }) : void 0,
1608
+ children: panel.content
1609
+ }
1610
+ );
1611
+ }
1612
+
1391
1613
  // src/genre-tree/ChangeParentPopup.tsx
1392
- import { useEffect as useEffect4, useMemo as useMemo6 } from "react";
1614
+ import { useEffect as useEffect4, useMemo as useMemo7 } from "react";
1393
1615
  import {
1394
1616
  getReparentCandidates,
1395
1617
  useUpdateGenre
1396
1618
  } from "@behindthemusictree/app-kit/genre-tree";
1397
- import { jsx as jsx19, jsxs as jsxs15 } from "react/jsx-runtime";
1619
+ import { jsx as jsx21, jsxs as jsxs16 } from "react/jsx-runtime";
1398
1620
  function ChangeParentPopup({
1399
1621
  genrePlaylist,
1400
1622
  genrePlaylists,
@@ -1404,7 +1626,7 @@ function ChangeParentPopup({
1404
1626
  onParentChanged
1405
1627
  }) {
1406
1628
  const { mutate, isPending, formErrors } = useUpdateGenre(scope, getBackendBaseUrl);
1407
- const candidates = useMemo6(
1629
+ const candidates = useMemo7(
1408
1630
  () => getReparentCandidates(genrePlaylists, genrePlaylist.uuid),
1409
1631
  [genrePlaylists, genrePlaylist.uuid]
1410
1632
  );
@@ -1429,12 +1651,12 @@ function ChangeParentPopup({
1429
1651
  window.addEventListener("keydown", handleKeyDown);
1430
1652
  return () => window.removeEventListener("keydown", handleKeyDown);
1431
1653
  }, [onClose]);
1432
- return /* @__PURE__ */ jsxs15(BasePopup_default, { title: `Change parent of ${genrePlaylist.name}`, onClose, showCancelButton: true, fitContent: true, children: [
1433
- /* @__PURE__ */ jsxs15("p", { className: "mb-3 text-sm text-[var(--color-text-muted)]", children: [
1654
+ return /* @__PURE__ */ jsxs16(BasePopup_default, { title: `Change parent of ${genrePlaylist.name}`, onClose, showCancelButton: true, fitContent: true, children: [
1655
+ /* @__PURE__ */ jsxs16("p", { className: "mb-3 text-sm text-[var(--color-text-muted)]", children: [
1434
1656
  "Current parent: ",
1435
1657
  genrePlaylist.parent?.name ?? "none"
1436
1658
  ] }),
1437
- /* @__PURE__ */ jsx19("div", { "aria-busy": isPending, className: isPending ? "pointer-events-none opacity-60" : void 0, children: /* @__PURE__ */ jsx19(
1659
+ /* @__PURE__ */ jsx21("div", { "aria-busy": isPending, className: isPending ? "pointer-events-none opacity-60" : void 0, children: /* @__PURE__ */ jsx21(
1438
1660
  GenreSearch,
1439
1661
  {
1440
1662
  genrePlaylists: candidates,
@@ -1442,12 +1664,12 @@ function ChangeParentPopup({
1442
1664
  placeholder: "Search a new parent\u2026"
1443
1665
  }
1444
1666
  ) }),
1445
- formErrors.length > 0 && /* @__PURE__ */ jsx19("p", { role: "alert", className: "mt-3 text-sm text-[var(--color-red-500)]", children: formErrors.map(({ message }) => message).join(" ") })
1667
+ formErrors.length > 0 && /* @__PURE__ */ jsx21("p", { role: "alert", className: "mt-3 text-sm text-[var(--color-red-500)]", children: formErrors.map(({ message }) => message).join(" ") })
1446
1668
  ] });
1447
1669
  }
1448
1670
 
1449
1671
  // src/genre-tree/artist/useArtistPanel.tsx
1450
- import { Skeleton as Skeleton4 } from "@behindthemusictree/ui";
1672
+ import { Skeleton as Skeleton5 } from "@behindthemusictree/ui";
1451
1673
  import { useArtist } from "@behindthemusictree/app-kit/genre-tree";
1452
1674
 
1453
1675
  // src/genre-tree/artist/ArtistHeader.tsx
@@ -1466,7 +1688,7 @@ import {
1466
1688
  SiYoutube
1467
1689
  } from "react-icons/si";
1468
1690
  import { Chip } from "@behindthemusictree/ui";
1469
- import { jsx as jsx20 } from "react/jsx-runtime";
1691
+ import { jsx as jsx22 } from "react/jsx-runtime";
1470
1692
  var LINK_KINDS = {
1471
1693
  official: { label: "Official website", Icon: Globe },
1472
1694
  wikipedia: { label: "Wikipedia", Icon: SiWikipedia2 },
@@ -1503,7 +1725,7 @@ function artistLinks(artist) {
1503
1725
  return links;
1504
1726
  }
1505
1727
  function ArtistLinks({ links }) {
1506
- return /* @__PURE__ */ jsx20("ul", { className: "flex flex-wrap gap-1.5", children: links.map(({ key, url, Icon, label }) => /* @__PURE__ */ jsx20("li", { children: /* @__PURE__ */ jsx20(Chip, { href: url, icon: /* @__PURE__ */ jsx20(Icon, { className: "h-3.5 w-3.5 shrink-0", "aria-hidden": "true" }), children: label }) }, key)) });
1728
+ return /* @__PURE__ */ jsx22("ul", { className: "flex flex-wrap gap-1.5", children: links.map(({ key, url, Icon, label }) => /* @__PURE__ */ jsx22("li", { children: /* @__PURE__ */ jsx22(Chip, { href: url, icon: /* @__PURE__ */ jsx22(Icon, { className: "h-3.5 w-3.5 shrink-0", "aria-hidden": "true" }), children: label }) }, key)) });
1507
1729
  }
1508
1730
 
1509
1731
  // src/genre-tree/artist/formatYears.ts
@@ -1514,7 +1736,7 @@ function formatYears(beginYear, endYear, ended) {
1514
1736
  }
1515
1737
 
1516
1738
  // src/genre-tree/artist/ArtistHeader.tsx
1517
- import { jsx as jsx21 } from "react/jsx-runtime";
1739
+ import { jsx as jsx23 } from "react/jsx-runtime";
1518
1740
  var countryNames = new Intl.DisplayNames(["en"], { type: "region" });
1519
1741
  function ArtistHeader({ artist }) {
1520
1742
  const fields = [];
@@ -1524,35 +1746,35 @@ function ArtistHeader({ artist }) {
1524
1746
  const years = formatYears(artist.beginYear, artist.endYear, artist.ended);
1525
1747
  if (years) fields.push({ label: "Active", value: years });
1526
1748
  const links = artistLinks(artist);
1527
- if (links.length > 0) fields.push({ label: "Links", value: /* @__PURE__ */ jsx21(ArtistLinks, { links }) });
1749
+ if (links.length > 0) fields.push({ label: "Links", value: /* @__PURE__ */ jsx23(ArtistLinks, { links }) });
1528
1750
  if (fields.length === 0) return null;
1529
- return /* @__PURE__ */ jsx21(FieldList, { items: fields });
1751
+ return /* @__PURE__ */ jsx23(FieldList, { items: fields });
1530
1752
  }
1531
1753
 
1532
1754
  // src/genre-tree/artist/ArtistPhoto.tsx
1533
- import { jsx as jsx22, jsxs as jsxs16 } from "react/jsx-runtime";
1755
+ import { jsx as jsx24, jsxs as jsxs17 } from "react/jsx-runtime";
1534
1756
  function ExternalLink({ href, children }) {
1535
- return /* @__PURE__ */ jsx22("a", { href, target: "_blank", rel: "noopener noreferrer", className: "underline-offset-2 hover:underline", children });
1757
+ return /* @__PURE__ */ jsx24("a", { href, target: "_blank", rel: "noopener noreferrer", className: "underline-offset-2 hover:underline", children });
1536
1758
  }
1537
1759
  function ArtistPhoto({ image, name }) {
1538
- return /* @__PURE__ */ jsxs16("figure", { className: "flex flex-col gap-1.5 px-6 py-2.5", children: [
1539
- /* @__PURE__ */ jsx22("img", { src: image.url, alt: name, className: "h-64 w-full object-contain bg-[var(--color-surface-muted)] rounded" }),
1540
- /* @__PURE__ */ jsxs16("figcaption", { className: "text-xs text-[var(--color-text-muted)]", children: [
1760
+ return /* @__PURE__ */ jsxs17("figure", { className: "flex flex-col gap-1.5 px-6 py-2.5", children: [
1761
+ /* @__PURE__ */ jsx24("img", { src: image.url, alt: name, className: "h-64 w-full object-contain bg-[var(--color-surface-muted)] rounded" }),
1762
+ /* @__PURE__ */ jsxs17("figcaption", { className: "text-xs text-[var(--color-text-muted)]", children: [
1541
1763
  "Photo: ",
1542
- /* @__PURE__ */ jsx22(ExternalLink, { href: image.pageUrl, children: image.author ?? "Wikimedia Commons" }),
1764
+ /* @__PURE__ */ jsx24(ExternalLink, { href: image.pageUrl, children: image.author ?? "Wikimedia Commons" }),
1543
1765
  ",",
1544
1766
  " ",
1545
- image.licenseUrl ? /* @__PURE__ */ jsx22(ExternalLink, { href: image.licenseUrl, children: image.license }) : image.license
1767
+ image.licenseUrl ? /* @__PURE__ */ jsx24(ExternalLink, { href: image.licenseUrl, children: image.license }) : image.license
1546
1768
  ] })
1547
1769
  ] });
1548
1770
  }
1549
1771
 
1550
1772
  // src/genre-tree/artist/ArtistMembers.tsx
1551
1773
  import { PanelGroup, PanelSection as PanelSection3 } from "@behindthemusictree/ui";
1552
- import { jsx as jsx23, jsxs as jsxs17 } from "react/jsx-runtime";
1774
+ import { jsx as jsx25, jsxs as jsxs18 } from "react/jsx-runtime";
1553
1775
  function MembershipList({ title, memberships, renderArtistLink }) {
1554
1776
  if (memberships.length === 0) return null;
1555
- return /* @__PURE__ */ jsx23(PanelGroup, { title, children: /* @__PURE__ */ jsx23("ul", { children: memberships.map((membership, index) => {
1777
+ return /* @__PURE__ */ jsx25(PanelGroup, { title, children: /* @__PURE__ */ jsx25("ul", { children: memberships.map((membership, index) => {
1556
1778
  const details = [
1557
1779
  formatYears(membership.beginYear, membership.endYear, membership.ended),
1558
1780
  ...membership.roles
@@ -1560,9 +1782,9 @@ function MembershipList({ title, memberships, renderArtistLink }) {
1560
1782
  const detailsText = details.filter(Boolean).join(" \xB7 ");
1561
1783
  return (
1562
1784
  // A member can have several periods in the same group.
1563
- /* @__PURE__ */ jsxs17("li", { className: "truncate py-0.5", title: detailsText || void 0, children: [
1564
- /* @__PURE__ */ jsx23("span", { className: "font-medium text-[var(--color-text)]", children: renderArtistLink(membership.artist) }),
1565
- detailsText && /* @__PURE__ */ jsxs17("span", { className: "text-[var(--color-text-muted)]", children: [
1785
+ /* @__PURE__ */ jsxs18("li", { className: "truncate py-0.5", title: detailsText || void 0, children: [
1786
+ /* @__PURE__ */ jsx25("span", { className: "font-medium text-[var(--color-text)]", children: renderArtistLink(membership.artist) }),
1787
+ detailsText && /* @__PURE__ */ jsxs18("span", { className: "text-[var(--color-text-muted)]", children: [
1566
1788
  " \u2014 ",
1567
1789
  detailsText
1568
1790
  ] })
@@ -1573,9 +1795,9 @@ function MembershipList({ title, memberships, renderArtistLink }) {
1573
1795
  function ArtistMembers({ title, memberships, renderArtistLink }) {
1574
1796
  if (memberships.length === 0) return null;
1575
1797
  const isCurrent = (membership) => !membership.ended && membership.endYear === null;
1576
- return /* @__PURE__ */ jsxs17(PanelSection3, { title, children: [
1577
- /* @__PURE__ */ jsx23(MembershipList, { title: "Current", memberships: memberships.filter(isCurrent), renderArtistLink }),
1578
- /* @__PURE__ */ jsx23(
1798
+ return /* @__PURE__ */ jsxs18(PanelSection3, { title, children: [
1799
+ /* @__PURE__ */ jsx25(MembershipList, { title: "Current", memberships: memberships.filter(isCurrent), renderArtistLink }),
1800
+ /* @__PURE__ */ jsx25(
1579
1801
  MembershipList,
1580
1802
  {
1581
1803
  title: "Past",
@@ -1588,7 +1810,7 @@ function ArtistMembers({ title, memberships, renderArtistLink }) {
1588
1810
 
1589
1811
  // src/genre-tree/artist/ArtistDiscography.tsx
1590
1812
  import { PanelGroup as PanelGroup2, PanelSection as PanelSection4 } from "@behindthemusictree/ui";
1591
- import { jsx as jsx24, jsxs as jsxs18 } from "react/jsx-runtime";
1813
+ import { jsx as jsx26, jsxs as jsxs19 } from "react/jsx-runtime";
1592
1814
  var GROUPS = ["Albums", "EPs", "Singles", "Live", "Compilations", "Other"];
1593
1815
  var PRIMARY_GROUPS = { album: "Albums", ep: "EPs", single: "Singles" };
1594
1816
  function discographyGroup({ primaryType, secondaryTypes }) {
@@ -1608,21 +1830,21 @@ function byYear(a, b) {
1608
1830
  function ArtistDiscography({ albums }) {
1609
1831
  if (albums.length === 0) return null;
1610
1832
  const sorted = [...albums].sort(byYear);
1611
- return /* @__PURE__ */ jsx24(PanelSection4, { title: "Discography", children: GROUPS.map((group) => {
1833
+ return /* @__PURE__ */ jsx26(PanelSection4, { title: "Discography", children: GROUPS.map((group) => {
1612
1834
  const groupAlbums = sorted.filter((album) => discographyGroup(album) === group);
1613
1835
  if (groupAlbums.length === 0) return null;
1614
- return /* @__PURE__ */ jsx24(PanelGroup2, { title: group, children: /* @__PURE__ */ jsx24("ul", { children: groupAlbums.map((album) => /* @__PURE__ */ jsxs18("li", { className: "grid grid-cols-[2.5rem_minmax(0,1fr)] gap-2 py-0.5", children: [
1615
- /* @__PURE__ */ jsx24("span", { className: "tabular-nums text-[var(--color-text-muted)]", children: album.year ?? "\u2014" }),
1616
- /* @__PURE__ */ jsx24("span", { className: "truncate text-[var(--color-text)]", title: album.name, children: album.name })
1836
+ return /* @__PURE__ */ jsx26(PanelGroup2, { title: group, children: /* @__PURE__ */ jsx26("ul", { children: groupAlbums.map((album) => /* @__PURE__ */ jsxs19("li", { className: "grid grid-cols-[2.5rem_minmax(0,1fr)] gap-2 py-0.5", children: [
1837
+ /* @__PURE__ */ jsx26("span", { className: "tabular-nums text-[var(--color-text-muted)]", children: album.year ?? "\u2014" }),
1838
+ /* @__PURE__ */ jsx26("span", { className: "truncate text-[var(--color-text)]", title: album.name, children: album.name })
1617
1839
  ] }, album.uuid)) }) }, group);
1618
1840
  }) });
1619
1841
  }
1620
1842
 
1621
1843
  // src/genre-tree/artist/ArtistSongs.tsx
1622
- import { useState as useState6 } from "react";
1844
+ import { useState as useState7 } from "react";
1623
1845
  import { keepPreviousData } from "@tanstack/react-query";
1624
1846
  import { Play as Play2 } from "lucide-react";
1625
- import { Pagination, PanelSection as PanelSection5, Skeleton as Skeleton3 } from "@behindthemusictree/ui";
1847
+ import { Pagination, PanelSection as PanelSection5, Skeleton as Skeleton4 } from "@behindthemusictree/ui";
1626
1848
  import { usePlayer as usePlayer8 } from "@behindthemusictree/app-kit/player";
1627
1849
  import {
1628
1850
  SongDetailedSchema as SongDetailedSchema2,
@@ -1640,7 +1862,7 @@ import { useTrackList as useTrackList3 } from "@behindthemusictree/app-kit/genre
1640
1862
  import { FaPlay as FaPlay2, FaPause as FaPause2 } from "react-icons/fa";
1641
1863
  import { usePlayer as usePlayer6 } from "@behindthemusictree/app-kit/player";
1642
1864
  import { PlayStates as PlayStates2 } from "@behindthemusictree/app-kit/player";
1643
- import { jsx as jsx25, jsxs as jsxs19 } from "react/jsx-runtime";
1865
+ import { jsx as jsx27, jsxs as jsxs20 } from "react/jsx-runtime";
1644
1866
  function TrackPositionPlayPause({
1645
1867
  position,
1646
1868
  uuid,
@@ -1654,14 +1876,14 @@ function TrackPositionPlayPause({
1654
1876
  const isPlaying = isThisTrack && isConfirmedPlayable && playState === PlayStates2.PLAYING;
1655
1877
  const isPaused = isThisTrack && isConfirmedPlayable && playState === PlayStates2.PAUSED;
1656
1878
  const isCurrentTrackActive = isPlaying || isPaused;
1657
- return /* @__PURE__ */ jsxs19(
1879
+ return /* @__PURE__ */ jsxs20(
1658
1880
  "div",
1659
1881
  {
1660
1882
  className: "track-position-play-pause relative flex items-center justify-center gap-2 text-lg w-16 cursor-pointer",
1661
1883
  style: { minWidth: "4rem", maxWidth: "4rem" },
1662
1884
  onClick: handlePlayPauseClick,
1663
1885
  children: [
1664
- /* @__PURE__ */ jsx25("div", { className: "track-position-play-pause__status group-hover:hidden flex items-center justify-center", children: isAwaitingPlayback ? /* @__PURE__ */ jsx25("span", { className: "track-position-play-pause__spinner", role: "status", "aria-label": "Loading track" }) : isCurrentTrackActive ? /* @__PURE__ */ jsxs19(
1886
+ /* @__PURE__ */ jsx27("div", { className: "track-position-play-pause__status group-hover:hidden flex items-center justify-center", children: isAwaitingPlayback ? /* @__PURE__ */ jsx27("span", { className: "track-position-play-pause__spinner", role: "status", "aria-label": "Loading track" }) : isCurrentTrackActive ? /* @__PURE__ */ jsxs20(
1665
1887
  "span",
1666
1888
  {
1667
1889
  className: "track-position-play-pause__bars",
@@ -1669,18 +1891,18 @@ function TrackPositionPlayPause({
1669
1891
  role: "img",
1670
1892
  "aria-label": isPlaying ? "Playing" : "Paused",
1671
1893
  children: [
1672
- /* @__PURE__ */ jsx25("span", { className: "track-position-play-pause__bar" }),
1673
- /* @__PURE__ */ jsx25("span", { className: "track-position-play-pause__bar" }),
1674
- /* @__PURE__ */ jsx25("span", { className: "track-position-play-pause__bar" })
1894
+ /* @__PURE__ */ jsx27("span", { className: "track-position-play-pause__bar" }),
1895
+ /* @__PURE__ */ jsx27("span", { className: "track-position-play-pause__bar" }),
1896
+ /* @__PURE__ */ jsx27("span", { className: "track-position-play-pause__bar" })
1675
1897
  ]
1676
1898
  }
1677
1899
  ) : position }),
1678
- /* @__PURE__ */ jsx25(
1900
+ /* @__PURE__ */ jsx27(
1679
1901
  "div",
1680
1902
  {
1681
1903
  className: `hidden group-hover:flex items-center justify-center ${isCurrentTrackActive ? "" : "absolute inset-0"}`,
1682
1904
  "aria-hidden": "true",
1683
- children: isAwaitingPlayback ? /* @__PURE__ */ jsx25("span", { className: "track-position-play-pause__spinner" }) : isPlaying ? /* @__PURE__ */ jsx25(FaPause2, {}) : /* @__PURE__ */ jsx25(FaPlay2, {})
1905
+ children: isAwaitingPlayback ? /* @__PURE__ */ jsx27("span", { className: "track-position-play-pause__spinner" }) : isPlaying ? /* @__PURE__ */ jsx27(FaPause2, {}) : /* @__PURE__ */ jsx27(FaPlay2, {})
1684
1906
  }
1685
1907
  )
1686
1908
  ]
@@ -1691,7 +1913,7 @@ function TrackPositionPlayPause({
1691
1913
  // src/genre-tree/track-list-sidebar/TrackItem.tsx
1692
1914
  import { isSong as isSong2, songUnplayableReason as songUnplayableReason2 } from "@behindthemusictree/app-kit/genre-tree";
1693
1915
  import { ArtistNames as ArtistNames2 } from "@behindthemusictree/app-kit/genre-tree";
1694
- import { jsx as jsx26, jsxs as jsxs20 } from "react/jsx-runtime";
1916
+ import { jsx as jsx28, jsxs as jsxs21 } from "react/jsx-runtime";
1695
1917
  function TrackItem({
1696
1918
  track,
1697
1919
  position,
@@ -1715,12 +1937,12 @@ function TrackItem({
1715
1937
  loadTrackForPlayer(track.uuid);
1716
1938
  }
1717
1939
  };
1718
- return /* @__PURE__ */ jsxs20(
1940
+ return /* @__PURE__ */ jsxs21(
1719
1941
  "div",
1720
1942
  {
1721
1943
  className: `track-item flex h-14 text-[var(--color-text)] ${isUnplayable ? "opacity-50" : "hover:bg-[var(--color-surface-muted)] group"}`,
1722
1944
  children: [
1723
- isUnplayable ? /* @__PURE__ */ jsx26(
1945
+ isUnplayable ? /* @__PURE__ */ jsx28(
1724
1946
  "div",
1725
1947
  {
1726
1948
  className: "flex items-center justify-center text-lg w-16",
@@ -1728,7 +1950,7 @@ function TrackItem({
1728
1950
  "aria-disabled": "true",
1729
1951
  children: position
1730
1952
  }
1731
- ) : /* @__PURE__ */ jsx26(
1953
+ ) : /* @__PURE__ */ jsx28(
1732
1954
  TrackPositionPlayPause,
1733
1955
  {
1734
1956
  position,
@@ -1737,12 +1959,12 @@ function TrackItem({
1737
1959
  isSelected: selectedTrack?.uuid === track.uuid
1738
1960
  }
1739
1961
  ),
1740
- /* @__PURE__ */ jsxs20("div", { className: "title-artist-container flex flex-col items-start justify-center min-w-0 flex-1", children: [
1741
- /* @__PURE__ */ jsx26("div", { className: "flex max-w-full items-center gap-2", children: /* @__PURE__ */ jsx26("div", { className: "title text-lg font-bold text-[var(--color-text)] max-w-full truncate", children: renderSongLink ? renderSongLink(track) : isSong2(track) ? /* @__PURE__ */ jsx26(MusicBrainzRecordingLink, { song: track, className: "hover:underline focus-visible:underline", children: track.title }) : track.title }) }),
1742
- track.artists && track.artists.length > 0 ? /* @__PURE__ */ jsx26("div", { className: "artist text-base max-w-full truncate text-[var(--color-text-muted)]", children: /* @__PURE__ */ jsx26(ArtistNames2, { artists: track.artists, renderArtistLink }) }) : ""
1962
+ /* @__PURE__ */ jsxs21("div", { className: "title-artist-container flex flex-col items-start justify-center min-w-0 flex-1", children: [
1963
+ /* @__PURE__ */ jsx28("div", { className: "flex max-w-full items-center gap-2", children: /* @__PURE__ */ jsx28("div", { className: "title text-lg font-bold text-[var(--color-text)] max-w-full truncate", children: renderSongLink ? renderSongLink(track) : isSong2(track) ? /* @__PURE__ */ jsx28(MusicBrainzRecordingLink, { song: track, className: "hover:underline focus-visible:underline", children: track.title }) : track.title }) }),
1964
+ track.artists && track.artists.length > 0 ? /* @__PURE__ */ jsx28("div", { className: "artist text-base max-w-full truncate text-[var(--color-text-muted)]", children: /* @__PURE__ */ jsx28(ArtistNames2, { artists: track.artists, renderArtistLink }) }) : ""
1743
1965
  ] }),
1744
- /* @__PURE__ */ jsx26("div", { className: "genre-name-container flex items-center justify-start w-[21.6%] min-w-0 shrink-0", children: track.genre ? /* @__PURE__ */ jsx26("div", { className: "genre-name max-w-full whitespace-normal break-words p-1 text-xs font-bold", children: track.genre.name }) : "" }),
1745
- renderDuration && /* @__PURE__ */ jsx26(
1966
+ /* @__PURE__ */ jsx28("div", { className: "genre-name-container flex items-center justify-start w-[21.6%] min-w-0 shrink-0", children: track.genre ? /* @__PURE__ */ jsx28("div", { className: "genre-name max-w-full whitespace-normal break-words p-1 text-xs font-bold", children: track.genre.name }) : "" }),
1967
+ renderDuration && /* @__PURE__ */ jsx28(
1746
1968
  "div",
1747
1969
  {
1748
1970
  className: "duration flex text-base w-16 items-center justify-center",
@@ -1750,22 +1972,22 @@ function TrackItem({
1750
1972
  children: renderDuration(track)
1751
1973
  }
1752
1974
  ),
1753
- renderActions && /* @__PURE__ */ jsx26("div", { className: "edit flex text-base w-6 items-center justify-center mr-2 cursor-pointer", children: renderActions(track) })
1975
+ renderActions && /* @__PURE__ */ jsx28("div", { className: "edit flex text-base w-6 items-center justify-center mr-2 cursor-pointer", children: renderActions(track) })
1754
1976
  ]
1755
1977
  }
1756
1978
  );
1757
1979
  }
1758
1980
 
1759
1981
  // src/genre-tree/artist/ArtistSongs.tsx
1760
- import { Fragment as Fragment10, jsx as jsx27, jsxs as jsxs21 } from "react/jsx-runtime";
1982
+ import { Fragment as Fragment11, jsx as jsx29, jsxs as jsxs22 } from "react/jsx-runtime";
1761
1983
  var SongPageSchema2 = PaginatedResponseSchema2(SongDetailedSchema2);
1762
1984
  var SORTS = [
1763
1985
  { ordering: "title", label: "Title" },
1764
1986
  { ordering: "-albumYear", label: "Year" }
1765
1987
  ];
1766
1988
  function ArtistSongs({ artist, getBackendBaseUrl, renderArtistLink, renderSongLink }) {
1767
- const [page, setPage] = useState6(1);
1768
- const [ordering, setOrdering] = useState6("title");
1989
+ const [page, setPage] = useState7(1);
1990
+ const [ordering, setOrdering] = useState7("title");
1769
1991
  const { fetch } = useFetchWrapper2(getBackendBaseUrl);
1770
1992
  const { playNewTrackListFromArtist } = useTrackList4();
1771
1993
  const { playerTrackObject } = usePlayer8();
@@ -1790,21 +2012,21 @@ function ArtistSongs({ artist, getBackendBaseUrl, renderArtistLink, renderSongLi
1790
2012
  event.stopPropagation();
1791
2013
  void play(song.uuid);
1792
2014
  };
1793
- return /* @__PURE__ */ jsx27(PanelSection5, { title: "Songs", children: isPending ? /* @__PURE__ */ jsx27(Skeleton3, { className: "h-14", "aria-busy": "true" }) : isError ? /* @__PURE__ */ jsx27("p", { className: "text-red-600", children: "Could not load songs." }) : data.results.length === 0 ? /* @__PURE__ */ jsx27("p", { className: "text-[var(--color-text-muted)]", children: "No songs yet." }) : /* @__PURE__ */ jsxs21(Fragment10, { children: [
1794
- /* @__PURE__ */ jsxs21("div", { className: "flex flex-wrap items-center justify-between gap-2 mb-2", children: [
1795
- /* @__PURE__ */ jsxs21(
2015
+ return /* @__PURE__ */ jsx29(PanelSection5, { title: "Songs", children: isPending ? /* @__PURE__ */ jsx29(Skeleton4, { className: "h-14", "aria-busy": "true" }) : isError ? /* @__PURE__ */ jsx29("p", { className: "text-red-600", children: "Could not load songs." }) : data.results.length === 0 ? /* @__PURE__ */ jsx29("p", { className: "text-[var(--color-text-muted)]", children: "No songs yet." }) : /* @__PURE__ */ jsxs22(Fragment11, { children: [
2016
+ /* @__PURE__ */ jsxs22("div", { className: "flex flex-wrap items-center justify-between gap-2 mb-2", children: [
2017
+ /* @__PURE__ */ jsxs22(
1796
2018
  "button",
1797
2019
  {
1798
2020
  type: "button",
1799
2021
  onClick: () => void play(),
1800
2022
  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",
1801
2023
  children: [
1802
- /* @__PURE__ */ jsx27(Play2, { className: "h-3.5 w-3.5", "aria-hidden": "true" }),
2024
+ /* @__PURE__ */ jsx29(Play2, { className: "h-3.5 w-3.5", "aria-hidden": "true" }),
1803
2025
  "Play all"
1804
2026
  ]
1805
2027
  }
1806
2028
  ),
1807
- /* @__PURE__ */ jsx27("div", { role: "group", "aria-label": "Sort songs by", className: "flex gap-1 text-xs", children: SORTS.map((sort) => /* @__PURE__ */ jsx27(
2029
+ /* @__PURE__ */ jsx29("div", { role: "group", "aria-label": "Sort songs by", className: "flex gap-1 text-xs", children: SORTS.map((sort) => /* @__PURE__ */ jsx29(
1808
2030
  "button",
1809
2031
  {
1810
2032
  type: "button",
@@ -1819,7 +2041,7 @@ function ArtistSongs({ artist, getBackendBaseUrl, renderArtistLink, renderSongLi
1819
2041
  sort.ordering
1820
2042
  )) })
1821
2043
  ] }),
1822
- /* @__PURE__ */ jsx27("ul", { className: "overflow-hidden mb-2 bg-zinc-950 rounded-lg", children: data.results.map((song, index) => /* @__PURE__ */ jsx27("li", { onClickCapture: (event) => handleRowClickCapture(event, song), children: /* @__PURE__ */ jsx27(
2044
+ /* @__PURE__ */ jsx29("ul", { className: "overflow-hidden mb-2 bg-zinc-950 rounded-lg", children: data.results.map((song, index) => /* @__PURE__ */ jsx29("li", { onClickCapture: (event) => handleRowClickCapture(event, song), children: /* @__PURE__ */ jsx29(
1823
2045
  TrackItem,
1824
2046
  {
1825
2047
  track: song,
@@ -1828,12 +2050,12 @@ function ArtistSongs({ artist, getBackendBaseUrl, renderArtistLink, renderSongLi
1828
2050
  renderSongLink
1829
2051
  }
1830
2052
  ) }, song.uuid)) }),
1831
- /* @__PURE__ */ jsx27(Pagination, { currentPage: page, totalPages: data.totalPages, onPageChange: setPage })
2053
+ /* @__PURE__ */ jsx29(Pagination, { currentPage: page, totalPages: data.totalPages, onPageChange: setPage })
1832
2054
  ] }) });
1833
2055
  }
1834
2056
 
1835
2057
  // src/genre-tree/artist/useArtistPanel.tsx
1836
- import { Fragment as Fragment11, jsx as jsx28, jsxs as jsxs22 } from "react/jsx-runtime";
2058
+ import { Fragment as Fragment12, jsx as jsx30, jsxs as jsxs23 } from "react/jsx-runtime";
1837
2059
  function useArtistPanel({
1838
2060
  uuid,
1839
2061
  scope,
@@ -1854,26 +2076,26 @@ function useArtistPanel({
1854
2076
  return {
1855
2077
  ...panel,
1856
2078
  title: "Artist",
1857
- content: /* @__PURE__ */ jsxs22("div", { "aria-busy": "true", className: "flex flex-col gap-2.5 px-6 py-2.5", children: [
1858
- /* @__PURE__ */ jsx28(Skeleton4, { className: "h-3 w-16" }),
1859
- /* @__PURE__ */ jsx28(Skeleton4, { className: "h-4 w-32" }),
1860
- /* @__PURE__ */ jsx28(Skeleton4, { className: "h-3 w-16" }),
1861
- /* @__PURE__ */ jsx28(Skeleton4, { className: "h-4 w-24" }),
1862
- /* @__PURE__ */ jsx28(Skeleton4, { className: "h-16 mt-2.5" }),
1863
- /* @__PURE__ */ jsx28(Skeleton4, { className: "h-16" })
2079
+ content: /* @__PURE__ */ jsxs23("div", { "aria-busy": "true", className: "flex flex-col gap-2.5 px-6 py-2.5", children: [
2080
+ /* @__PURE__ */ jsx30(Skeleton5, { className: "h-3 w-16" }),
2081
+ /* @__PURE__ */ jsx30(Skeleton5, { className: "h-4 w-32" }),
2082
+ /* @__PURE__ */ jsx30(Skeleton5, { className: "h-3 w-16" }),
2083
+ /* @__PURE__ */ jsx30(Skeleton5, { className: "h-4 w-24" }),
2084
+ /* @__PURE__ */ jsx30(Skeleton5, { className: "h-16 mt-2.5" }),
2085
+ /* @__PURE__ */ jsx30(Skeleton5, { className: "h-16" })
1864
2086
  ] })
1865
2087
  };
1866
2088
  case "not-found":
1867
2089
  return {
1868
2090
  ...panel,
1869
2091
  title: "Artist not found",
1870
- content: /* @__PURE__ */ jsx28("p", { className: "px-6 py-2.5 text-[var(--color-text-muted)]", children: "This artist does not exist." })
2092
+ content: /* @__PURE__ */ jsx30("p", { className: "px-6 py-2.5 text-[var(--color-text-muted)]", children: "This artist does not exist." })
1871
2093
  };
1872
2094
  case "error":
1873
2095
  return {
1874
2096
  ...panel,
1875
2097
  title: "Artist",
1876
- content: /* @__PURE__ */ jsx28("p", { className: "px-6 py-2.5 text-red-600", children: "Could not load this artist." })
2098
+ content: /* @__PURE__ */ jsx30("p", { className: "px-6 py-2.5 text-red-600", children: "Could not load this artist." })
1877
2099
  };
1878
2100
  case "success": {
1879
2101
  const { artist } = result;
@@ -1882,13 +2104,13 @@ function useArtistPanel({
1882
2104
  return {
1883
2105
  ...panel,
1884
2106
  title: artist.name,
1885
- content: /* @__PURE__ */ jsxs22(Fragment11, { children: [
1886
- shareUrl && /* @__PURE__ */ jsx28("div", { className: "genre-action-band border-b border-[var(--color-border)]", children: /* @__PURE__ */ jsx28("div", { className: "flex flex-wrap gap-1 px-3.5 py-3", children: shareTarget && /* @__PURE__ */ jsx28(ShareActionButton, { target: shareTarget, url: shareUrl }) }) }),
1887
- artist.image && /* @__PURE__ */ jsx28(ArtistPhoto, { image: artist.image, name: artist.name }),
1888
- /* @__PURE__ */ jsx28(ArtistHeader, { artist }),
1889
- /* @__PURE__ */ jsx28(ArtistMembers, { title: "Members", memberships: artist.members, renderArtistLink }),
1890
- /* @__PURE__ */ jsx28(ArtistMembers, { title: "Member of", memberships: artist.memberOf, renderArtistLink }),
1891
- artist.tracksCount > 0 && /* @__PURE__ */ jsx28(
2107
+ content: /* @__PURE__ */ jsxs23(Fragment12, { children: [
2108
+ shareUrl && /* @__PURE__ */ jsx30("div", { className: "genre-action-band border-b border-[var(--color-border)]", children: /* @__PURE__ */ jsx30("div", { className: "flex flex-wrap gap-1 px-3.5 py-3", children: shareTarget && /* @__PURE__ */ jsx30(ShareActionButton, { target: shareTarget, url: shareUrl }) }) }),
2109
+ artist.image && /* @__PURE__ */ jsx30(ArtistPhoto, { image: artist.image, name: artist.name }),
2110
+ /* @__PURE__ */ jsx30(ArtistHeader, { artist }),
2111
+ /* @__PURE__ */ jsx30(ArtistMembers, { title: "Members", memberships: artist.members, renderArtistLink }),
2112
+ /* @__PURE__ */ jsx30(ArtistMembers, { title: "Member of", memberships: artist.memberOf, renderArtistLink }),
2113
+ artist.tracksCount > 0 && /* @__PURE__ */ jsx30(
1892
2114
  ArtistSongs,
1893
2115
  {
1894
2116
  artist: { uuid: artist.uuid, name: artist.name },
@@ -1898,22 +2120,22 @@ function useArtistPanel({
1898
2120
  }
1899
2121
  )
1900
2122
  ] }),
1901
- extraContent: artist.albums.length > 0 ? /* @__PURE__ */ jsx28(ArtistDiscography, { albums: artist.albums }) : void 0
2123
+ extraContent: artist.albums.length > 0 ? /* @__PURE__ */ jsx30(ArtistDiscography, { albums: artist.albums }) : void 0
1902
2124
  };
1903
2125
  }
1904
2126
  }
1905
2127
  }
1906
2128
 
1907
2129
  // src/genre-tree/song/useSongPanel.tsx
1908
- import { Skeleton as Skeleton6 } from "@behindthemusictree/ui";
2130
+ import { Skeleton as Skeleton7 } from "@behindthemusictree/ui";
1909
2131
  import {
1910
2132
  useSong
1911
2133
  } from "@behindthemusictree/app-kit/genre-tree";
1912
2134
 
1913
2135
  // src/genre-tree/song/SongPanelContent.tsx
1914
- import { useState as useState7 } from "react";
2136
+ import { useState as useState8 } from "react";
1915
2137
  import { Pause as Pause2, Play as Play3 } from "lucide-react";
1916
- import { FieldList as FieldList2, PanelSection as PanelSection6, Skeleton as Skeleton5 } from "@behindthemusictree/ui";
2138
+ import { FieldList as FieldList2, PanelSection as PanelSection6, Skeleton as Skeleton6 } from "@behindthemusictree/ui";
1917
2139
  import { PlayStates as PlayStates3, usePlayer as usePlayer9 } from "@behindthemusictree/app-kit/player";
1918
2140
  import {
1919
2141
  ArtistNames as ArtistNames3,
@@ -1922,40 +2144,40 @@ import {
1922
2144
  useSongHistory,
1923
2145
  useTrackList as useTrackList5
1924
2146
  } from "@behindthemusictree/app-kit/genre-tree";
1925
- import { Fragment as Fragment12, jsx as jsx29, jsxs as jsxs23 } from "react/jsx-runtime";
2147
+ import { Fragment as Fragment13, jsx as jsx31, jsxs as jsxs24 } from "react/jsx-runtime";
1926
2148
  var numberFormat = new Intl.NumberFormat("en");
1927
2149
  var dateFormat = new Intl.DateTimeFormat("en", { dateStyle: "medium" });
1928
2150
  var externalLink = "text-[var(--color-primary)] hover:underline focus-visible:underline";
1929
2151
  function SongHistory({ uuid, getBackendBaseUrl }) {
1930
- const [open, setOpen] = useState7(false);
2152
+ const [open, setOpen] = useState8(false);
1931
2153
  const history = useSongHistory(uuid, getBackendBaseUrl, open);
1932
2154
  let body = null;
1933
- if (history.status === "loading") body = /* @__PURE__ */ jsx29(Skeleton5, { className: "h-10", "aria-busy": "true" });
1934
- else if (history.status === "error") body = /* @__PURE__ */ jsx29("p", { className: "text-red-600", children: "Could not load the history." });
2155
+ if (history.status === "loading") body = /* @__PURE__ */ jsx31(Skeleton6, { className: "h-10", "aria-busy": "true" });
2156
+ else if (history.status === "error") body = /* @__PURE__ */ jsx31("p", { className: "text-red-600", children: "Could not load the history." });
1935
2157
  else if (history.status === "success")
1936
- body = history.entries.length === 0 ? /* @__PURE__ */ jsx29("p", { className: "text-[var(--color-text-muted)]", children: "No changes yet." }) : /* @__PURE__ */ jsx29("ul", { className: "divide-y divide-[var(--color-border)]", children: history.entries.map((entry) => /* @__PURE__ */ jsxs23("li", { className: "py-1.5 text-sm", children: [
1937
- /* @__PURE__ */ jsx29("span", { className: "font-medium", children: entry.action }),
1938
- entry.oldValue !== null || entry.newValue !== null ? /* @__PURE__ */ jsxs23("span", { className: "text-[var(--color-text-muted)]", children: [
2158
+ body = history.entries.length === 0 ? /* @__PURE__ */ jsx31("p", { className: "text-[var(--color-text-muted)]", children: "No changes yet." }) : /* @__PURE__ */ jsx31("ul", { className: "divide-y divide-[var(--color-border)]", children: history.entries.map((entry) => /* @__PURE__ */ jsxs24("li", { className: "py-1.5 text-sm", children: [
2159
+ /* @__PURE__ */ jsx31("span", { className: "font-medium", children: entry.action }),
2160
+ entry.oldValue !== null || entry.newValue !== null ? /* @__PURE__ */ jsxs24("span", { className: "text-[var(--color-text-muted)]", children: [
1939
2161
  " ",
1940
2162
  "\u2014 ",
1941
2163
  entry.oldValue ?? "none",
1942
2164
  " \u2192 ",
1943
2165
  entry.newValue ?? "none"
1944
2166
  ] }) : null,
1945
- /* @__PURE__ */ jsxs23("span", { className: "block text-xs text-[var(--color-text-muted)]", children: [
2167
+ /* @__PURE__ */ jsxs24("span", { className: "block text-xs text-[var(--color-text-muted)]", children: [
1946
2168
  entry.actorPseudo ?? "Anonymous",
1947
2169
  " \xB7 ",
1948
2170
  dateFormat.format(new Date(entry.createdOn))
1949
2171
  ] })
1950
2172
  ] }, entry.uuid)) });
1951
- return /* @__PURE__ */ jsxs23(
2173
+ return /* @__PURE__ */ jsxs24(
1952
2174
  "details",
1953
2175
  {
1954
2176
  onToggle: (event) => setOpen(event.currentTarget.open),
1955
2177
  className: "px-6 pb-2.5 mt-2.5 border-t border-[var(--color-border)]",
1956
2178
  children: [
1957
- /* @__PURE__ */ jsx29("summary", { className: "mt-2.5 cursor-pointer text-[0.6875rem] font-semibold uppercase text-[var(--color-text-muted)]", children: "History" }),
1958
- /* @__PURE__ */ jsx29("div", { className: "mt-1.5", children: body })
2179
+ /* @__PURE__ */ jsx31("summary", { className: "mt-2.5 cursor-pointer text-[0.6875rem] font-semibold uppercase text-[var(--color-text-muted)]", children: "History" }),
2180
+ /* @__PURE__ */ jsx31("div", { className: "mt-1.5", children: body })
1959
2181
  ]
1960
2182
  }
1961
2183
  );
@@ -1981,13 +2203,13 @@ function SongPanelContent({
1981
2203
  const path = genrePath.length > 0 ? genrePath : null;
1982
2204
  fields.push({
1983
2205
  label: "Genre",
1984
- value: path ? /* @__PURE__ */ jsx29("span", { className: "flex flex-wrap items-center gap-x-1", children: path.map((genrePlaylist, index) => /* @__PURE__ */ jsxs23("span", { children: [
1985
- index > 0 && /* @__PURE__ */ jsx29("span", { "aria-hidden": "true", children: "\u203A " }),
1986
- /* @__PURE__ */ jsx29("button", { type: "button", onClick: () => onSelectGenre(genrePlaylist), className: "hover:underline", children: genrePlaylist.name })
2206
+ value: path ? /* @__PURE__ */ jsx31("span", { className: "flex flex-wrap items-center gap-x-1", children: path.map((genrePlaylist, index) => /* @__PURE__ */ jsxs24("span", { children: [
2207
+ index > 0 && /* @__PURE__ */ jsx31("span", { "aria-hidden": "true", children: "\u203A " }),
2208
+ /* @__PURE__ */ jsx31("button", { type: "button", onClick: () => onSelectGenre(genrePlaylist), className: "hover:underline", children: genrePlaylist.name })
1987
2209
  ] }, genrePlaylist.uuid)) }) : song.genre.name
1988
2210
  });
1989
2211
  } else {
1990
- fields.push({ label: "Genre", value: /* @__PURE__ */ jsx29("span", { className: "text-[var(--color-text-muted)]", children: "No genre" }) });
2212
+ fields.push({ label: "Genre", value: /* @__PURE__ */ jsx31("span", { className: "text-[var(--color-text-muted)]", children: "No genre" }) });
1991
2213
  }
1992
2214
  if (song.firstReleaseYear) fields.push({ label: "First released", value: song.firstReleaseYear });
1993
2215
  const viewCount = song.youtubeVideo?.viewCount;
@@ -1997,11 +2219,11 @@ function SongPanelContent({
1997
2219
  const links = [];
1998
2220
  if (musicBrainzUrl)
1999
2221
  links.push(
2000
- /* @__PURE__ */ jsx29("a", { href: musicBrainzUrl, target: "_blank", rel: "noopener noreferrer", className: externalLink, children: "MusicBrainz" }, "mb")
2222
+ /* @__PURE__ */ jsx31("a", { href: musicBrainzUrl, target: "_blank", rel: "noopener noreferrer", className: externalLink, children: "MusicBrainz" }, "mb")
2001
2223
  );
2002
2224
  if (song.youtubeVideo)
2003
2225
  links.push(
2004
- /* @__PURE__ */ jsx29(
2226
+ /* @__PURE__ */ jsx31(
2005
2227
  "a",
2006
2228
  {
2007
2229
  href: `https://www.youtube.com/watch?v=${encodeURIComponent(song.youtubeVideo.id)}`,
@@ -2016,33 +2238,33 @@ function SongPanelContent({
2016
2238
  if (links.length > 0)
2017
2239
  fields.push({
2018
2240
  label: "Links",
2019
- value: /* @__PURE__ */ jsx29("span", { className: "flex gap-3", children: links })
2241
+ value: /* @__PURE__ */ jsx31("span", { className: "flex gap-3", children: links })
2020
2242
  });
2021
- return /* @__PURE__ */ jsxs23(Fragment12, { children: [
2022
- /* @__PURE__ */ jsx29("div", { className: "genre-action-band border-b border-[var(--color-border)]", children: /* @__PURE__ */ jsxs23("div", { className: "flex flex-wrap gap-1 px-3.5 py-3", children: [
2023
- unplayableReason ? /* @__PURE__ */ jsx29("p", { className: "flex items-center px-2.5 text-sm text-[var(--color-text-muted)]", children: unplayableReason }) : /* @__PURE__ */ jsxs23(
2243
+ return /* @__PURE__ */ jsxs24(Fragment13, { children: [
2244
+ /* @__PURE__ */ jsx31("div", { className: "genre-action-band border-b border-[var(--color-border)]", children: /* @__PURE__ */ jsxs24("div", { className: "flex flex-wrap gap-1 px-3.5 py-3", children: [
2245
+ unplayableReason ? /* @__PURE__ */ jsx31("p", { className: "flex items-center px-2.5 text-sm text-[var(--color-text-muted)]", children: unplayableReason }) : /* @__PURE__ */ jsxs24(
2024
2246
  "button",
2025
2247
  {
2026
2248
  type: "button",
2027
2249
  onClick: () => isCurrent ? handlePlayPauseAction() : playNewTrackListFromTrackUuid(song, scope),
2028
2250
  className: "group flex flex-col items-center gap-1 w-16 text-xs text-center text-[var(--color-text)] focus-visible:outline-none",
2029
2251
  children: [
2030
- /* @__PURE__ */ jsx29("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__ */ jsx29(Pause2, { "aria-hidden": "true", className: "w-5 h-5" }) : /* @__PURE__ */ jsx29(Play3, { "aria-hidden": "true", className: "w-5 h-5" }) }),
2252
+ /* @__PURE__ */ jsx31("span", { className: "flex items-center justify-center w-10 h-10 rounded-full group-focus-visible:ring-2 group-focus-visible:ring-[var(--color-focus-ring)] bg-[var(--color-surface-muted)] transition-colors group-hover:bg-[var(--color-border)]", children: isPlaying ? /* @__PURE__ */ jsx31(Pause2, { "aria-hidden": "true", className: "w-5 h-5" }) : /* @__PURE__ */ jsx31(Play3, { "aria-hidden": "true", className: "w-5 h-5" }) }),
2031
2253
  isPlaying ? "Pause" : "Play"
2032
2254
  ]
2033
2255
  }
2034
2256
  ),
2035
- shareUrl && /* @__PURE__ */ jsx29(ShareActionButton, { target: shareTarget, url: shareUrl })
2257
+ shareUrl && /* @__PURE__ */ jsx31(ShareActionButton, { target: shareTarget, url: shareUrl })
2036
2258
  ] }) }),
2037
- song.artists && song.artists.length > 0 && /* @__PURE__ */ jsx29("p", { className: "px-6 pt-2.5 text-[var(--color-text-muted)]", children: /* @__PURE__ */ jsx29(ArtistNames3, { artists: song.artists, renderArtistLink }) }),
2038
- /* @__PURE__ */ jsx29(FieldList2, { items: fields }),
2039
- song.essentialGenres && song.essentialGenres.length > 0 && /* @__PURE__ */ jsx29(PanelSection6, { title: "Essential in", children: /* @__PURE__ */ jsx29("ul", { children: song.essentialGenres.map((genre) => /* @__PURE__ */ jsx29("li", { className: "py-0.5", children: genre.name }, genre.uuid)) }) }),
2040
- /* @__PURE__ */ jsx29(SongHistory, { uuid: song.uuid, getBackendBaseUrl })
2259
+ song.artists && song.artists.length > 0 && /* @__PURE__ */ jsx31("p", { className: "px-6 pt-2.5 text-[var(--color-text-muted)]", children: /* @__PURE__ */ jsx31(ArtistNames3, { artists: song.artists, renderArtistLink }) }),
2260
+ /* @__PURE__ */ jsx31(FieldList2, { items: fields }),
2261
+ song.essentialGenres && song.essentialGenres.length > 0 && /* @__PURE__ */ jsx31(PanelSection6, { title: "Essential in", children: /* @__PURE__ */ jsx31("ul", { children: song.essentialGenres.map((genre) => /* @__PURE__ */ jsx31("li", { className: "py-0.5", children: genre.name }, genre.uuid)) }) }),
2262
+ /* @__PURE__ */ jsx31(SongHistory, { uuid: song.uuid, getBackendBaseUrl })
2041
2263
  ] });
2042
2264
  }
2043
2265
 
2044
2266
  // src/genre-tree/song/useSongPanel.tsx
2045
- import { jsx as jsx30, jsxs as jsxs24 } from "react/jsx-runtime";
2267
+ import { jsx as jsx32, jsxs as jsxs25 } from "react/jsx-runtime";
2046
2268
  function useSongPanel({
2047
2269
  uuid,
2048
2270
  scope,
@@ -2064,24 +2286,24 @@ function useSongPanel({
2064
2286
  return {
2065
2287
  ...panel,
2066
2288
  title: "Song",
2067
- content: /* @__PURE__ */ jsxs24("div", { "aria-busy": "true", className: "flex flex-col gap-2.5 px-6 py-2.5", children: [
2068
- /* @__PURE__ */ jsx30(Skeleton6, { className: "h-3 w-16" }),
2069
- /* @__PURE__ */ jsx30(Skeleton6, { className: "h-4 w-32" }),
2070
- /* @__PURE__ */ jsx30(Skeleton6, { className: "h-3 w-16" }),
2071
- /* @__PURE__ */ jsx30(Skeleton6, { className: "h-4 w-24" })
2289
+ content: /* @__PURE__ */ jsxs25("div", { "aria-busy": "true", className: "flex flex-col gap-2.5 px-6 py-2.5", children: [
2290
+ /* @__PURE__ */ jsx32(Skeleton7, { className: "h-3 w-16" }),
2291
+ /* @__PURE__ */ jsx32(Skeleton7, { className: "h-4 w-32" }),
2292
+ /* @__PURE__ */ jsx32(Skeleton7, { className: "h-3 w-16" }),
2293
+ /* @__PURE__ */ jsx32(Skeleton7, { className: "h-4 w-24" })
2072
2294
  ] })
2073
2295
  };
2074
2296
  case "not-found":
2075
2297
  return {
2076
2298
  ...panel,
2077
2299
  title: "Song not found",
2078
- content: /* @__PURE__ */ jsx30("p", { className: "px-6 py-2.5 text-[var(--color-text-muted)]", children: "This song does not exist." })
2300
+ content: /* @__PURE__ */ jsx32("p", { className: "px-6 py-2.5 text-[var(--color-text-muted)]", children: "This song does not exist." })
2079
2301
  };
2080
2302
  case "error":
2081
2303
  return {
2082
2304
  ...panel,
2083
2305
  title: "Song",
2084
- content: /* @__PURE__ */ jsx30("p", { className: "px-6 py-2.5 text-red-600", children: "Could not load this song." })
2306
+ content: /* @__PURE__ */ jsx32("p", { className: "px-6 py-2.5 text-red-600", children: "Could not load this song." })
2085
2307
  };
2086
2308
  case "success": {
2087
2309
  const { song } = result;
@@ -2089,7 +2311,7 @@ function useSongPanel({
2089
2311
  return {
2090
2312
  ...panel,
2091
2313
  title: song.title,
2092
- content: /* @__PURE__ */ jsx30(
2314
+ content: /* @__PURE__ */ jsx32(
2093
2315
  SongPanelContent,
2094
2316
  {
2095
2317
  song,
@@ -2109,12 +2331,12 @@ function useSongPanel({
2109
2331
  // src/genre-tree/SizeMetricToggle.tsx
2110
2332
  import { MdMusicNote, MdVisibility } from "react-icons/md";
2111
2333
  import { SegmentedControl } from "@behindthemusictree/ui";
2112
- import { jsx as jsx31 } from "react/jsx-runtime";
2334
+ import { jsx as jsx33 } from "react/jsx-runtime";
2113
2335
  function SizeMetricToggle({
2114
2336
  value,
2115
2337
  onChange
2116
2338
  }) {
2117
- return /* @__PURE__ */ jsx31(
2339
+ return /* @__PURE__ */ jsx33(
2118
2340
  SegmentedControl,
2119
2341
  {
2120
2342
  variant: "floating",
@@ -2122,15 +2344,15 @@ function SizeMetricToggle({
2122
2344
  value,
2123
2345
  onChange,
2124
2346
  options: [
2125
- { value: "songs", label: /* @__PURE__ */ jsx31(MdMusicNote, { size: 18, "aria-hidden": true }), "aria-label": "Size by songs", title: "Node size by song count" },
2126
- { value: "views", label: /* @__PURE__ */ jsx31(MdVisibility, { size: 18, "aria-hidden": true }), "aria-label": "Size by views", title: "Node size by song views" }
2347
+ { value: "songs", label: /* @__PURE__ */ jsx33(MdMusicNote, { size: 18, "aria-hidden": true }), "aria-label": "Size by songs", title: "Node size by song count" },
2348
+ { value: "views", label: /* @__PURE__ */ jsx33(MdVisibility, { size: 18, "aria-hidden": true }), "aria-label": "Size by views", title: "Node size by song views" }
2127
2349
  ]
2128
2350
  }
2129
2351
  );
2130
2352
  }
2131
2353
 
2132
2354
  // src/genre-tree/GenreTreeView.tsx
2133
- import { Fragment as Fragment13, jsx as jsx32, jsxs as jsxs25 } from "react/jsx-runtime";
2355
+ import { Fragment as Fragment14, jsx as jsx34, jsxs as jsxs26 } from "react/jsx-runtime";
2134
2356
  var NO_POP_ROOT_TITLE = "This genre tree has no 'Mainstream Pop' root yet";
2135
2357
  var REGIONS_CLASS = "[--gtv-center-left:var(--layout-center-left)] [--gtv-center-right:var(--layout-center-right)] [--gtv-center-top:var(--layout-band-top)]";
2136
2358
  var noop = () => {
@@ -2143,8 +2365,8 @@ function GenreTreeView({
2143
2365
  getBackendBaseUrl,
2144
2366
  additionalActions,
2145
2367
  viewMode: controlledViewMode,
2146
- selectedNodeId: controlledSelectedNodeId,
2147
- onSelectedNodeIdChange,
2368
+ selectedGenreUuid: controlledSelectedGenreUuid,
2369
+ onSelectedGenreUuidChange,
2148
2370
  defaultViewport,
2149
2371
  viewportRestore,
2150
2372
  onViewportChange,
@@ -2165,6 +2387,7 @@ function GenreTreeView({
2165
2387
  onArtistExpandedChange,
2166
2388
  getShareUrl,
2167
2389
  extraSearchGroups = [],
2390
+ SearchGroups,
2168
2391
  onSearchQueryChange,
2169
2392
  onSearchFocus,
2170
2393
  artistSearch = true,
@@ -2193,19 +2416,19 @@ function GenreTreeView({
2193
2416
  }) {
2194
2417
  const getShareUrlRef = useRef3(getShareUrl);
2195
2418
  getShareUrlRef.current = getShareUrl;
2196
- const [internalArtistUuid, setInternalArtistUuid] = useState8(null);
2419
+ const [internalArtistUuid, setInternalArtistUuid] = useState9(null);
2197
2420
  const isArtistControlled = controlledArtistUuid !== void 0;
2198
2421
  const artistUuid = isArtistControlled ? controlledArtistUuid : internalArtistUuid;
2199
- const [internalSongUuid, setInternalSongUuid] = useState8(null);
2422
+ const [internalSongUuid, setInternalSongUuid] = useState9(null);
2200
2423
  const isSongControlled = controlledSongUuid !== void 0;
2201
2424
  const songUuid = isSongControlled ? controlledSongUuid : internalSongUuid;
2202
2425
  const changeSong = (uuid) => {
2203
2426
  if (!isSongControlled) setInternalSongUuid(uuid);
2204
2427
  onSongUuidChange?.(uuid);
2205
2428
  };
2206
- const [internalSizeMetric, setInternalSizeMetric] = useState8("songs");
2429
+ const [internalSizeMetric, setInternalSizeMetric] = useState9("songs");
2207
2430
  const sizeMetric = controlledSizeMetric ?? internalSizeMetric;
2208
- const deferredSizeMetric = useDeferredValue(sizeMetric);
2431
+ const deferredSizeMetric = useDeferredValue2(sizeMetric);
2209
2432
  const changeSizeMetric = (metric) => {
2210
2433
  if (controlledSizeMetric === void 0) setInternalSizeMetric(metric);
2211
2434
  onSizeMetricChange?.(metric);
@@ -2220,6 +2443,8 @@ function GenreTreeView({
2220
2443
  genrePlaylistByNodeId,
2221
2444
  visibleGenrePlaylists,
2222
2445
  groupedGenrePlaylistsByRoot,
2446
+ selectedGenreUuid,
2447
+ selectGenre,
2223
2448
  selectedNodeId,
2224
2449
  selectedGenrePlaylist,
2225
2450
  selectNode,
@@ -2236,13 +2461,13 @@ function GenreTreeView({
2236
2461
  scope,
2237
2462
  getBackendBaseUrl,
2238
2463
  viewMode: controlledViewMode,
2239
- selectedNodeId: controlledSelectedNodeId,
2240
- onSelectedNodeIdChange,
2464
+ selectedGenreUuid: controlledSelectedGenreUuid,
2465
+ onSelectedGenreUuidChange,
2241
2466
  criteriaOverviewSchema,
2242
2467
  artistUuid
2243
2468
  });
2244
2469
  const restoreRevision = viewportRestore?.revision;
2245
- const [stackedRestoreOwner, setStackedRestoreOwner] = useState8({ revision: restoreRevision, rootUuid: selectedGenrePlaylist?.root.uuid });
2470
+ const [stackedRestoreOwner, setStackedRestoreOwner] = useState9({ revision: restoreRevision, rootUuid: selectedGenrePlaylist?.root.uuid });
2246
2471
  let restoreRootUuid = stackedRestoreOwner.rootUuid;
2247
2472
  if (stackedRestoreOwner.revision !== restoreRevision) {
2248
2473
  restoreRootUuid = selectedGenrePlaylist?.root.uuid;
@@ -2258,29 +2483,29 @@ function GenreTreeView({
2258
2483
  const canSizeByViews = !isArtistFiltered && hasViewsCount(visibleGenrePlaylists);
2259
2484
  const artistResult = useArtist2(artistUuid, artistSearchBaseUrl ?? getBackendBaseUrl);
2260
2485
  const filterArtistName = artistResult.status === "success" ? artistResult.artist.name : null;
2261
- const filterArtist = useMemo7(
2486
+ const filterArtist = useMemo8(
2262
2487
  () => artistUuid !== null && filterArtistName !== null ? { uuid: artistUuid, name: filterArtistName } : null,
2263
2488
  [artistUuid, filterArtistName]
2264
2489
  );
2265
2490
  const hasSelectedGenreNode = selectedGenrePlaylist !== void 0 && visibleGenrePlaylists.some(({ uuid }) => uuid === selectedGenrePlaylist.uuid);
2266
2491
  const playback = useGenrePlaylistPlayback({ scope, genrePlaylists: visibleGenrePlaylists, artist: filterArtist });
2267
2492
  const openArtist = (uuid) => {
2268
- if (selectedNodeId !== null) clearSelection();
2493
+ if (selectedGenreUuid !== null) clearSelection();
2269
2494
  if (songUuid !== null) changeSong(null);
2270
2495
  changeArtist(uuid);
2271
2496
  };
2272
2497
  const openSong = (uuid) => {
2273
- if (selectedNodeId !== null) clearSelection();
2498
+ if (selectedGenreUuid !== null) clearSelection();
2274
2499
  changeSong(uuid);
2275
2500
  };
2276
2501
  const openSongRef = useRef3(openSong);
2277
2502
  openSongRef.current = openSong;
2278
2503
  const resolvedRenderSongLink = useCallback6(
2279
- (song) => renderSongLink ? renderSongLink(song) : /* @__PURE__ */ jsx32("button", { type: "button", onClick: () => openSongRef.current(song.uuid), className: "hover:underline", children: song.title }),
2504
+ (song) => renderSongLink ? renderSongLink(song) : /* @__PURE__ */ jsx34("button", { type: "button", onClick: () => openSongRef.current(song.uuid), className: "hover:underline", children: song.title }),
2280
2505
  [renderSongLink]
2281
2506
  );
2282
- const [expandedSongUuid, setExpandedSongUuid] = useState8(null);
2283
- const [expandedArtistUuid, setExpandedArtistUuid] = useState8(null);
2507
+ const [expandedSongUuid, setExpandedSongUuid] = useState9(null);
2508
+ const [expandedArtistUuid, setExpandedArtistUuid] = useState9(null);
2284
2509
  const artistBaseUrl = artistSearchBaseUrl ?? getBackendBaseUrl;
2285
2510
  const songBaseUrl = getBackendBaseUrl;
2286
2511
  const artistPanel = useArtistPanel({
@@ -2288,7 +2513,7 @@ function GenreTreeView({
2288
2513
  scope,
2289
2514
  getBackendBaseUrl: artistBaseUrl,
2290
2515
  getShareUrl: getShareUrlRef.current,
2291
- renderArtistLink: renderArtistLink ?? ((artist) => artist.isSpecialPurpose ? artist.name : /* @__PURE__ */ jsx32("button", { type: "button", onClick: () => openArtist(artist.uuid), className: "hover:underline", children: artist.name })),
2516
+ renderArtistLink: renderArtistLink ?? ((artist) => artist.isSpecialPurpose ? artist.name : /* @__PURE__ */ jsx34("button", { type: "button", onClick: () => openArtist(artist.uuid), className: "hover:underline", children: artist.name })),
2292
2517
  expanded: controlledArtistExpanded ?? (artistUuid !== null && expandedArtistUuid === artistUuid),
2293
2518
  onExpandedChange: (on) => {
2294
2519
  if (controlledArtistExpanded === void 0) setExpandedArtistUuid(on ? artistUuid : null);
@@ -2297,7 +2522,7 @@ function GenreTreeView({
2297
2522
  renderSongLink: resolvedRenderSongLink,
2298
2523
  onClose: () => changeArtist(null)
2299
2524
  });
2300
- const resolvedRenderArtistLink = renderArtistLink ?? ((artist) => artist.isSpecialPurpose ? artist.name : /* @__PURE__ */ jsx32("button", { type: "button", onClick: () => openArtist(artist.uuid), className: "hover:underline", children: artist.name }));
2525
+ const resolvedRenderArtistLink = renderArtistLink ?? ((artist) => artist.isSpecialPurpose ? artist.name : /* @__PURE__ */ jsx34("button", { type: "button", onClick: () => openArtist(artist.uuid), className: "hover:underline", children: artist.name }));
2301
2526
  const getGenrePath = (genre) => {
2302
2527
  const path = [];
2303
2528
  let current = visibleGenrePlaylists.find((playlist) => playlist.criteria?.uuid === genre.uuid);
@@ -2315,9 +2540,9 @@ function GenreTreeView({
2315
2540
  getShareUrl: getShareUrlRef.current,
2316
2541
  renderArtistLink: resolvedRenderArtistLink,
2317
2542
  getGenrePath,
2318
- onSelectGenre: (genrePlaylist) => {
2543
+ onSelectGenre: (genrePlaylist2) => {
2319
2544
  changeSong(null);
2320
- selectGenrePlaylist(genrePlaylist);
2545
+ selectGenrePlaylist(genrePlaylist2);
2321
2546
  },
2322
2547
  expanded: controlledSongExpanded ?? (songUuid !== null && expandedSongUuid === songUuid),
2323
2548
  onExpandedChange: (on) => {
@@ -2326,11 +2551,11 @@ function GenreTreeView({
2326
2551
  },
2327
2552
  onClose: () => changeSong(null)
2328
2553
  });
2329
- const builtInSongPanel = overlayPanel || hasSelectedGenreNode ? void 0 : songPanel;
2330
- const builtInArtistPanel = overlayPanel || hasSelectedGenreNode || builtInSongPanel ? void 0 : artistPanel;
2554
+ const builtInSongPanel = overlayPanel || selectedGenreUuid !== null ? void 0 : songPanel;
2555
+ const builtInArtistPanel = overlayPanel || selectedGenreUuid !== null || builtInSongPanel ? void 0 : artistPanel;
2331
2556
  const builtInPanel = builtInSongPanel ?? builtInArtistPanel;
2332
- const [uncontrolledCollapsed, setUncontrolledCollapsed] = useState8(false);
2333
- const [uncontrolledExpanded, setUncontrolledExpanded] = useState8(false);
2557
+ const [uncontrolledCollapsed, setUncontrolledCollapsed] = useState9(false);
2558
+ const [uncontrolledExpanded, setUncontrolledExpanded] = useState9(false);
2334
2559
  const isCollapsed = collapsed ?? uncontrolledCollapsed;
2335
2560
  const isExpanded = expanded ?? uncontrolledExpanded;
2336
2561
  const changeCollapsed = (next) => {
@@ -2343,11 +2568,11 @@ function GenreTreeView({
2343
2568
  };
2344
2569
  useEffect5(() => {
2345
2570
  setUncontrolledCollapsed(false);
2346
- }, [selectedNodeId, artistUuid, songUuid]);
2347
- const [isWheelRevealed, setIsWheelRevealed] = useState8(false);
2571
+ }, [selectedGenreUuid, artistUuid, songUuid]);
2572
+ const [isWheelRevealed, setIsWheelRevealed] = useState9(false);
2348
2573
  const usesHandoff = viewMode === "wheel" || viewMode === "pop-core";
2349
2574
  const isTreeReady = !isLoading && (!usesHandoff || isWheelRevealed);
2350
- const [resolveStyle, setResolveStyle] = useState8(null);
2575
+ const [resolveStyle, setResolveStyle] = useState9(null);
2351
2576
  const handleNodeStyleResolverChange = useCallback6(
2352
2577
  (resolver) => setResolveStyle(() => resolver),
2353
2578
  []
@@ -2356,162 +2581,99 @@ function GenreTreeView({
2356
2581
  const obscuredInset = useObscuredInset(panelRef);
2357
2582
  const hostPanel = { obscuredInset, onNodeStyleResolverChange: handleNodeStyleResolverChange };
2358
2583
  const nodeMetric = isArtistFiltered || !canSizeByViews ? "songs" : deferredSizeMetric;
2359
- const treeNodes = useMemo7(() => toTreeNodes(visibleGenrePlaylists, nodeMetric), [visibleGenrePlaylists, nodeMetric]);
2584
+ const treeNodes = useMemo8(() => toTreeNodes(visibleGenrePlaylists, nodeMetric), [visibleGenrePlaylists, nodeMetric]);
2360
2585
  const relations = useGenreNodeRelations(
2361
2586
  treeNodes,
2362
2587
  !overlayPanel && hasSelectedGenreNode ? selectedNodeId : null,
2363
2588
  resolveStyle
2364
2589
  );
2365
- const [selectionHistory, setSelectionHistory] = useState8({
2366
- current: selectedNodeId,
2367
- previous: null
2368
- });
2369
- if (selectionHistory.current !== selectedNodeId) {
2370
- setSelectionHistory({
2371
- current: selectedNodeId,
2372
- previous: selectionHistory.current ?? selectionHistory.previous
2373
- });
2590
+ const [selectionHistory, setSelectionHistory] = useState9({ current: selectedGenreUuid, previous: null });
2591
+ if (selectionHistory.current !== selectedGenreUuid) {
2592
+ setSelectionHistory({ current: selectedGenreUuid, previous: selectionHistory.current ?? selectionHistory.previous });
2374
2593
  }
2375
- const previousPlaylist = selectionHistory.previous !== null && selectionHistory.previous !== selectedNodeId ? genrePlaylistByNodeId.get(selectionHistory.previous) : void 0;
2376
- const handlePrevious = previousPlaylist && visibleGenrePlaylists.includes(previousPlaylist) ? () => handleGenreSearchSelect(previousPlaylist) : void 0;
2594
+ const handlePrevious = selectionHistory.previous ? () => selectGenre(selectionHistory.previous) : void 0;
2377
2595
  const { showPopup, hidePopup } = usePopup5();
2378
- const [searchText, setSearchText] = useState8("");
2379
- const artistGroup = useArtistSearchGroup(
2380
- artistBaseUrl,
2381
- searchText,
2382
- (artist) => {
2383
- openArtist(artist.uuid);
2384
- onArtistSelect?.(artist);
2385
- },
2386
- Boolean(artistSearch)
2387
- );
2388
- const songGroup = useSongSearchGroup(
2389
- songBaseUrl,
2390
- searchText,
2391
- (song) => {
2392
- openSong(song.uuid);
2393
- onSongSelect?.(song);
2394
- },
2395
- Boolean(songSearch)
2396
- );
2397
- const extraGroups = [...extraSearchGroups, artistGroup, songGroup].filter(
2398
- (group) => group !== void 0
2399
- );
2596
+ const reopenPanel = () => {
2597
+ setUncontrolledCollapsed(false);
2598
+ onCollapsedChange?.(false);
2599
+ };
2400
2600
  const handleSelectedNodeChange = (node) => {
2601
+ reopenPanel();
2401
2602
  if (node !== null && songUuid !== null) changeSong(null);
2402
2603
  selectNode(node);
2403
2604
  };
2404
2605
  const handleGenreSearchClear = clearSelection;
2405
- const handleGenreSearchSelect = (genrePlaylist) => {
2606
+ const handleGenreSearchSelect = (genrePlaylist2) => {
2607
+ reopenPanel();
2406
2608
  if (songUuid !== null) changeSong(null);
2407
- selectGenrePlaylist(genrePlaylist);
2609
+ selectGenrePlaylist(genrePlaylist2);
2408
2610
  };
2409
2611
  const selectedUuid = selectedGenrePlaylist?.uuid;
2410
- const selectedChildren = selectedUuid ? visibleGenrePlaylists.filter((genrePlaylist) => genrePlaylist.parent?.uuid === selectedUuid) : [];
2612
+ const selectedChildren = selectedUuid ? visibleGenrePlaylists.filter((genrePlaylist2) => genrePlaylist2.parent?.uuid === selectedUuid) : [];
2411
2613
  const canZoomIn = viewMode === "pop-core" && focus.onFocusGenreChange !== void 0 && selectedUuid !== void 0 && focus.focusGenreUuid !== selectedUuid && selectedChildren.length > 0 && selectedChildren.filter((child) => child.criteria?.side === "pop").length <= 1;
2412
2614
  const handleGenreSearchZoomIn = () => {
2413
2615
  if (selectedUuid) focus.onFocusGenreChange?.(selectedUuid, "zoom-in");
2414
2616
  };
2415
2617
  const zoomInRef = useRef3(void 0);
2416
2618
  zoomInRef.current = canZoomIn ? handleGenreSearchZoomIn : void 0;
2417
- const selectGenreFromPanel = (nodeId) => {
2418
- const genrePlaylist = genrePlaylistByNodeId.get(nodeId);
2419
- if (genrePlaylist) handleGenreSearchSelect(genrePlaylist);
2420
- };
2421
- const renderGenreBody = (relation) => {
2422
- const { node } = relation;
2423
- const family = /* @__PURE__ */ jsx32(
2424
- GenreNodeFamily,
2425
- {
2426
- node,
2427
- fill: relation.fill,
2428
- textColor: relation.textColor,
2429
- parentNode: relation.parentNode,
2430
- childNodes: relation.childNodes,
2431
- onSelectNode: selectGenreFromPanel
2432
- }
2433
- );
2434
- const ancestors = /* @__PURE__ */ jsx32(GenreNodeAncestors, { ancestorNodes: relation.ancestorNodes, onSelectNode: selectGenreFromPanel });
2435
- const genrePlaylist = genrePlaylistByNodeId.get(node.id);
2436
- const criteria = genrePlaylist?.criteria;
2437
- if (!genrePlaylist || !criteria) return { primary: /* @__PURE__ */ jsxs25(Fragment13, { children: [
2438
- family,
2439
- ancestors
2440
- ] }) };
2441
- const genreUuid = criteria.uuid;
2442
- const parentPlaylist = visibleGenrePlaylists.find(({ uuid }) => uuid === genrePlaylist.parent?.uuid);
2443
- const canToggleSide = !readOnly && parentPlaylist !== void 0 && parentPlaylist.parent === null;
2444
- const side = criteria.side ?? "core";
2445
- const shareTarget = {
2446
- kind: "genre",
2447
- genrePlaylistUuid: genrePlaylist.uuid,
2448
- name: criteria.name,
2449
- scope,
2450
- artistUuid
2451
- };
2452
- const shareUrl = getShareUrlRef.current?.(shareTarget) ?? void 0;
2453
- const popSibling = canToggleSide && side === "core" ? visibleGenrePlaylists.find(
2454
- (sibling) => sibling.parent?.uuid === parentPlaylist.uuid && sibling.uuid !== genrePlaylist.uuid && sibling.criteria?.side === "pop"
2455
- ) : void 0;
2456
- return {
2457
- primary: /* @__PURE__ */ jsxs25(Fragment13, { children: [
2458
- /* @__PURE__ */ jsx32(
2459
- GenreInfoPanelBody,
2619
+ const genrePlaylist = selectedGenrePlaylist;
2620
+ const criteria = genrePlaylist?.criteria;
2621
+ const parentPlaylist = visibleGenrePlaylists.find(({ uuid }) => uuid === genrePlaylist?.parent?.uuid);
2622
+ const canToggleSide = !readOnly && criteria !== void 0 && criteria !== null && parentPlaylist !== void 0 && parentPlaylist.parent === null;
2623
+ const side = criteria?.side ?? "core";
2624
+ const popSibling = canToggleSide && side === "core" ? visibleGenrePlaylists.find((sibling) => sibling.parent?.uuid === parentPlaylist.uuid && sibling.uuid !== genrePlaylist?.uuid && sibling.criteria?.side === "pop") : void 0;
2625
+ const shareTarget = selectedGenreUuid ? { kind: "genre", genreUuid: selectedGenreUuid, name: criteria?.name ?? "Genre", scope, artistUuid } : void 0;
2626
+ const shareUrl = shareTarget ? getShareUrlRef.current?.(shareTarget) ?? void 0 : void 0;
2627
+ const genreContent = useGenreDetailContent({
2628
+ genreUuid: selectedGenreUuid,
2629
+ scope,
2630
+ getBackendBaseUrl,
2631
+ onSelectGenre: (uuid) => {
2632
+ reopenPanel();
2633
+ selectGenre(uuid);
2634
+ },
2635
+ expanded: isExpanded,
2636
+ playback,
2637
+ genrePlaylist: selectedGenrePlaylist,
2638
+ band: {
2639
+ ...relations ? { node: relations.node } : {},
2640
+ shareTarget: shareUrl ? shareTarget : void 0,
2641
+ shareUrl,
2642
+ onZoomIn: zoomInRef.current,
2643
+ onAddChild: readOnly || !criteria ? void 0 : () => handleGenreCreationAction(criteria),
2644
+ onRename: readOnly || !criteria ? void 0 : () => handleGenreRenameAction(criteria),
2645
+ onChangeParent: readOnly || !genrePlaylist || !criteria ? void 0 : () => showPopup(
2646
+ /* @__PURE__ */ jsx34(
2647
+ ChangeParentPopup,
2460
2648
  {
2461
- family: /* @__PURE__ */ jsxs25(Fragment13, { children: [
2462
- family,
2463
- ancestors
2464
- ] }),
2465
- genreUuid,
2466
- genrePlaylist,
2467
- catalogue: genrePlaylist.catalogue,
2468
- wikidataId: criteria.wikidataId,
2649
+ genrePlaylist: { ...genrePlaylist, criteria },
2650
+ genrePlaylists: visibleGenrePlaylists,
2469
2651
  scope,
2470
2652
  getBackendBaseUrl,
2471
- playback,
2472
- band: {
2473
- node,
2474
- shareTarget: shareUrl ? shareTarget : void 0,
2475
- shareUrl,
2476
- onZoomIn: zoomInRef.current,
2477
- onAddChild: readOnly ? void 0 : () => handleGenreCreationAction(criteria),
2478
- onRename: readOnly ? void 0 : () => handleGenreRenameAction(criteria),
2479
- onChangeParent: readOnly ? void 0 : () => showPopup(
2480
- /* @__PURE__ */ jsx32(
2481
- ChangeParentPopup,
2482
- {
2483
- genrePlaylist: { ...genrePlaylist, criteria },
2484
- genrePlaylists: visibleGenrePlaylists,
2485
- scope,
2486
- getBackendBaseUrl,
2487
- onClose: hidePopup,
2488
- onParentChanged
2489
- }
2490
- )
2491
- ),
2492
- side,
2493
- onToggleSide: canToggleSide ? () => updateGenre({ uuid: genreUuid, data: { side: side === "pop" ? "core" : "pop" } }) : void 0,
2494
- toggleSideDisabledReason: popSibling ? `${popSibling.name} is already the pop side of ${parentPlaylist?.name}` : void 0,
2495
- additionalActions
2496
- },
2497
- extras: {
2498
- criteriaOverviewSchema,
2499
- renderGenreDetailExtras,
2500
- renderArtistLink,
2501
- renderSongLink: resolvedRenderSongLink,
2502
- artist: filterArtist,
2503
- renderSummary,
2504
- renderEssentialTracks,
2505
- renderSongLeading,
2506
- renderSongActions,
2507
- renderTracksToolbar
2508
- }
2653
+ onClose: hidePopup,
2654
+ onParentChanged
2509
2655
  }
2510
- ),
2511
- renderDangerZone?.(node)
2512
- ] })
2513
- };
2514
- };
2656
+ )
2657
+ ),
2658
+ side,
2659
+ onToggleSide: canToggleSide && criteria ? () => updateGenre({ uuid: criteria.uuid, data: { side: side === "pop" ? "core" : "pop" } }) : void 0,
2660
+ toggleSideDisabledReason: popSibling ? `${popSibling.name} is already the pop side of ${parentPlaylist?.name}` : void 0,
2661
+ additionalActions
2662
+ },
2663
+ extras: {
2664
+ criteriaOverviewSchema,
2665
+ renderGenreDetailExtras,
2666
+ renderArtistLink,
2667
+ renderSongLink: resolvedRenderSongLink,
2668
+ artist: filterArtist,
2669
+ renderSummary,
2670
+ renderEssentialTracks,
2671
+ renderSongLeading,
2672
+ renderSongActions,
2673
+ renderTracksToolbar
2674
+ },
2675
+ trailing: relations ? renderDangerZone?.(relations.node) : void 0
2676
+ });
2515
2677
  const isRotationControlled = controlledAllowWheelRotation !== void 0;
2516
2678
  const allowWheelRotation = controlledAllowWheelRotation ?? internalAllowWheelRotation;
2517
2679
  let activePanel = null;
@@ -2525,16 +2687,16 @@ function GenreTreeView({
2525
2687
  onClose: overlayPanel.onClose,
2526
2688
  compact: false
2527
2689
  };
2528
- } else if (relations) {
2529
- const { primary } = renderGenreBody(relations);
2690
+ } else if (selectedGenreUuid) {
2530
2691
  activePanel = {
2531
- title: relations.node.name,
2532
- content: primary,
2692
+ title: genreContent.title,
2693
+ content: genreContent.content,
2694
+ aside: genreContent.aside,
2533
2695
  expanded: isExpanded,
2534
2696
  onExpandedChange: changeExpanded,
2535
2697
  onClose: clearSelection,
2536
- compact: true,
2537
- headerAccent: { background: relations.fill, color: relations.textColor }
2698
+ compact: false,
2699
+ headerAccent: relations ? { background: relations.fill, color: relations.textColor } : genreContent.headerAccent
2538
2700
  };
2539
2701
  } else if (builtInPanel) {
2540
2702
  activePanel = {
@@ -2547,36 +2709,36 @@ function GenreTreeView({
2547
2709
  compact: true
2548
2710
  };
2549
2711
  }
2550
- const isPanelShown = isTreeReady && activePanel !== null;
2712
+ const isPanelShown = activePanel !== null && (selectedGenreUuid !== null || isTreeReady);
2551
2713
  const detailTab = placement === "regions" && activePanel?.onExpandedChange ? {
2552
2714
  edge: activePanel.expanded ? { label: "Shrink panel", onClick: () => activePanel.onExpandedChange?.(false) } : isCollapsed ? null : { label: "Hide panel", onClick: () => changeCollapsed(true) },
2553
2715
  center: activePanel.expanded ? null : isCollapsed ? { label: "Show panel", onClick: () => changeCollapsed(false) } : activePanel.onExpandedChange ? { label: "Extend panel", onClick: () => activePanel.onExpandedChange?.(true) } : null
2554
2716
  } : void 0;
2555
- const expandButton = placement === "default" && activePanel?.onExpandedChange ? /* @__PURE__ */ jsx32(
2556
- IconButton,
2717
+ const expandButton = placement === "default" && activePanel?.onExpandedChange ? /* @__PURE__ */ jsx34(
2718
+ IconButton2,
2557
2719
  {
2558
2720
  "aria-label": activePanel.expanded ? "Shrink panel" : "Extend panel",
2559
2721
  style: activePanel.headerAccent && { color: "inherit" },
2560
2722
  onClick: () => activePanel.onExpandedChange?.(!activePanel.expanded),
2561
- children: activePanel.expanded ? /* @__PURE__ */ jsx32(Minimize2, { "aria-hidden": "true", className: "h-4 w-4" }) : /* @__PURE__ */ jsx32(Maximize2, { "aria-hidden": "true", className: "h-4 w-4" })
2723
+ children: activePanel.expanded ? /* @__PURE__ */ jsx34(Minimize22, { "aria-hidden": "true", className: "h-4 w-4" }) : /* @__PURE__ */ jsx34(Maximize22, { "aria-hidden": "true", className: "h-4 w-4" })
2562
2724
  }
2563
2725
  ) : null;
2564
2726
  const renderArtistFilter = () => {
2565
2727
  if (isLoading || !filterArtist) return null;
2566
- return /* @__PURE__ */ jsxs25("span", { className: "inline-flex items-center overflow-hidden rounded-full bg-[var(--color-neutral-100)] text-sm", children: [
2567
- /* @__PURE__ */ jsx32(
2728
+ return /* @__PURE__ */ jsxs26("span", { className: "inline-flex items-center overflow-hidden rounded-full bg-[var(--color-neutral-100)] text-sm", children: [
2729
+ /* @__PURE__ */ jsx34(
2568
2730
  "button",
2569
2731
  {
2570
2732
  type: "button",
2571
2733
  className: "py-1 pl-3 pr-2 [@media(pointer:coarse)]:min-h-[var(--layout-touch-target)] hover:underline",
2572
2734
  "aria-label": `Show artist ${filterArtist.name}`,
2573
2735
  onClick: () => {
2574
- if (selectedNodeId !== null) clearSelection();
2736
+ if (selectedGenreUuid !== null) clearSelection();
2575
2737
  },
2576
2738
  children: filterArtist.name
2577
2739
  }
2578
2740
  ),
2579
- /* @__PURE__ */ jsx32(
2741
+ /* @__PURE__ */ jsx34(
2580
2742
  "button",
2581
2743
  {
2582
2744
  type: "button",
@@ -2588,26 +2750,41 @@ function GenreTreeView({
2588
2750
  )
2589
2751
  ] });
2590
2752
  };
2591
- const actions = /* @__PURE__ */ jsxs25(Fragment13, { children: [
2592
- !isLoading && /* @__PURE__ */ jsx32(
2593
- GenreSearch,
2753
+ const actions = /* @__PURE__ */ jsxs26(Fragment14, { children: [
2754
+ !isLoading && /* @__PURE__ */ jsx34(
2755
+ GenreTreeSearch,
2594
2756
  {
2757
+ artistBaseUrl,
2758
+ songBaseUrl,
2759
+ artistSearch: Boolean(artistSearch),
2760
+ songSearch: Boolean(songSearch),
2761
+ onArtistSelect: (artist) => {
2762
+ openArtist(artist.uuid);
2763
+ onArtistSelect?.(artist);
2764
+ },
2765
+ onSongSelect: (song) => {
2766
+ openSong(song.uuid);
2767
+ onSongSelect?.(song);
2768
+ },
2769
+ SearchGroups,
2595
2770
  genrePlaylists: visibleGenrePlaylists,
2596
2771
  onSelect: handleGenreSearchSelect,
2597
- selectedName: selectedGenrePlaylist?.name ?? (typeof builtInPanel?.title === "string" ? builtInPanel.title : null),
2772
+ onSelectGenre: (uuid) => {
2773
+ reopenPanel();
2774
+ if (songUuid) changeSong(null);
2775
+ selectGenre(uuid);
2776
+ },
2777
+ selectedName: (selectedGenreUuid ? genreContent.title : null) ?? (typeof builtInPanel?.title === "string" ? builtInPanel.title : null),
2598
2778
  onClear: builtInSongPanel ? () => changeSong(null) : builtInArtistPanel ? () => changeArtist(null) : handleGenreSearchClear,
2599
2779
  onPrevious: handlePrevious,
2600
- onQueryChange: (text) => {
2601
- setSearchText(text);
2602
- onSearchQueryChange?.(text);
2603
- },
2780
+ onQueryChange: onSearchQueryChange,
2604
2781
  onFocus: onSearchFocus,
2605
- extraGroups
2782
+ extraGroups: extraSearchGroups
2606
2783
  }
2607
2784
  ),
2608
- !isLoading && filterArtist && /* @__PURE__ */ jsx32("div", { className: "hidden w-full justify-center max-md:flex", children: renderArtistFilter() }),
2609
- /* @__PURE__ */ jsxs25("div", { className: "contents max-md:flex max-md:w-full max-md:items-center max-md:gap-2 max-md:overflow-x-auto max-md:[scrollbar-width:none] max-md:[&>*]:shrink-0", children: [
2610
- !isLoading && !isControlled && /* @__PURE__ */ jsx32(
2785
+ !isLoading && filterArtist && /* @__PURE__ */ jsx34("div", { className: "hidden w-full justify-center max-md:flex", children: renderArtistFilter() }),
2786
+ /* @__PURE__ */ jsxs26("div", { className: "contents max-md:flex max-md:w-full max-md:items-center max-md:gap-2 max-md:overflow-x-auto max-md:[scrollbar-width:none] max-md:[&>*]:shrink-0", children: [
2787
+ !isLoading && !isControlled && /* @__PURE__ */ jsx34(
2611
2788
  SegmentedControl2,
2612
2789
  {
2613
2790
  "aria-label": "Tree view mode",
@@ -2621,13 +2798,13 @@ function GenreTreeView({
2621
2798
  ]
2622
2799
  }
2623
2800
  ),
2624
- !isLoading && showDisplayOptions && /* @__PURE__ */ jsx32(
2801
+ !isLoading && showDisplayOptions && /* @__PURE__ */ jsx34(
2625
2802
  "div",
2626
2803
  {
2627
2804
  className: "flex items-center gap-1",
2628
2805
  role: "group",
2629
2806
  "aria-label": "Tree display options",
2630
- children: !isRotationControlled && viewMode !== "stacked" && viewMode !== "outline" && /* @__PURE__ */ jsx32(
2807
+ children: !isRotationControlled && viewMode !== "stacked" && viewMode !== "outline" && /* @__PURE__ */ jsx34(
2631
2808
  Button,
2632
2809
  {
2633
2810
  variant: allowWheelRotation ? "default" : "outline",
@@ -2638,7 +2815,7 @@ function GenreTreeView({
2638
2815
  )
2639
2816
  }
2640
2817
  ),
2641
- !isLoading && !readOnly && showAddRoot && /* @__PURE__ */ jsx32(
2818
+ !isLoading && !readOnly && showAddRoot && /* @__PURE__ */ jsx34(
2642
2819
  IconTextButton,
2643
2820
  {
2644
2821
  icon: Plus2,
@@ -2648,34 +2825,34 @@ function GenreTreeView({
2648
2825
  )
2649
2826
  ] })
2650
2827
  ] });
2651
- return /* @__PURE__ */ jsxs25(
2828
+ return /* @__PURE__ */ jsxs26(
2652
2829
  "div",
2653
2830
  {
2654
2831
  "data-left-panel": isPanelShown && !isCollapsed ? void 0 : "hidden",
2655
2832
  className: ["genre-tree-view relative flex flex-col h-full", placement === "regions" && REGIONS_CLASS, className2].filter(Boolean).join(" "),
2656
2833
  children: [
2657
- /* @__PURE__ */ jsx32(
2834
+ /* @__PURE__ */ jsx34(
2658
2835
  "div",
2659
2836
  {
2660
2837
  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"}`,
2661
2838
  children: actions
2662
2839
  }
2663
2840
  ),
2664
- !isLoading && filterArtist && /* @__PURE__ */ jsx32(
2841
+ !isLoading && filterArtist && /* @__PURE__ */ jsx34(
2665
2842
  "div",
2666
2843
  {
2667
2844
  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"}`,
2668
2845
  children: renderArtistFilter()
2669
2846
  }
2670
2847
  ),
2671
- /* @__PURE__ */ jsx32("div", { className: "content-container flex-1 min-h-0 flex flex-row gap-4", children: /* @__PURE__ */ jsxs25("div", { className: "tree-view-container relative flex-1 min-w-0 flex flex-col h-full", children: [
2672
- !isLoading && viewMode === "outline" && floatingControlsLeading && /* @__PURE__ */ jsx32("div", { className: "absolute bottom-3 right-3 z-10", children: floatingControlsLeading }),
2673
- isLoading ? /* @__PURE__ */ jsx32(GenreTreeViewSkeleton, { viewMode }) : viewMode === "wheel" ? (
2848
+ /* @__PURE__ */ jsx34("div", { className: "content-container flex-1 min-h-0 flex flex-row gap-4", children: /* @__PURE__ */ jsxs26("div", { className: "tree-view-container relative flex-1 min-w-0 flex flex-col h-full", children: [
2849
+ !isLoading && viewMode === "outline" && floatingControlsLeading && /* @__PURE__ */ jsx34("div", { className: "absolute bottom-3 right-3 z-10", children: floatingControlsLeading }),
2850
+ isLoading ? /* @__PURE__ */ jsx34(GenreTreeViewSkeleton, { viewMode }) : viewMode === "wheel" ? (
2674
2851
  // The info panel's top edge aligns with the tree's; the search bar floats `--layout-panel-inset` below it,
2675
2852
  // and the header inset (the bar's height plus that inset) extends the header's fill behind the bar,
2676
2853
  // Google Maps style; the 8px bottom padding mirrors the inset above the bar, and the hidden title's height is zeroed.
2677
2854
  // The panel's own Zoom in button is hidden: the search bar carries it.
2678
- /* @__PURE__ */ jsx32("div", { className: "tree-container search-overlay relative flex-1 w-full min-h-0 ", children: /* @__PURE__ */ jsx32(GenreTreeWheelHandoff, { skeleton: /* @__PURE__ */ jsx32(GenreTreeWheelSkeleton, {}), onRevealedChange: setIsWheelRevealed, children: /* @__PURE__ */ jsx32(
2855
+ /* @__PURE__ */ jsx34("div", { className: "tree-container search-overlay relative flex-1 w-full min-h-0 ", children: /* @__PURE__ */ jsx34(GenreTreeWheelHandoff, { skeleton: /* @__PURE__ */ jsx34(GenreTreeWheelSkeleton, {}), onRevealedChange: setIsWheelRevealed, children: /* @__PURE__ */ jsx34(
2679
2856
  GenrePlaylistTreeWheel,
2680
2857
  {
2681
2858
  scope,
@@ -2692,9 +2869,9 @@ function GenreTreeView({
2692
2869
  allowWheelRotation,
2693
2870
  sizeMetric: isArtistFiltered || !canSizeByViews ? "songs" : deferredSizeMetric,
2694
2871
  busy,
2695
- floatingControlsLeading: floatingControlsLeading || canSizeByViews ? /* @__PURE__ */ jsxs25("div", { className: "flex items-center gap-2", children: [
2872
+ floatingControlsLeading: floatingControlsLeading || canSizeByViews ? /* @__PURE__ */ jsxs26("div", { className: "flex items-center gap-2", children: [
2696
2873
  floatingControlsLeading,
2697
- canSizeByViews && /* @__PURE__ */ jsx32(SizeMetricToggle, { value: sizeMetric, onChange: changeSizeMetric })
2874
+ canSizeByViews && /* @__PURE__ */ jsx34(SizeMetricToggle, { value: sizeMetric, onChange: changeSizeMetric })
2698
2875
  ] }) : void 0,
2699
2876
  defaultViewport,
2700
2877
  viewportRestore,
@@ -2703,7 +2880,7 @@ function GenreTreeView({
2703
2880
  ) }) })
2704
2881
  ) : viewMode === "pop-core" ? (
2705
2882
  // Same as the wheel: the header inset extends its fill behind the floating search bar, Google Maps style.
2706
- /* @__PURE__ */ jsx32("div", { className: "tree-container search-overlay relative flex-1 w-full min-h-0 ", children: /* @__PURE__ */ jsx32(GenreTreeWheelHandoff, { skeleton: /* @__PURE__ */ jsx32(GenreTreeWheelSkeleton, {}), onRevealedChange: setIsWheelRevealed, children: /* @__PURE__ */ jsx32(
2883
+ /* @__PURE__ */ jsx34("div", { className: "tree-container search-overlay relative flex-1 w-full min-h-0 ", children: /* @__PURE__ */ jsx34(GenreTreeWheelHandoff, { skeleton: /* @__PURE__ */ jsx34(GenreTreeWheelSkeleton, {}), onRevealedChange: setIsWheelRevealed, children: /* @__PURE__ */ jsx34(
2707
2884
  GenrePlaylistTreeWheelRadialPopCore,
2708
2885
  {
2709
2886
  scope,
@@ -2720,9 +2897,9 @@ function GenreTreeView({
2720
2897
  allowWheelRotation,
2721
2898
  sizeMetric: isArtistFiltered || !canSizeByViews ? "songs" : deferredSizeMetric,
2722
2899
  busy,
2723
- floatingControlsLeading: floatingControlsLeading || canSizeByViews ? /* @__PURE__ */ jsxs25("div", { className: "flex items-center gap-2", children: [
2900
+ floatingControlsLeading: floatingControlsLeading || canSizeByViews ? /* @__PURE__ */ jsxs26("div", { className: "flex items-center gap-2", children: [
2724
2901
  floatingControlsLeading,
2725
- canSizeByViews && /* @__PURE__ */ jsx32(SizeMetricToggle, { value: sizeMetric, onChange: changeSizeMetric })
2902
+ canSizeByViews && /* @__PURE__ */ jsx34(SizeMetricToggle, { value: sizeMetric, onChange: changeSizeMetric })
2726
2903
  ] }) : void 0,
2727
2904
  defaultViewport,
2728
2905
  viewportRestore,
@@ -2730,7 +2907,7 @@ function GenreTreeView({
2730
2907
  ...focus
2731
2908
  }
2732
2909
  ) }) })
2733
- ) : viewMode === "outline" ? /* @__PURE__ */ jsx32("div", { className: "tree-container flex-1 min-h-0 w-full overflow-y-auto relative", children: /* @__PURE__ */ jsx32(
2910
+ ) : viewMode === "outline" ? /* @__PURE__ */ jsx34("div", { className: "tree-container flex-1 min-h-0 w-full overflow-y-auto relative", children: /* @__PURE__ */ jsx34(
2734
2911
  GenrePlaylistTreeWheelRadialPopCore,
2735
2912
  {
2736
2913
  outline: true,
@@ -2746,13 +2923,13 @@ function GenreTreeView({
2746
2923
  selectedNodeId,
2747
2924
  readOnly
2748
2925
  }
2749
- ) }) : /* @__PURE__ */ jsx32("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(
2926
+ ) }) : /* @__PURE__ */ jsx34("div", { className: "tree-container relative flex flex-col gap-4 w-full pt-14 overflow-x-auto overflow-y-auto text-[var(--color-text)]", children: Object.entries(groupedGenrePlaylistsByRoot).map(
2750
2927
  ([uuid, genrePlaylistTreePerRoot]) => {
2751
- return /* @__PURE__ */ jsx32(
2928
+ return /* @__PURE__ */ jsx34(
2752
2929
  "div",
2753
2930
  {
2754
2931
  className: "tree-per-root-container relative shrink-0 h-[28rem] mt-2 mr-16 p-2 bg-[var(--color-neutral-100)] rounded-lg",
2755
- children: /* @__PURE__ */ jsx32("div", { className: "graph-container relative z-10 h-full", children: /* @__PURE__ */ jsx32(
2932
+ children: /* @__PURE__ */ jsx34("div", { className: "graph-container relative z-10 h-full", children: /* @__PURE__ */ jsx34(
2756
2933
  GenrePlaylistTreePerRoot,
2757
2934
  {
2758
2935
  scope,
@@ -2778,8 +2955,8 @@ function GenreTreeView({
2778
2955
  );
2779
2956
  }
2780
2957
  ) }),
2781
- /* @__PURE__ */ jsx32(
2782
- DetailPanel,
2958
+ /* @__PURE__ */ jsx34(
2959
+ DetailPanel2,
2783
2960
  {
2784
2961
  ref: panelRef,
2785
2962
  side: "left",
@@ -2789,13 +2966,13 @@ function GenreTreeView({
2789
2966
  headerAccent: activePanel?.headerAccent,
2790
2967
  headerActions: expandButton,
2791
2968
  onClose: activePanel?.onClose ?? noop,
2792
- collapsed: isCollapsed || !isTreeReady || !activePanel,
2969
+ collapsed: isCollapsed || !isPanelShown,
2793
2970
  expanded: activePanel?.expanded,
2794
2971
  onExpandedChange: activePanel?.onExpandedChange,
2795
2972
  aside: activePanel?.aside,
2796
2973
  edgeTab: detailTab,
2797
- headerClassName: activePanel?.compact ? "pt-[calc(var(--layout-search-h)+var(--layout-panel-inset)+0.5rem)]" : void 0,
2798
- children: activePanel?.aside && !activePanel.expanded ? /* @__PURE__ */ jsxs25(Fragment13, { children: [
2974
+ headerClassName: "pt-[calc(var(--layout-search-h)+var(--layout-panel-inset)+0.5rem)]",
2975
+ children: activePanel?.aside && !activePanel.expanded ? /* @__PURE__ */ jsxs26(Fragment14, { children: [
2799
2976
  activePanel.content,
2800
2977
  activePanel.aside
2801
2978
  ] }) : activePanel?.content
@@ -2807,145 +2984,6 @@ function GenreTreeView({
2807
2984
  );
2808
2985
  }
2809
2986
 
2810
- // src/genre-tree/GenreDetailPanel.tsx
2811
- import { useMemo as useMemo8 } from "react";
2812
- import {
2813
- ACCENT_TEXT_COLOR,
2814
- GenreNodeAncestors as GenreNodeAncestors2,
2815
- GenreNodeFamily as GenreNodeFamily2,
2816
- getGenreTreeColor as getGenreTreeColor2,
2817
- TEXT_COLOR,
2818
- tintSurface
2819
- } from "@behindthemusictree/genre-tree-view";
2820
- import { DetailPanel as DetailPanel2, IconButton as IconButton2, Skeleton as Skeleton7 } from "@behindthemusictree/ui";
2821
- import { Maximize2 as Maximize22, Minimize2 as Minimize22 } from "lucide-react";
2822
- import {
2823
- useFetchGenreDetail,
2824
- genreMusicCount as genreMusicCount2,
2825
- GenrePlaylistSummarySchema
2826
- } from "@behindthemusictree/app-kit/genre-tree";
2827
- import { Fragment as Fragment14, jsx as jsx33, jsxs as jsxs26 } from "react/jsx-runtime";
2828
- var RELATIVE_ITEM_COUNT = 1;
2829
- function toNode(genre, parentId) {
2830
- return {
2831
- id: genre.uuid,
2832
- parentId,
2833
- name: genre.name,
2834
- itemCount: RELATIVE_ITEM_COUNT,
2835
- side: genre.side ?? void 0,
2836
- wikidataId: genre.wikidataId
2837
- };
2838
- }
2839
- function GenreDetailPanel({
2840
- genreUuid,
2841
- scope,
2842
- getBackendBaseUrl,
2843
- onClose,
2844
- onSelectGenre,
2845
- onZoomIn,
2846
- shareUrl,
2847
- shareTarget,
2848
- collapsed,
2849
- expanded,
2850
- onExpandedChange,
2851
- edgeTab,
2852
- extras
2853
- }) {
2854
- const { data: detail, isPending, isError } = useFetchGenreDetail(genreUuid, scope, getBackendBaseUrl);
2855
- const playback = useGenrePlaylistPlayback({ scope, genrePlaylists: [] });
2856
- const view = useMemo8(() => {
2857
- if (!detail) return null;
2858
- const rootColor = getGenreTreeColor2(detail.root.wikidataId);
2859
- const relative = (genre, parentId) => ({
2860
- node: toNode(genre, parentId),
2861
- fill: tintSurface(rootColor),
2862
- textColor: TEXT_COLOR
2863
- });
2864
- const ancestors2 = [...detail.ascendants].sort((a, b) => b.degree - a.degree);
2865
- return {
2866
- node: {
2867
- id: detail.uuid,
2868
- parentId: detail.parent?.uuid ?? null,
2869
- name: detail.name,
2870
- itemCount: scope === "reference" ? genreMusicCount2(GenrePlaylistSummarySchema.parse(detail.criteriaPlaylist)) : detail.tracksCount,
2871
- wikidataId: detail.wikidataId
2872
- },
2873
- fill: rootColor,
2874
- parentNode: detail.parent ? relative(detail.parent, null) : null,
2875
- childNodes: detail.children.map((child) => relative(child, detail.uuid)),
2876
- ancestorNodes: ancestors2.map(
2877
- ({ ascendant }, index) => relative(ascendant, index === 0 ? null : ancestors2[index - 1].ascendant.uuid)
2878
- )
2879
- };
2880
- }, [detail, scope]);
2881
- if (isError) {
2882
- return /* @__PURE__ */ jsx33("div", { role: "alert", className: "p-4 text-sm text-[var(--color-text)]", children: "This genre could not be loaded." });
2883
- }
2884
- if (isPending || !detail || !view) {
2885
- return /* @__PURE__ */ jsxs26("div", { className: "flex flex-col gap-3 p-4", "aria-busy": "true", "data-testid": "genre-detail-panel-skeleton", children: [
2886
- /* @__PURE__ */ jsx33(Skeleton7, { className: "h-6 w-2/3" }),
2887
- /* @__PURE__ */ jsx33(Skeleton7, { className: "h-4 w-full" }),
2888
- /* @__PURE__ */ jsx33(Skeleton7, { className: "h-4 w-3/4" })
2889
- ] });
2890
- }
2891
- const selectGenre = (nodeId) => onSelectGenre(nodeId);
2892
- const family = /* @__PURE__ */ jsx33(
2893
- GenreNodeFamily2,
2894
- {
2895
- node: view.node,
2896
- fill: view.fill,
2897
- textColor: ACCENT_TEXT_COLOR,
2898
- parentNode: view.parentNode,
2899
- childNodes: view.childNodes,
2900
- onSelectNode: selectGenre
2901
- }
2902
- );
2903
- const ancestors = /* @__PURE__ */ jsx33(GenreNodeAncestors2, { ancestorNodes: view.ancestorNodes, onSelectNode: selectGenre });
2904
- return /* @__PURE__ */ jsxs26(
2905
- DetailPanel2,
2906
- {
2907
- side: "left",
2908
- title: view.node.name,
2909
- headerAccent: { background: view.fill, color: ACCENT_TEXT_COLOR },
2910
- headerActions: onExpandedChange ? /* @__PURE__ */ jsx33(
2911
- IconButton2,
2912
- {
2913
- "aria-label": expanded ? "Shrink panel" : "Extend panel",
2914
- style: { color: "inherit" },
2915
- onClick: () => onExpandedChange(!expanded),
2916
- children: expanded ? /* @__PURE__ */ jsx33(Minimize22, { "aria-hidden": "true", className: "h-4 w-4" }) : /* @__PURE__ */ jsx33(Maximize22, { "aria-hidden": "true", className: "h-4 w-4" })
2917
- }
2918
- ) : void 0,
2919
- onClose,
2920
- collapsed,
2921
- expanded,
2922
- onExpandedChange,
2923
- edgeTab,
2924
- children: [
2925
- /* @__PURE__ */ jsx33(
2926
- GenreInfoPanelBody,
2927
- {
2928
- family: /* @__PURE__ */ jsxs26(Fragment14, { children: [
2929
- family,
2930
- ancestors
2931
- ] }),
2932
- genreUuid: detail.uuid,
2933
- genrePlaylist: scope === "reference" ? GenrePlaylistSummarySchema.parse(detail.criteriaPlaylist) : { ...detail.criteriaPlaylist, tracksCount: detail.tracksCount },
2934
- catalogue: detail.catalogue,
2935
- wikidataId: detail.wikidataId,
2936
- scope,
2937
- getBackendBaseUrl,
2938
- playback,
2939
- band: { node: view.node, onZoomIn, shareUrl, shareTarget: shareUrl ? shareTarget : void 0 },
2940
- extras
2941
- }
2942
- ),
2943
- !expanded && ancestors
2944
- ]
2945
- }
2946
- );
2947
- }
2948
-
2949
2987
  // src/genre-tree/index.ts
2950
2988
  import {
2951
2989
  GenreTreeSkeleton,
@@ -2960,7 +2998,7 @@ import { ListMusic } from "lucide-react";
2960
2998
  import { useTrackList as useTrackList6 } from "@behindthemusictree/app-kit/genre-tree";
2961
2999
  import { TrackListOriginType as TrackListOriginType2 } from "@behindthemusictree/app-kit/genre-tree";
2962
3000
  import { useTrackNoun as useTrackNoun4 } from "@behindthemusictree/app-kit/track-noun";
2963
- import { jsx as jsx34, jsxs as jsxs27 } from "react/jsx-runtime";
3001
+ import { jsx as jsx35, jsxs as jsxs27 } from "react/jsx-runtime";
2964
3002
  var useBrowserLayoutEffect = typeof window === "undefined" ? useEffect6 : useLayoutEffect2;
2965
3003
  function TrackListSidebar({
2966
3004
  className: className2 = "",
@@ -3072,15 +3110,15 @@ function TrackListSidebar({
3072
3110
  /* @__PURE__ */ jsxs27("div", { className: "header flex h-16 items-center px-4 py-2 text-[var(--color-text)]", children: [
3073
3111
  /* @__PURE__ */ jsxs27("div", { className: "min-w-0 flex flex-1 flex-col justify-center", children: [
3074
3112
  /* @__PURE__ */ jsxs27("div", { className: "origin flex text-xl", children: [
3075
- /* @__PURE__ */ jsx34("div", { className: "from h-auto flex flex-col justify-center items-center mr-2", children: "From" }),
3076
- /* @__PURE__ */ jsx34("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__ */ jsx34("div", { className: "name max-w-full truncate", children: trackList.origin.label }) })
3113
+ /* @__PURE__ */ jsx35("div", { className: "from h-auto flex flex-col justify-center items-center mr-2", children: "From" }),
3114
+ /* @__PURE__ */ jsx35("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__ */ jsx35("div", { className: "name max-w-full truncate", children: trackList.origin.label }) })
3077
3115
  ] }),
3078
3116
  /* @__PURE__ */ jsxs27("div", { className: "info flex items-center text-sm text-[var(--color-text-muted)]", children: [
3079
3117
  trackList.origin.type === TrackListOriginType2.GENRE_PLAYLIST ? "Genre \u2022 " : trackList.origin.type === TrackListOriginType2.ARTIST ? "Artist \u2022 " : `${trackNoun.singular} playlist \u2022 `,
3080
3118
  `${trackList.total} ${trackList.total > 1 ? trackNoun.plural : trackNoun.singular}`
3081
3119
  ] })
3082
3120
  ] }),
3083
- (isCollapsed && onExpand || !isCollapsed && onMinimize) && /* @__PURE__ */ jsx34(
3121
+ (isCollapsed && onExpand || !isCollapsed && onMinimize) && /* @__PURE__ */ jsx35(
3084
3122
  "button",
3085
3123
  {
3086
3124
  type: "button",
@@ -3089,7 +3127,7 @@ function TrackListSidebar({
3089
3127
  "aria-expanded": !isCollapsed,
3090
3128
  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"}`,
3091
3129
  onClick: isCollapsed ? onExpand : onMinimize,
3092
- children: /* @__PURE__ */ jsx34(ListMusic, { "aria-hidden": "true", className: "h-5 w-5" })
3130
+ children: /* @__PURE__ */ jsx35(ListMusic, { "aria-hidden": "true", className: "h-5 w-5" })
3093
3131
  }
3094
3132
  )
3095
3133
  ] }),
@@ -3103,7 +3141,7 @@ function TrackListSidebar({
3103
3141
  onPointerDown: markUserScrollIntent,
3104
3142
  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",
3105
3143
  children: [
3106
- trackList.tracks.map((track, index) => /* @__PURE__ */ jsx34("li", { "data-track-uuid": track.uuid, children: /* @__PURE__ */ jsx34(
3144
+ trackList.tracks.map((track, index) => /* @__PURE__ */ jsx35("li", { "data-track-uuid": track.uuid, children: /* @__PURE__ */ jsx35(
3107
3145
  TrackItem,
3108
3146
  {
3109
3147
  track,
@@ -3114,7 +3152,7 @@ function TrackListSidebar({
3114
3152
  renderSongLink
3115
3153
  }
3116
3154
  ) }, track.uuid)),
3117
- hasMore && /* @__PURE__ */ jsx34("li", { ref: loadMoreSentinelRef, "aria-hidden": "true", className: "h-px" })
3155
+ hasMore && /* @__PURE__ */ jsx35("li", { ref: loadMoreSentinelRef, "aria-hidden": "true", className: "h-px" })
3118
3156
  ]
3119
3157
  }
3120
3158
  )
@@ -3124,7 +3162,7 @@ function TrackListSidebar({
3124
3162
  }
3125
3163
 
3126
3164
  // src/genre-tree/PlayerPanel.tsx
3127
- import { useEffect as useEffect7, useState as useState9 } from "react";
3165
+ import { useEffect as useEffect7, useState as useState10 } from "react";
3128
3166
  import { twMerge } from "tailwind-merge";
3129
3167
  import {
3130
3168
  usePlayer as usePlayer11,
@@ -3139,7 +3177,7 @@ import { PanelEdgeTab } from "@behindthemusictree/ui";
3139
3177
  import { Pause as Pause3, Play as Play4, SkipBack, SkipForward, Volume2, VolumeX } from "lucide-react";
3140
3178
  import { formatTime, useTrackList as useTrackList7 } from "@behindthemusictree/app-kit/genre-tree";
3141
3179
  import { PlayStates as PlayStates4, useCurrentTime, usePlayer as usePlayer10 } from "@behindthemusictree/app-kit/player";
3142
- import { jsx as jsx35, jsxs as jsxs28 } from "react/jsx-runtime";
3180
+ import { jsx as jsx36, jsxs as jsxs28 } from "react/jsx-runtime";
3143
3181
  function PlayerControls() {
3144
3182
  const {
3145
3183
  duration,
@@ -3165,8 +3203,8 @@ function PlayerControls() {
3165
3203
  className: "player-controls flex shrink-0 flex-col gap-2 border-b border-zinc-800 px-3 py-2 text-zinc-200",
3166
3204
  children: [
3167
3205
  /* @__PURE__ */ jsxs28("div", { className: "flex min-w-0 items-center gap-2 text-xs tabular-nums text-zinc-400", children: [
3168
- /* @__PURE__ */ jsx35("span", { "aria-label": "Elapsed time", children: formatTime(safeCurrentTime) }),
3169
- /* @__PURE__ */ jsx35(
3206
+ /* @__PURE__ */ jsx36("span", { "aria-label": "Elapsed time", children: formatTime(safeCurrentTime) }),
3207
+ /* @__PURE__ */ jsx36(
3170
3208
  "input",
3171
3209
  {
3172
3210
  "aria-label": "Seek",
@@ -3180,11 +3218,11 @@ function PlayerControls() {
3180
3218
  onChange: (event) => seekTo(Number(event.currentTarget.value))
3181
3219
  }
3182
3220
  ),
3183
- /* @__PURE__ */ jsx35("span", { "aria-label": "Duration", children: formatTime(safeDuration) })
3221
+ /* @__PURE__ */ jsx36("span", { "aria-label": "Duration", children: formatTime(safeDuration) })
3184
3222
  ] }),
3185
3223
  /* @__PURE__ */ jsxs28("div", { className: "grid min-h-9 grid-cols-[1fr_auto_1fr] items-center gap-3", children: [
3186
3224
  /* @__PURE__ */ jsxs28("div", { className: "col-start-2 flex items-center justify-center gap-2", children: [
3187
- /* @__PURE__ */ jsx35(
3225
+ /* @__PURE__ */ jsx36(
3188
3226
  "button",
3189
3227
  {
3190
3228
  type: "button",
@@ -3193,10 +3231,10 @@ function PlayerControls() {
3193
3231
  disabled: !isReady || !hasPreviousTrack,
3194
3232
  onClick: playPreviousTrack,
3195
3233
  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",
3196
- children: /* @__PURE__ */ jsx35(SkipBack, { "aria-hidden": "true", className: "h-4 w-4" })
3234
+ children: /* @__PURE__ */ jsx36(SkipBack, { "aria-hidden": "true", className: "h-4 w-4" })
3197
3235
  }
3198
3236
  ),
3199
- /* @__PURE__ */ jsx35(
3237
+ /* @__PURE__ */ jsx36(
3200
3238
  "button",
3201
3239
  {
3202
3240
  type: "button",
@@ -3205,10 +3243,10 @@ function PlayerControls() {
3205
3243
  disabled: !isReady,
3206
3244
  onClick: handlePlayPauseAction,
3207
3245
  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",
3208
- children: isPlaying ? /* @__PURE__ */ jsx35(Pause3, { "aria-hidden": "true", className: "h-4 w-4", fill: "currentColor" }) : /* @__PURE__ */ jsx35(Play4, { "aria-hidden": "true", className: "h-4 w-4", fill: "currentColor" })
3246
+ children: isPlaying ? /* @__PURE__ */ jsx36(Pause3, { "aria-hidden": "true", className: "h-4 w-4", fill: "currentColor" }) : /* @__PURE__ */ jsx36(Play4, { "aria-hidden": "true", className: "h-4 w-4", fill: "currentColor" })
3209
3247
  }
3210
3248
  ),
3211
- /* @__PURE__ */ jsx35(
3249
+ /* @__PURE__ */ jsx36(
3212
3250
  "button",
3213
3251
  {
3214
3252
  type: "button",
@@ -3217,14 +3255,14 @@ function PlayerControls() {
3217
3255
  disabled: !isReady || !hasNextTrack,
3218
3256
  onClick: playNextTrack,
3219
3257
  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",
3220
- children: /* @__PURE__ */ jsx35(SkipForward, { "aria-hidden": "true", className: "h-4 w-4" })
3258
+ children: /* @__PURE__ */ jsx36(SkipForward, { "aria-hidden": "true", className: "h-4 w-4" })
3221
3259
  }
3222
3260
  )
3223
3261
  ] }),
3224
3262
  /* @__PURE__ */ jsxs28("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: [
3225
- volume === 0 ? /* @__PURE__ */ jsx35(VolumeX, { "aria-hidden": "true", className: "h-[0.7rem] w-[0.7rem] shrink-0" }) : /* @__PURE__ */ jsx35(Volume2, { "aria-hidden": "true", className: "h-[0.7rem] w-[0.7rem] shrink-0" }),
3226
- /* @__PURE__ */ jsx35("span", { className: "sr-only", children: "Volume" }),
3227
- /* @__PURE__ */ jsx35(
3263
+ volume === 0 ? /* @__PURE__ */ jsx36(VolumeX, { "aria-hidden": "true", className: "h-[0.7rem] w-[0.7rem] shrink-0" }) : /* @__PURE__ */ jsx36(Volume2, { "aria-hidden": "true", className: "h-[0.7rem] w-[0.7rem] shrink-0" }),
3264
+ /* @__PURE__ */ jsx36("span", { className: "sr-only", children: "Volume" }),
3265
+ /* @__PURE__ */ jsx36(
3228
3266
  "input",
3229
3267
  {
3230
3268
  "aria-label": "Volume",
@@ -3246,7 +3284,7 @@ function PlayerControls() {
3246
3284
  }
3247
3285
 
3248
3286
  // src/genre-tree/PlayerPanel.tsx
3249
- import { jsx as jsx36, jsxs as jsxs29 } from "react/jsx-runtime";
3287
+ import { jsx as jsx37, jsxs as jsxs29 } from "react/jsx-runtime";
3250
3288
  var PANEL_ID = "player-panel";
3251
3289
  var DEFAULT_EXTENDED_WIDTH = "calc(100vw - var(--layout-center-left) - var(--layout-gutter))";
3252
3290
  function PlayerPanel({
@@ -3260,12 +3298,12 @@ function PlayerPanel({
3260
3298
  }) {
3261
3299
  const { playerTrackObject, retryPlayback } = usePlayer11();
3262
3300
  const { toggleTrackListSidebar, isTrackListSidebarVisible } = useTrackListSidebarVisibility();
3263
- const [lastLoadedTrackTitle, setLastLoadedTrackTitle] = useState9("");
3301
+ const [lastLoadedTrackTitle, setLastLoadedTrackTitle] = useState10("");
3264
3302
  const loadedTrackTitle = playerTrackObject?.track.title;
3265
3303
  useEffect7(() => {
3266
3304
  if (loadedTrackTitle) setLastLoadedTrackTitle(loadedTrackTitle);
3267
3305
  }, [loadedTrackTitle]);
3268
- const [uncontrolledExtended, setUncontrolledExtended] = useState9(false);
3306
+ const [uncontrolledExtended, setUncontrolledExtended] = useState10(false);
3269
3307
  const isExtendRequested = expanded ?? uncontrolledExtended;
3270
3308
  const setIsExtendRequested = (next) => {
3271
3309
  setUncontrolledExtended(next);
@@ -3299,7 +3337,7 @@ function PlayerPanel({
3299
3337
  ),
3300
3338
  style: isExtended ? { width: extendedWidth } : void 0,
3301
3339
  children: [
3302
- /* @__PURE__ */ jsx36(
3340
+ /* @__PURE__ */ jsx37(
3303
3341
  PanelEdgeTab,
3304
3342
  {
3305
3343
  side: "right",
@@ -3318,19 +3356,19 @@ function PlayerPanel({
3318
3356
  ),
3319
3357
  children: [
3320
3358
  /* @__PURE__ */ jsxs29("div", { className: "flex w-[var(--layout-right-w)] shrink-0 flex-col", children: [
3321
- /* @__PURE__ */ jsx36("div", { className: "flex shrink-0 items-center justify-between gap-2 border-b border-zinc-800 p-2", children: /* @__PURE__ */ jsx36("span", { className: "min-w-0 truncate text-sm font-semibold", children: loadedTrackTitle || lastLoadedTrackTitle }) }),
3359
+ /* @__PURE__ */ jsx37("div", { className: "flex shrink-0 items-center justify-between gap-2 border-b border-zinc-800 p-2", children: /* @__PURE__ */ jsx37("span", { className: "min-w-0 truncate text-sm font-semibold", children: loadedTrackTitle || lastLoadedTrackTitle }) }),
3322
3360
  /* @__PURE__ */ jsxs29("div", { className: "relative w-full shrink-0", children: [
3323
- /* @__PURE__ */ jsx36(PlayerVideoSurface, { className: "aspect-video w-full bg-black" }),
3361
+ /* @__PURE__ */ jsx37(PlayerVideoSurface, { className: "aspect-video w-full bg-black" }),
3324
3362
  playerTrackObject?.loadError && /* @__PURE__ */ jsxs29("span", { className: "absolute bottom-2 left-2 right-2 text-xs text-red-400", children: [
3325
3363
  playerTrackObject.loadError,
3326
- /* @__PURE__ */ jsx36("button", { type: "button", className: "ml-2 underline", onClick: () => {
3364
+ /* @__PURE__ */ jsx37("button", { type: "button", className: "ml-2 underline", onClick: () => {
3327
3365
  void retryPlayback();
3328
3366
  }, children: "Retry" })
3329
3367
  ] })
3330
3368
  ] }),
3331
- /* @__PURE__ */ jsx36(PlayerControls, {})
3369
+ /* @__PURE__ */ jsx37(PlayerControls, {})
3332
3370
  ] }),
3333
- /* @__PURE__ */ jsx36(
3371
+ /* @__PURE__ */ jsx37(
3334
3372
  TrackListSidebar,
3335
3373
  {
3336
3374
  layout: "inline",
@@ -3357,11 +3395,11 @@ function PlayerPanel({
3357
3395
  import { v4 as uuidv4 } from "uuid";
3358
3396
  import { AiFillStar, AiOutlineStar } from "react-icons/ai";
3359
3397
  import { FORM_RATING_NULL_VALUE } from "@behindthemusictree/app-kit/genre-tree";
3360
- import { jsx as jsx37, jsxs as jsxs30 } from "react/jsx-runtime";
3398
+ import { jsx as jsx38, jsxs as jsxs30 } from "react/jsx-runtime";
3361
3399
  function Rating({ rating, handleChange }) {
3362
3400
  const instanceId = uuidv4();
3363
- return /* @__PURE__ */ jsx37("div", { className: "flex items-center", children: [...Array(6)].map((_, i) => /* @__PURE__ */ jsxs30("div", { className: `relative ${i === 0 ? "mr-4" : "mr-1"}`, children: [
3364
- /* @__PURE__ */ jsx37(
3401
+ return /* @__PURE__ */ jsx38("div", { className: "flex items-center", children: [...Array(6)].map((_, i) => /* @__PURE__ */ jsxs30("div", { className: `relative ${i === 0 ? "mr-4" : "mr-1"}`, children: [
3402
+ /* @__PURE__ */ jsx38(
3365
3403
  "input",
3366
3404
  {
3367
3405
  id: `star-${instanceId}-${i}`,
@@ -3373,16 +3411,16 @@ function Rating({ rating, handleChange }) {
3373
3411
  onChange: handleChange
3374
3412
  }
3375
3413
  ),
3376
- /* @__PURE__ */ jsx37("label", { htmlFor: `star-${instanceId}-${i}`, className: "cursor-pointer", children: i === 0 ? /* @__PURE__ */ jsxs30("div", { className: "flex", children: [
3377
- /* @__PURE__ */ jsx37(
3414
+ /* @__PURE__ */ jsx38("label", { htmlFor: `star-${instanceId}-${i}`, className: "cursor-pointer", children: i === 0 ? /* @__PURE__ */ jsxs30("div", { className: "flex", children: [
3415
+ /* @__PURE__ */ jsx38(
3378
3416
  AiOutlineStar,
3379
3417
  {
3380
3418
  className: `w-7 h-7 absolute top-0 left-0 ${rating === FORM_RATING_NULL_VALUE ? "text-[var(--color-accent-rating)]" : "text-[var(--color-text-muted)]"}`
3381
3419
  }
3382
3420
  ),
3383
- /* @__PURE__ */ jsx37("span", { className: "flex-grow w-8" }),
3421
+ /* @__PURE__ */ jsx38("span", { className: "flex-grow w-8" }),
3384
3422
  " "
3385
- ] }) : /* @__PURE__ */ jsx37(
3423
+ ] }) : /* @__PURE__ */ jsx38(
3386
3424
  AiFillStar,
3387
3425
  {
3388
3426
  className: `w-7 h-7 absolute top-0 left-0 ${(rating ?? -1) >= i ? "text-[var(--color-accent-rating)]" : "text-[var(--color-text-muted)]"}`
@@ -3403,10 +3441,10 @@ export {
3403
3441
  GenreSearch,
3404
3442
  GenreNodeActionBand,
3405
3443
  GenreInfoPanelBody,
3444
+ GenreDetailPanel,
3406
3445
  TrackPositionPlayPause,
3407
3446
  TrackItem,
3408
3447
  GenreTreeView,
3409
- GenreDetailPanel,
3410
3448
  TrackListSidebar,
3411
3449
  PlayerPanel,
3412
3450
  Rating,
@@ -3415,4 +3453,4 @@ export {
3415
3453
  GenreTreeViewSkeleton2 as GenreTreeViewSkeleton,
3416
3454
  GenreTreeOutlineSkeleton
3417
3455
  };
3418
- //# sourceMappingURL=chunk-A7OWCEIK.js.map
3456
+ //# sourceMappingURL=chunk-YC3WBUVO.js.map