@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.
@@ -46,13 +46,13 @@ __export(genre_tree_exports, {
46
46
  module.exports = __toCommonJS(genre_tree_exports);
47
47
 
48
48
  // src/genre-tree/GenreTreeView.tsx
49
- var import_react16 = require("react");
49
+ var import_react18 = require("react");
50
50
  var import_react_query3 = require("@tanstack/react-query");
51
- var import_lucide_react11 = require("lucide-react");
52
51
  var import_lucide_react12 = require("lucide-react");
53
- var import_ui16 = require("@behindthemusictree/ui");
54
- var import_genre_tree_view4 = require("@behindthemusictree/genre-tree-view");
55
- var import_genre_tree27 = require("@behindthemusictree/app-kit/genre-tree");
52
+ var import_lucide_react13 = require("lucide-react");
53
+ var import_ui17 = require("@behindthemusictree/ui");
54
+ var import_genre_tree_view5 = require("@behindthemusictree/genre-tree-view");
55
+ var import_genre_tree28 = require("@behindthemusictree/app-kit/genre-tree");
56
56
  var import_popup5 = require("@behindthemusictree/app-kit/popup");
57
57
 
58
58
  // src/genre-tree/playlist-tree/size-metric.ts
@@ -702,6 +702,9 @@ function GenreTreeWheelHandoff({ skeleton, children, onRevealedChange }) {
702
702
  ] });
703
703
  }
704
704
 
705
+ // src/genre-tree/GenreTreeSearch.tsx
706
+ var import_react8 = require("react");
707
+
705
708
  // src/genre-tree/GenreSearch.tsx
706
709
  var import_lucide_react3 = require("lucide-react");
707
710
  var import_ui4 = require("@behindthemusictree/ui");
@@ -902,6 +905,52 @@ function GenreSearch({
902
905
  ) });
903
906
  }
904
907
 
908
+ // src/genre-tree/GenreTreeSearch.tsx
909
+ var import_jsx_runtime14 = require("react/jsx-runtime");
910
+ function GenreTreeSearch({
911
+ artistBaseUrl,
912
+ songBaseUrl,
913
+ onArtistSelect,
914
+ onSongSelect,
915
+ artistSearch,
916
+ songSearch,
917
+ SearchGroups,
918
+ onSelectGenre,
919
+ extraGroups = [],
920
+ onQueryChange,
921
+ onFocus,
922
+ ...props
923
+ }) {
924
+ const [query, setQuery] = (0, import_react8.useState)("");
925
+ const [focused, setFocused] = (0, import_react8.useState)(false);
926
+ const deferredQuery = (0, import_react8.useDeferredValue)(query);
927
+ const artistGroup = useArtistSearchGroup(artistBaseUrl, deferredQuery, onArtistSelect, artistSearch);
928
+ const songGroup = useSongSearchGroup(songBaseUrl, deferredQuery, onSongSelect, songSearch);
929
+ const render = (hostGroups) => /* @__PURE__ */ (0, import_jsx_runtime14.jsx)(
930
+ GenreSearch,
931
+ {
932
+ ...props,
933
+ onQueryChange: (text) => {
934
+ setQuery(text);
935
+ onQueryChange?.(text);
936
+ },
937
+ onFocus: () => {
938
+ setFocused(true);
939
+ onFocus?.();
940
+ },
941
+ extraGroups: [...extraGroups, ...hostGroups, artistGroup, songGroup].filter((group) => group !== void 0)
942
+ }
943
+ );
944
+ return SearchGroups ? /* @__PURE__ */ (0, import_jsx_runtime14.jsx)(SearchGroups, { query: deferredQuery, focused, onSelectGenre, children: render }) : render([]);
945
+ }
946
+
947
+ // src/genre-tree/GenreDetailPanel.tsx
948
+ var import_react14 = require("react");
949
+ var import_genre_tree_view4 = require("@behindthemusictree/genre-tree-view");
950
+ var import_ui7 = require("@behindthemusictree/ui");
951
+ var import_lucide_react8 = require("lucide-react");
952
+ var import_genre_tree19 = require("@behindthemusictree/app-kit/genre-tree");
953
+
905
954
  // src/genre-tree/GenreInfoPanelBody.tsx
906
955
  var import_genre_tree18 = require("@behindthemusictree/app-kit/genre-tree");
907
956
  var import_track_noun3 = require("@behindthemusictree/app-kit/track-noun");
@@ -915,25 +964,25 @@ var import_lucide_react6 = require("lucide-react");
915
964
  var import_popup3 = require("@behindthemusictree/app-kit/popup");
916
965
 
917
966
  // src/genre-tree/SharePopup.tsx
918
- var import_react9 = require("react");
967
+ var import_react10 = require("react");
919
968
  var import_lucide_react5 = require("lucide-react");
920
969
  var import_fa = require("react-icons/fa");
921
970
  var import_si = require("react-icons/si");
922
971
 
923
972
  // src/popup/Toast.tsx
924
- var import_react8 = require("react");
973
+ var import_react9 = require("react");
925
974
  var import_react_dom2 = require("react-dom");
926
- var import_jsx_runtime14 = require("react/jsx-runtime");
975
+ var import_jsx_runtime15 = require("react/jsx-runtime");
927
976
  function Toast({ message, onClose, durationMs = 2200 }) {
928
- const [mounted, setMounted] = (0, import_react8.useState)(false);
929
- (0, import_react8.useEffect)(() => {
977
+ const [mounted, setMounted] = (0, import_react9.useState)(false);
978
+ (0, import_react9.useEffect)(() => {
930
979
  setMounted(true);
931
980
  const timeout = window.setTimeout(onClose, durationMs);
932
981
  return () => window.clearTimeout(timeout);
933
982
  }, [durationMs, onClose]);
934
983
  if (!mounted) return null;
935
984
  return (0, import_react_dom2.createPortal)(
936
- /* @__PURE__ */ (0, import_jsx_runtime14.jsx)("div", { className: "pointer-events-none fixed inset-x-0 bottom-8 z-[60] flex justify-center px-4", children: /* @__PURE__ */ (0, import_jsx_runtime14.jsx)(
985
+ /* @__PURE__ */ (0, import_jsx_runtime15.jsx)("div", { className: "pointer-events-none fixed inset-x-0 bottom-8 z-[60] flex justify-center px-4", children: /* @__PURE__ */ (0, import_jsx_runtime15.jsx)(
937
986
  "div",
938
987
  {
939
988
  role: "status",
@@ -948,12 +997,12 @@ function Toast({ message, onClose, durationMs = 2200 }) {
948
997
  }
949
998
 
950
999
  // src/genre-tree/ShareDestinationButton.tsx
951
- var import_jsx_runtime15 = require("react/jsx-runtime");
1000
+ var import_jsx_runtime16 = require("react/jsx-runtime");
952
1001
  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";
953
1002
  function Contents({ icon, label }) {
954
- return /* @__PURE__ */ (0, import_jsx_runtime15.jsxs)(import_jsx_runtime15.Fragment, { children: [
955
- /* @__PURE__ */ (0, import_jsx_runtime15.jsx)("span", { className: "flex h-12 w-12 items-center justify-center rounded-full bg-[var(--color-surface-muted)] transition-colors group-hover:bg-[var(--color-border)]", children: icon }),
956
- /* @__PURE__ */ (0, import_jsx_runtime15.jsx)("span", { className: "max-w-full truncate", children: label })
1003
+ return /* @__PURE__ */ (0, import_jsx_runtime16.jsxs)(import_jsx_runtime16.Fragment, { children: [
1004
+ /* @__PURE__ */ (0, import_jsx_runtime16.jsx)("span", { className: "flex h-12 w-12 items-center justify-center rounded-full bg-[var(--color-surface-muted)] transition-colors group-hover:bg-[var(--color-border)]", children: icon }),
1005
+ /* @__PURE__ */ (0, import_jsx_runtime16.jsx)("span", { className: "max-w-full truncate", children: label })
957
1006
  ] });
958
1007
  }
959
1008
  function ShareDestinationButton({
@@ -965,29 +1014,29 @@ function ShareDestinationButton({
965
1014
  ariaLabel
966
1015
  }) {
967
1016
  if (href) {
968
- return /* @__PURE__ */ (0, import_jsx_runtime15.jsx)("a", { href, target: "_blank", rel: "noopener noreferrer", "aria-label": ariaLabel, className, children: /* @__PURE__ */ (0, import_jsx_runtime15.jsx)(Contents, { icon, label }) });
1017
+ return /* @__PURE__ */ (0, import_jsx_runtime16.jsx)("a", { href, target: "_blank", rel: "noopener noreferrer", "aria-label": ariaLabel, className, children: /* @__PURE__ */ (0, import_jsx_runtime16.jsx)(Contents, { icon, label }) });
969
1018
  }
970
- return /* @__PURE__ */ (0, import_jsx_runtime15.jsx)("button", { type: "button", onClick, disabled, "aria-label": ariaLabel, className, children: /* @__PURE__ */ (0, import_jsx_runtime15.jsx)(Contents, { icon, label }) });
1019
+ return /* @__PURE__ */ (0, import_jsx_runtime16.jsx)("button", { type: "button", onClick, disabled, "aria-label": ariaLabel, className, children: /* @__PURE__ */ (0, import_jsx_runtime16.jsx)(Contents, { icon, label }) });
971
1020
  }
972
1021
 
973
1022
  // src/genre-tree/SharePreviewCard.tsx
974
1023
  var import_lucide_react4 = require("lucide-react");
975
- var import_jsx_runtime16 = require("react/jsx-runtime");
1024
+ var import_jsx_runtime17 = require("react/jsx-runtime");
976
1025
  function SharePreviewCard({ target, url }) {
977
1026
  const Icon = { genre: import_lucide_react4.Music2, artist: import_lucide_react4.UserRound, song: import_lucide_react4.Music }[target.kind];
978
1027
  const typeLabel = { genre: "Genre", artist: "Artist", song: "Song" }[target.kind];
979
- return /* @__PURE__ */ (0, import_jsx_runtime16.jsx)("div", { className: "rounded-3xl bg-[var(--color-surface-muted)] p-4", children: /* @__PURE__ */ (0, import_jsx_runtime16.jsxs)("div", { className: "flex min-h-32 overflow-hidden rounded-2xl bg-[var(--color-surface)] shadow-lg", children: [
980
- /* @__PURE__ */ (0, import_jsx_runtime16.jsx)("div", { className: "flex w-28 shrink-0 items-center justify-center bg-[var(--color-primary)]/15 text-[var(--color-primary)]", children: /* @__PURE__ */ (0, import_jsx_runtime16.jsx)(Icon, { "aria-hidden": "true", className: "h-10 w-10" }) }),
981
- /* @__PURE__ */ (0, import_jsx_runtime16.jsxs)("div", { className: "flex min-w-0 flex-1 flex-col justify-center gap-1 p-4", children: [
982
- /* @__PURE__ */ (0, import_jsx_runtime16.jsx)("span", { className: "text-xs font-medium uppercase tracking-wide text-[var(--color-text-muted)]", children: typeLabel }),
983
- /* @__PURE__ */ (0, import_jsx_runtime16.jsx)("span", { className: "truncate text-base font-semibold text-[var(--color-text)]", title: target.name, children: target.name }),
984
- /* @__PURE__ */ (0, import_jsx_runtime16.jsx)("span", { className: "truncate text-sm text-[var(--color-primary)]", title: url, children: url })
1028
+ return /* @__PURE__ */ (0, import_jsx_runtime17.jsx)("div", { className: "rounded-3xl bg-[var(--color-surface-muted)] p-4", children: /* @__PURE__ */ (0, import_jsx_runtime17.jsxs)("div", { className: "flex min-h-32 overflow-hidden rounded-2xl bg-[var(--color-surface)] shadow-lg", children: [
1029
+ /* @__PURE__ */ (0, import_jsx_runtime17.jsx)("div", { className: "flex w-28 shrink-0 items-center justify-center bg-[var(--color-primary)]/15 text-[var(--color-primary)]", children: /* @__PURE__ */ (0, import_jsx_runtime17.jsx)(Icon, { "aria-hidden": "true", className: "h-10 w-10" }) }),
1030
+ /* @__PURE__ */ (0, import_jsx_runtime17.jsxs)("div", { className: "flex min-w-0 flex-1 flex-col justify-center gap-1 p-4", children: [
1031
+ /* @__PURE__ */ (0, import_jsx_runtime17.jsx)("span", { className: "text-xs font-medium uppercase tracking-wide text-[var(--color-text-muted)]", children: typeLabel }),
1032
+ /* @__PURE__ */ (0, import_jsx_runtime17.jsx)("span", { className: "truncate text-base font-semibold text-[var(--color-text)]", title: target.name, children: target.name }),
1033
+ /* @__PURE__ */ (0, import_jsx_runtime17.jsx)("span", { className: "truncate text-sm text-[var(--color-primary)]", title: url, children: url })
985
1034
  ] })
986
1035
  ] }) });
987
1036
  }
988
1037
 
989
1038
  // src/genre-tree/SharePopup.tsx
990
- var import_jsx_runtime17 = require("react/jsx-runtime");
1039
+ var import_jsx_runtime18 = require("react/jsx-runtime");
991
1040
  function resolveAbsoluteUrl(url) {
992
1041
  try {
993
1042
  const parsed = new URL(url, window.location.href);
@@ -1020,20 +1069,20 @@ async function copyText(text) {
1020
1069
  }
1021
1070
  }
1022
1071
  function SharePopup({ target, url, onClose }) {
1023
- const [absoluteUrl, setAbsoluteUrl] = (0, import_react9.useState)(url);
1024
- const [copyStatus, setCopyStatus] = (0, import_react9.useState)("idle");
1025
- const [canUseNativeShare, setCanUseNativeShare] = (0, import_react9.useState)(false);
1026
- const [shareError, setShareError] = (0, import_react9.useState)(null);
1027
- const [toastId, setToastId] = (0, import_react9.useState)(0);
1028
- (0, import_react9.useEffect)(() => {
1072
+ const [absoluteUrl, setAbsoluteUrl] = (0, import_react10.useState)(url);
1073
+ const [copyStatus, setCopyStatus] = (0, import_react10.useState)("idle");
1074
+ const [canUseNativeShare, setCanUseNativeShare] = (0, import_react10.useState)(false);
1075
+ const [shareError, setShareError] = (0, import_react10.useState)(null);
1076
+ const [toastId, setToastId] = (0, import_react10.useState)(0);
1077
+ (0, import_react10.useEffect)(() => {
1029
1078
  setAbsoluteUrl(resolveAbsoluteUrl(url) ?? "");
1030
1079
  setCopyStatus("idle");
1031
1080
  setShareError(null);
1032
1081
  }, [url]);
1033
- (0, import_react9.useEffect)(() => {
1082
+ (0, import_react10.useEffect)(() => {
1034
1083
  setCanUseNativeShare(typeof navigator !== "undefined" && typeof navigator.share === "function");
1035
1084
  }, []);
1036
- const destinationUrls = (0, import_react9.useMemo)(() => {
1085
+ const destinationUrls = (0, import_react10.useMemo)(() => {
1037
1086
  if (!absoluteUrl) return null;
1038
1087
  const message = `${target.name} \u2014 ${absoluteUrl}`;
1039
1088
  return {
@@ -1051,7 +1100,7 @@ function SharePopup({ target, url, onClose }) {
1051
1100
  setCopyStatus(copied ? "idle" : "failed");
1052
1101
  if (copied) setToastId((id) => id + 1);
1053
1102
  };
1054
- const dismissToast = (0, import_react9.useCallback)(() => setToastId(0), []);
1103
+ const dismissToast = (0, import_react10.useCallback)(() => setToastId(0), []);
1055
1104
  const handleNativeShare = async () => {
1056
1105
  if (!absoluteUrl || !navigator.share) return;
1057
1106
  setShareError(null);
@@ -1062,8 +1111,8 @@ function SharePopup({ target, url, onClose }) {
1062
1111
  setShareError("Could not open the share options.");
1063
1112
  }
1064
1113
  };
1065
- return /* @__PURE__ */ (0, import_jsx_runtime17.jsxs)(import_jsx_runtime17.Fragment, { children: [
1066
- /* @__PURE__ */ (0, import_jsx_runtime17.jsx)(
1114
+ return /* @__PURE__ */ (0, import_jsx_runtime18.jsxs)(import_jsx_runtime18.Fragment, { children: [
1115
+ /* @__PURE__ */ (0, import_jsx_runtime18.jsx)(
1067
1116
  BasePopup_default,
1068
1117
  {
1069
1118
  title: "Share",
@@ -1078,67 +1127,67 @@ function SharePopup({ target, url, onClose }) {
1078
1127
  titleTextClassName: "!text-xl !font-semibold !text-[var(--color-text)]",
1079
1128
  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)]",
1080
1129
  contentClassName: "!rounded-none !bg-transparent !px-6 !pb-6 !pt-2",
1081
- children: /* @__PURE__ */ (0, import_jsx_runtime17.jsxs)("div", { className: "flex flex-col gap-4", children: [
1082
- /* @__PURE__ */ (0, import_jsx_runtime17.jsx)(SharePreviewCard, { target, url: absoluteUrl || url }),
1083
- /* @__PURE__ */ (0, import_jsx_runtime17.jsxs)("div", { className: "grid grid-cols-3 gap-1 rounded-3xl bg-[var(--color-surface)] p-3 sm:grid-cols-4", children: [
1084
- /* @__PURE__ */ (0, import_jsx_runtime17.jsx)(
1130
+ children: /* @__PURE__ */ (0, import_jsx_runtime18.jsxs)("div", { className: "flex flex-col gap-4", children: [
1131
+ /* @__PURE__ */ (0, import_jsx_runtime18.jsx)(SharePreviewCard, { target, url: absoluteUrl || url }),
1132
+ /* @__PURE__ */ (0, import_jsx_runtime18.jsxs)("div", { className: "grid grid-cols-3 gap-1 rounded-3xl bg-[var(--color-surface)] p-3 sm:grid-cols-4", children: [
1133
+ /* @__PURE__ */ (0, import_jsx_runtime18.jsx)(
1085
1134
  ShareDestinationButton,
1086
1135
  {
1087
1136
  label: "Copy Link",
1088
- icon: /* @__PURE__ */ (0, import_jsx_runtime17.jsx)(import_lucide_react5.Link2, { "aria-hidden": "true", className: "h-6 w-6" }),
1137
+ icon: /* @__PURE__ */ (0, import_jsx_runtime18.jsx)(import_lucide_react5.Link2, { "aria-hidden": "true", className: "h-6 w-6" }),
1089
1138
  onClick: () => void handleCopy(),
1090
1139
  ariaLabel: "Copy link",
1091
1140
  disabled: !absoluteUrl
1092
1141
  }
1093
1142
  ),
1094
- /* @__PURE__ */ (0, import_jsx_runtime17.jsx)(
1143
+ /* @__PURE__ */ (0, import_jsx_runtime18.jsx)(
1095
1144
  ShareDestinationButton,
1096
1145
  {
1097
1146
  label: "WhatsApp",
1098
- icon: /* @__PURE__ */ (0, import_jsx_runtime17.jsx)(import_fa.FaWhatsapp, { "aria-hidden": "true", className: "h-9 w-9 text-[#25D366]" }),
1147
+ icon: /* @__PURE__ */ (0, import_jsx_runtime18.jsx)(import_fa.FaWhatsapp, { "aria-hidden": "true", className: "h-9 w-9 text-[#25D366]" }),
1099
1148
  href: destinationUrls?.whatsapp,
1100
1149
  ariaLabel: `Share ${target.name} via WhatsApp`,
1101
1150
  disabled: !absoluteUrl
1102
1151
  }
1103
1152
  ),
1104
- /* @__PURE__ */ (0, import_jsx_runtime17.jsx)(
1153
+ /* @__PURE__ */ (0, import_jsx_runtime18.jsx)(
1105
1154
  ShareDestinationButton,
1106
1155
  {
1107
1156
  label: "Gmail",
1108
- icon: /* @__PURE__ */ (0, import_jsx_runtime17.jsx)(import_si.SiGmail, { "aria-hidden": "true", className: "h-8 w-8 text-[#EA4335]" }),
1157
+ icon: /* @__PURE__ */ (0, import_jsx_runtime18.jsx)(import_si.SiGmail, { "aria-hidden": "true", className: "h-8 w-8 text-[#EA4335]" }),
1109
1158
  href: destinationUrls?.gmail,
1110
1159
  ariaLabel: `Email ${target.name} with Gmail`,
1111
1160
  disabled: !absoluteUrl
1112
1161
  }
1113
1162
  ),
1114
- canUseNativeShare && /* @__PURE__ */ (0, import_jsx_runtime17.jsx)(
1163
+ canUseNativeShare && /* @__PURE__ */ (0, import_jsx_runtime18.jsx)(
1115
1164
  ShareDestinationButton,
1116
1165
  {
1117
1166
  label: "More",
1118
- icon: /* @__PURE__ */ (0, import_jsx_runtime17.jsx)(import_lucide_react5.Share2, { "aria-hidden": "true", className: "h-6 w-6" }),
1167
+ icon: /* @__PURE__ */ (0, import_jsx_runtime18.jsx)(import_lucide_react5.Share2, { "aria-hidden": "true", className: "h-6 w-6" }),
1119
1168
  onClick: () => void handleNativeShare(),
1120
1169
  ariaLabel: "More sharing options",
1121
1170
  disabled: !absoluteUrl
1122
1171
  }
1123
1172
  )
1124
1173
  ] }),
1125
- copyStatus === "failed" && /* @__PURE__ */ (0, import_jsx_runtime17.jsx)("p", { role: "alert", className: "text-center text-sm text-[var(--color-red-500)]", children: "Could not copy the link." }),
1126
- shareError && /* @__PURE__ */ (0, import_jsx_runtime17.jsx)("p", { role: "alert", className: "text-center text-sm text-[var(--color-red-500)]", children: shareError })
1174
+ copyStatus === "failed" && /* @__PURE__ */ (0, import_jsx_runtime18.jsx)("p", { role: "alert", className: "text-center text-sm text-[var(--color-red-500)]", children: "Could not copy the link." }),
1175
+ shareError && /* @__PURE__ */ (0, import_jsx_runtime18.jsx)("p", { role: "alert", className: "text-center text-sm text-[var(--color-red-500)]", children: shareError })
1127
1176
  ] })
1128
1177
  }
1129
1178
  ),
1130
- toastId > 0 && /* @__PURE__ */ (0, import_jsx_runtime17.jsx)(Toast, { message: "Link copied", onClose: dismissToast }, toastId)
1179
+ toastId > 0 && /* @__PURE__ */ (0, import_jsx_runtime18.jsx)(Toast, { message: "Link copied", onClose: dismissToast }, toastId)
1131
1180
  ] });
1132
1181
  }
1133
1182
 
1134
1183
  // src/genre-tree/ShareActionButton.tsx
1135
- var import_jsx_runtime18 = require("react/jsx-runtime");
1184
+ var import_jsx_runtime19 = require("react/jsx-runtime");
1136
1185
  function ShareActionButton({ target, url }) {
1137
1186
  const { showPopup, hidePopup } = (0, import_popup3.usePopup)();
1138
1187
  const openSharePopup = () => {
1139
- showPopup(/* @__PURE__ */ (0, import_jsx_runtime18.jsx)(SharePopup, { target, url, onClose: hidePopup }));
1188
+ showPopup(/* @__PURE__ */ (0, import_jsx_runtime19.jsx)(SharePopup, { target, url, onClose: hidePopup }));
1140
1189
  };
1141
- return /* @__PURE__ */ (0, import_jsx_runtime18.jsxs)(
1190
+ return /* @__PURE__ */ (0, import_jsx_runtime19.jsxs)(
1142
1191
  "button",
1143
1192
  {
1144
1193
  type: "button",
@@ -1147,7 +1196,7 @@ function ShareActionButton({ target, url }) {
1147
1196
  "aria-label": `Share ${target.name}`,
1148
1197
  title: `Share ${target.name}`,
1149
1198
  children: [
1150
- /* @__PURE__ */ (0, import_jsx_runtime18.jsx)("span", { className: "flex items-center justify-center w-10 h-10 rounded-full group-focus-visible:ring-2 group-focus-visible:ring-[var(--color-focus-ring)] bg-[var(--color-surface-muted)] transition-colors group-hover:bg-[var(--color-border)]", children: /* @__PURE__ */ (0, import_jsx_runtime18.jsx)(import_lucide_react6.Share2, { "aria-hidden": "true", className: "w-5 h-5" }) }),
1199
+ /* @__PURE__ */ (0, import_jsx_runtime19.jsx)("span", { className: "flex items-center justify-center w-10 h-10 rounded-full group-focus-visible:ring-2 group-focus-visible:ring-[var(--color-focus-ring)] bg-[var(--color-surface-muted)] transition-colors group-hover:bg-[var(--color-border)]", children: /* @__PURE__ */ (0, import_jsx_runtime19.jsx)(import_lucide_react6.Share2, { "aria-hidden": "true", className: "w-5 h-5" }) }),
1151
1200
  "Share"
1152
1201
  ]
1153
1202
  }
@@ -1155,7 +1204,7 @@ function ShareActionButton({ target, url }) {
1155
1204
  }
1156
1205
 
1157
1206
  // src/genre-tree/GenreActionBand.tsx
1158
- var import_jsx_runtime19 = require("react/jsx-runtime");
1207
+ var import_jsx_runtime20 = require("react/jsx-runtime");
1159
1208
  function GenreActionBand({
1160
1209
  node,
1161
1210
  onPlayPause,
@@ -1177,7 +1226,7 @@ function GenreActionBand({
1177
1226
  items.push({
1178
1227
  key: "play-pause",
1179
1228
  label: isPlaying ? "Pause" : "Play",
1180
- icon: isPlaying ? /* @__PURE__ */ (0, import_jsx_runtime19.jsx)(import_lucide_react7.Pause, { "aria-hidden": "true", className: "w-5 h-5" }) : /* @__PURE__ */ (0, import_jsx_runtime19.jsx)(import_lucide_react7.Play, { "aria-hidden": "true", className: "w-5 h-5" }),
1229
+ icon: isPlaying ? /* @__PURE__ */ (0, import_jsx_runtime20.jsx)(import_lucide_react7.Pause, { "aria-hidden": "true", className: "w-5 h-5" }) : /* @__PURE__ */ (0, import_jsx_runtime20.jsx)(import_lucide_react7.Play, { "aria-hidden": "true", className: "w-5 h-5" }),
1181
1230
  onClick: onPlayPause
1182
1231
  });
1183
1232
  }
@@ -1185,24 +1234,24 @@ function GenreActionBand({
1185
1234
  items.push({
1186
1235
  key: "zoom-in",
1187
1236
  label: "Focus",
1188
- icon: /* @__PURE__ */ (0, import_jsx_runtime19.jsx)(import_lucide_react7.ZoomIn, { "aria-hidden": "true", className: "w-5 h-5" }),
1237
+ icon: /* @__PURE__ */ (0, import_jsx_runtime20.jsx)(import_lucide_react7.ZoomIn, { "aria-hidden": "true", className: "w-5 h-5" }),
1189
1238
  onClick: onZoomIn
1190
1239
  });
1191
1240
  }
1192
1241
  if (onAddChild) {
1193
- adminItems.push({ key: "add-child", label: "Add", icon: /* @__PURE__ */ (0, import_jsx_runtime19.jsx)(import_lucide_react7.Plus, { "aria-hidden": "true", className: "w-5 h-5" }), onClick: onAddChild });
1242
+ adminItems.push({ key: "add-child", label: "Add", icon: /* @__PURE__ */ (0, import_jsx_runtime20.jsx)(import_lucide_react7.Plus, { "aria-hidden": "true", className: "w-5 h-5" }), onClick: onAddChild });
1194
1243
  }
1195
1244
  if (onRename) {
1196
- adminItems.push({ key: "rename", label: "Rename", icon: /* @__PURE__ */ (0, import_jsx_runtime19.jsx)(import_lucide_react7.Pencil, { "aria-hidden": "true", className: "w-5 h-5" }), onClick: onRename });
1245
+ adminItems.push({ key: "rename", label: "Rename", icon: /* @__PURE__ */ (0, import_jsx_runtime20.jsx)(import_lucide_react7.Pencil, { "aria-hidden": "true", className: "w-5 h-5" }), onClick: onRename });
1197
1246
  }
1198
1247
  if (onChangeParent) {
1199
- adminItems.push({ key: "change-parent", label: "Parent", icon: /* @__PURE__ */ (0, import_jsx_runtime19.jsx)(import_lucide_react7.MoveUpRight, { "aria-hidden": "true", className: "w-5 h-5" }), onClick: onChangeParent });
1248
+ adminItems.push({ key: "change-parent", label: "Parent", icon: /* @__PURE__ */ (0, import_jsx_runtime20.jsx)(import_lucide_react7.MoveUpRight, { "aria-hidden": "true", className: "w-5 h-5" }), onClick: onChangeParent });
1200
1249
  }
1201
1250
  if (onToggleSide) {
1202
1251
  adminItems.push({
1203
1252
  key: "toggle-side",
1204
1253
  label: side === "pop" ? "Make core" : "Make pop",
1205
- icon: /* @__PURE__ */ (0, import_jsx_runtime19.jsx)(import_lucide_react7.ArrowLeftRight, { "aria-hidden": "true", className: "w-5 h-5" }),
1254
+ icon: /* @__PURE__ */ (0, import_jsx_runtime20.jsx)(import_lucide_react7.ArrowLeftRight, { "aria-hidden": "true", className: "w-5 h-5" }),
1206
1255
  onClick: onToggleSide,
1207
1256
  disabled: !!toggleSideDisabledReason,
1208
1257
  title: toggleSideDisabledReason
@@ -1218,7 +1267,7 @@ function GenreActionBand({
1218
1267
  });
1219
1268
  }
1220
1269
  if (items.length === 0 && (!shareTarget || !shareUrl) && adminItems.length === 0) return null;
1221
- const renderItems = (list) => list.map((item) => /* @__PURE__ */ (0, import_jsx_runtime19.jsxs)(
1270
+ const renderItems = (list) => list.map((item) => /* @__PURE__ */ (0, import_jsx_runtime20.jsxs)(
1222
1271
  "button",
1223
1272
  {
1224
1273
  type: "button",
@@ -1227,25 +1276,25 @@ function GenreActionBand({
1227
1276
  onClick: item.onClick,
1228
1277
  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",
1229
1278
  children: [
1230
- /* @__PURE__ */ (0, import_jsx_runtime19.jsx)("span", { className: "flex items-center justify-center w-10 h-10 rounded-full group-focus-visible:ring-2 group-focus-visible:ring-[var(--color-focus-ring)] bg-[var(--color-surface-muted)] transition-colors group-hover:bg-[var(--color-border)] group-disabled:group-hover:bg-[var(--color-surface-muted)]", children: item.icon }),
1279
+ /* @__PURE__ */ (0, import_jsx_runtime20.jsx)("span", { className: "flex items-center justify-center w-10 h-10 rounded-full group-focus-visible:ring-2 group-focus-visible:ring-[var(--color-focus-ring)] bg-[var(--color-surface-muted)] transition-colors group-hover:bg-[var(--color-border)] group-disabled:group-hover:bg-[var(--color-surface-muted)]", children: item.icon }),
1231
1280
  item.label
1232
1281
  ]
1233
1282
  },
1234
1283
  item.key
1235
1284
  ));
1236
- return /* @__PURE__ */ (0, import_jsx_runtime19.jsxs)("div", { className: "genre-action-band border-b border-[var(--color-border)]", children: [
1237
- (items.length > 0 || shareTarget && shareUrl) && /* @__PURE__ */ (0, import_jsx_runtime19.jsxs)("div", { className: "flex flex-wrap gap-1 px-3.5 py-3", children: [
1285
+ return /* @__PURE__ */ (0, import_jsx_runtime20.jsxs)("div", { className: "genre-action-band border-b border-[var(--color-border)]", children: [
1286
+ (items.length > 0 || shareTarget && shareUrl) && /* @__PURE__ */ (0, import_jsx_runtime20.jsxs)("div", { className: "flex flex-wrap gap-1 px-3.5 py-3", children: [
1238
1287
  renderItems(items),
1239
- shareTarget && shareUrl && /* @__PURE__ */ (0, import_jsx_runtime19.jsx)(ShareActionButton, { target: shareTarget, url: shareUrl })
1288
+ shareTarget && shareUrl && /* @__PURE__ */ (0, import_jsx_runtime20.jsx)(ShareActionButton, { target: shareTarget, url: shareUrl })
1240
1289
  ] }),
1241
- adminItems.length > 0 && /* @__PURE__ */ (0, import_jsx_runtime19.jsxs)(
1290
+ adminItems.length > 0 && /* @__PURE__ */ (0, import_jsx_runtime20.jsxs)(
1242
1291
  "section",
1243
1292
  {
1244
1293
  "aria-label": "Admin actions",
1245
1294
  className: `px-3.5 py-3 ${items.length > 0 ? "border-t border-[var(--color-border)]" : ""}`,
1246
1295
  children: [
1247
- /* @__PURE__ */ (0, import_jsx_runtime19.jsx)("h3", { className: "mb-2 text-xs font-semibold uppercase tracking-wide text-[var(--color-text-muted)]", children: "Admin" }),
1248
- /* @__PURE__ */ (0, import_jsx_runtime19.jsx)("div", { className: "flex flex-wrap gap-1", children: renderItems(adminItems) })
1296
+ /* @__PURE__ */ (0, import_jsx_runtime20.jsx)("h3", { className: "mb-2 text-xs font-semibold uppercase tracking-wide text-[var(--color-text-muted)]", children: "Admin" }),
1297
+ /* @__PURE__ */ (0, import_jsx_runtime20.jsx)("div", { className: "flex flex-wrap gap-1", children: renderItems(adminItems) })
1249
1298
  ]
1250
1299
  }
1251
1300
  )
@@ -1257,20 +1306,20 @@ var import_ui6 = require("@behindthemusictree/ui");
1257
1306
  var import_genre_tree17 = require("@behindthemusictree/app-kit/genre-tree");
1258
1307
 
1259
1308
  // src/genre-tree/ClampedText.tsx
1260
- var import_react10 = require("react");
1261
- var import_jsx_runtime20 = require("react/jsx-runtime");
1309
+ var import_react11 = require("react");
1310
+ var import_jsx_runtime21 = require("react/jsx-runtime");
1262
1311
  function ClampedText({ children, className: className2 = "" }) {
1263
- const ref = (0, import_react10.useRef)(null);
1264
- const [expanded, setExpanded] = (0, import_react10.useState)(false);
1265
- const [overflows, setOverflows] = (0, import_react10.useState)(false);
1266
- (0, import_react10.useLayoutEffect)(() => {
1312
+ const ref = (0, import_react11.useRef)(null);
1313
+ const [expanded, setExpanded] = (0, import_react11.useState)(false);
1314
+ const [overflows, setOverflows] = (0, import_react11.useState)(false);
1315
+ (0, import_react11.useLayoutEffect)(() => {
1267
1316
  const element = ref.current;
1268
1317
  if (!element || expanded) return;
1269
1318
  setOverflows(element.scrollHeight > element.clientHeight);
1270
1319
  }, [children, expanded]);
1271
- return /* @__PURE__ */ (0, import_jsx_runtime20.jsxs)(import_jsx_runtime20.Fragment, { children: [
1272
- /* @__PURE__ */ (0, import_jsx_runtime20.jsx)("p", { ref, className: `${className2} ${expanded ? "" : "line-clamp-3"}`, children }),
1273
- (overflows || expanded) && /* @__PURE__ */ (0, import_jsx_runtime20.jsx)(
1320
+ return /* @__PURE__ */ (0, import_jsx_runtime21.jsxs)(import_jsx_runtime21.Fragment, { children: [
1321
+ /* @__PURE__ */ (0, import_jsx_runtime21.jsx)("p", { ref, className: `${className2} ${expanded ? "" : "line-clamp-3"}`, children }),
1322
+ (overflows || expanded) && /* @__PURE__ */ (0, import_jsx_runtime21.jsx)(
1274
1323
  "button",
1275
1324
  {
1276
1325
  type: "button",
@@ -1284,7 +1333,7 @@ function ClampedText({ children, className: className2 = "" }) {
1284
1333
  }
1285
1334
 
1286
1335
  // src/genre-tree/GenrePlaylistTracks.tsx
1287
- var import_react11 = require("react");
1336
+ var import_react12 = require("react");
1288
1337
  var import_react_query = require("@tanstack/react-query");
1289
1338
  var import_zod = require("zod");
1290
1339
  var import_fa2 = require("react-icons/fa");
@@ -1305,15 +1354,15 @@ var import_genre_tree15 = require("@behindthemusictree/app-kit/genre-tree");
1305
1354
 
1306
1355
  // src/genre-tree/MusicBrainzRecordingLink.tsx
1307
1356
  var import_genre_tree10 = require("@behindthemusictree/app-kit/genre-tree");
1308
- var import_jsx_runtime21 = require("react/jsx-runtime");
1357
+ var import_jsx_runtime22 = require("react/jsx-runtime");
1309
1358
  function MusicBrainzRecordingLink({
1310
1359
  song,
1311
1360
  children,
1312
1361
  className: className2 = ""
1313
1362
  }) {
1314
1363
  const url = (0, import_genre_tree10.musicBrainzRecordingUrl)(song);
1315
- if (!url) return /* @__PURE__ */ (0, import_jsx_runtime21.jsx)(import_jsx_runtime21.Fragment, { children });
1316
- return /* @__PURE__ */ (0, import_jsx_runtime21.jsx)(
1364
+ if (!url) return /* @__PURE__ */ (0, import_jsx_runtime22.jsx)(import_jsx_runtime22.Fragment, { children });
1365
+ return /* @__PURE__ */ (0, import_jsx_runtime22.jsx)(
1317
1366
  "a",
1318
1367
  {
1319
1368
  href: url,
@@ -1330,7 +1379,7 @@ function MusicBrainzRecordingLink({
1330
1379
  // src/genre-tree/GenrePlaylistTracks.tsx
1331
1380
  var import_genre_tree16 = require("@behindthemusictree/app-kit/genre-tree");
1332
1381
  var import_track_noun = require("@behindthemusictree/app-kit/track-noun");
1333
- var import_jsx_runtime22 = require("react/jsx-runtime");
1382
+ var import_jsx_runtime23 = require("react/jsx-runtime");
1334
1383
  var CATALOGUE_PAGE_SIZE = 100;
1335
1384
  var SongPageSchema = (0, import_transport4.PaginatedResponseSchema)(import_genre_tree14.SongDetailedSchema).extend({
1336
1385
  overallTotal: import_zod.z.number().nullable(),
@@ -1350,6 +1399,7 @@ var nextCursor = (next) => {
1350
1399
  function GenrePlaylistTracks({
1351
1400
  genreUuid,
1352
1401
  genrePlaylist,
1402
+ genreName: selectedGenreName,
1353
1403
  selection,
1354
1404
  scope,
1355
1405
  getBackendBaseUrl,
@@ -1365,13 +1415,17 @@ function GenrePlaylistTracks({
1365
1415
  const { playerTrackObject, playState, handlePlayPauseAction } = (0, import_player4.usePlayer)();
1366
1416
  const { showPopup } = (0, import_popup4.usePopup)();
1367
1417
  const trackNoun = (0, import_track_noun.useTrackNoun)();
1368
- const { uuid } = genrePlaylist;
1418
+ const uuid = genrePlaylist?.uuid;
1419
+ const genreName = selectedGenreName ?? genrePlaylist?.name ?? "Genre";
1369
1420
  const { fetch } = (0, import_transport3.useFetchWrapper)(getBackendBaseUrl);
1370
1421
  const playback = (0, import_transport3.usePlaybackContext)();
1371
1422
  const { data, isPending, isError, refetch, hasNextPage, isFetchingNextPage, fetchNextPage } = (0, import_react_query.useInfiniteQuery)({
1372
- queryKey: [...scope === "me" ? import_genre_tree12.genrePlaylistQueryKeys.me.tracks(uuid) : artist ? [...import_genre_tree13.libraryQueryKeys.reference.songs.byGenre(getBackendBaseUrl(), genreUuid, selection), "artist", artist.uuid] : import_genre_tree13.libraryQueryKeys.reference.songs.byGenre(getBackendBaseUrl(), genreUuid, selection), ...playback.key ? [playback.key] : []],
1423
+ queryKey: [...scope === "me" ? uuid ? import_genre_tree12.genrePlaylistQueryKeys.me.tracks(uuid) : ["missing-personal-genre-playlist", genreUuid] : artist ? [...import_genre_tree13.libraryQueryKeys.reference.songs.byGenre(getBackendBaseUrl(), genreUuid, selection), "artist", artist.uuid] : import_genre_tree13.libraryQueryKeys.reference.songs.byGenre(getBackendBaseUrl(), genreUuid, selection), ...playback.key ? [playback.key] : []],
1373
1424
  queryFn: async ({ pageParam }) => {
1374
- if (scope === "me") return fetchGenrePlaylistTracksPage(uuid, scope, Number(pageParam));
1425
+ if (scope === "me") {
1426
+ if (!uuid) throw new Error("Personal genre has no playlist");
1427
+ return fetchGenrePlaylistTracksPage(uuid, scope, Number(pageParam));
1428
+ }
1375
1429
  const response = await fetch(
1376
1430
  import_genre_tree13.libraryEndpoints.reference.songs.list(),
1377
1431
  true,
@@ -1384,10 +1438,10 @@ function GenrePlaylistTracks({
1384
1438
  },
1385
1439
  initialPageParam: scope === "me" ? 1 : "",
1386
1440
  getNextPageParam: (lastPage) => lastPage.nextPage ?? void 0,
1387
- enabled: playback.ready && (scope !== "me" || sessionRestored && !!session?.accessToken)
1441
+ enabled: playback.ready && (scope !== "me" || uuid !== void 0 && sessionRestored && !!session?.accessToken)
1388
1442
  });
1389
- const [sentinel, setSentinel] = (0, import_react11.useState)(null);
1390
- (0, import_react11.useEffect)(() => {
1443
+ const [sentinel, setSentinel] = (0, import_react12.useState)(null);
1444
+ (0, import_react12.useEffect)(() => {
1391
1445
  if (!hasNextPage || isFetchingNextPage || !sentinel) return;
1392
1446
  const observer = new IntersectionObserver((entries) => {
1393
1447
  if (entries.some((entry) => entry.isIntersecting)) {
@@ -1406,38 +1460,38 @@ function GenrePlaylistTracks({
1406
1460
  ordering: "title",
1407
1461
  genreUuid,
1408
1462
  ...selection && { selection }
1409
- }) : scope === "reference" ? playNewTrackListFromGenreCatalogue({ uuid: genreUuid, name: genrePlaylist.name }, scope, { startSongUuid, ...selection && { selection } }) : playNewTrackListFromGenrePlaylist(genrePlaylist, scope, { startSongUuid });
1463
+ }) : 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"));
1410
1464
  started.catch((error) => {
1411
1465
  console.error("Failed to play genre playlist:", error);
1412
- showPopup(/* @__PURE__ */ (0, import_jsx_runtime22.jsx)(InternalErrorPopup, { errorCode: import_transport6.ErrorCode.CLIENT_UNKNOWN }));
1466
+ showPopup(/* @__PURE__ */ (0, import_jsx_runtime23.jsx)(InternalErrorPopup, { errorCode: import_transport6.ErrorCode.CLIENT_UNKNOWN }));
1413
1467
  });
1414
1468
  };
1415
- return /* @__PURE__ */ (0, import_jsx_runtime22.jsx)(import_ui5.PanelSection, { title: `${(0, import_track_noun.capitalize)(trackNoun.plural)}${total !== void 0 ? ` (${total})` : ""}`, children: /* @__PURE__ */ (0, import_jsx_runtime22.jsxs)("div", { children: [
1469
+ return /* @__PURE__ */ (0, import_jsx_runtime23.jsx)(import_ui5.PanelSection, { title: `${(0, import_track_noun.capitalize)(trackNoun.plural)}${total !== void 0 ? ` (${total})` : ""}`, children: /* @__PURE__ */ (0, import_jsx_runtime23.jsxs)("div", { children: [
1416
1470
  renderTracksToolbar?.(),
1417
- isPending ? /* @__PURE__ */ (0, import_jsx_runtime22.jsx)(import_ui5.Skeleton, { className: "h-4 w-3/4", "aria-busy": "true" }) : isError && tracks.length === 0 ? /* @__PURE__ */ (0, import_jsx_runtime22.jsxs)("p", { role: "alert", children: [
1471
+ scope === "me" && !genrePlaylist ? /* @__PURE__ */ (0, import_jsx_runtime23.jsx)("p", { children: "\u2014" }) : isPending ? /* @__PURE__ */ (0, import_jsx_runtime23.jsx)(import_ui5.Skeleton, { className: "h-4 w-3/4", "aria-busy": "true" }) : isError && tracks.length === 0 ? /* @__PURE__ */ (0, import_jsx_runtime23.jsxs)("p", { role: "alert", children: [
1418
1472
  "Couldn't load ",
1419
1473
  trackNoun.plural,
1420
1474
  ".",
1421
1475
  " ",
1422
- /* @__PURE__ */ (0, import_jsx_runtime22.jsx)("button", { type: "button", className: "underline", onClick: () => void refetch(), children: "Retry" })
1423
- ] }) : tracks.length === 0 ? /* @__PURE__ */ (0, import_jsx_runtime22.jsx)("p", { children: "\u2014" }) : /* @__PURE__ */ (0, import_jsx_runtime22.jsxs)("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: [
1476
+ /* @__PURE__ */ (0, import_jsx_runtime23.jsx)("button", { type: "button", className: "underline", onClick: () => void refetch(), children: "Retry" })
1477
+ ] }) : tracks.length === 0 ? /* @__PURE__ */ (0, import_jsx_runtime23.jsx)("p", { children: "\u2014" }) : /* @__PURE__ */ (0, import_jsx_runtime23.jsxs)("ul", { className: "mt-1 max-h-64 overflow-y-auto text-[0.8125rem] leading-[1.45] [container-type:inline-size]", "aria-label": `${genreName} ${trackNoun.plural}`, children: [
1424
1478
  tracks.map((track, index) => {
1425
1479
  const label = track.artists?.length ? `${track.title} \u2014 ${track.artists.map((artist2) => artist2.name).join(", ")}` : track.title;
1426
1480
  const isCurrent = playerTrackObject?.track.id === track.uuid;
1427
1481
  const isPlaying = isCurrent && playState === import_player5.PlayStates.PLAYING;
1428
- const text = /* @__PURE__ */ (0, import_jsx_runtime22.jsxs)("span", { className: TEXT_CELL, children: [
1429
- /* @__PURE__ */ (0, import_jsx_runtime22.jsx)("span", { className: TITLE_CELL, children: renderSongLink ? renderSongLink(track) : (0, import_genre_tree14.isSong)(track) ? /* @__PURE__ */ (0, import_jsx_runtime22.jsx)(MusicBrainzRecordingLink, { song: track, className: "hover:underline focus-visible:underline", children: track.title }) : track.title }),
1430
- track.artists?.length ? /* @__PURE__ */ (0, import_jsx_runtime22.jsx)("span", { className: SEPARATOR, children: " \u2014 " }) : null,
1431
- /* @__PURE__ */ (0, import_jsx_runtime22.jsx)("span", { className: COLUMN_CELL, children: track.artists?.length ? /* @__PURE__ */ (0, import_jsx_runtime22.jsx)(import_genre_tree16.ArtistNames, { artists: track.artists, renderArtistLink }) : null }),
1432
- track.album && /* @__PURE__ */ (0, import_jsx_runtime22.jsx)("span", { className: ALBUM_CELL, children: track.album.name })
1482
+ const text = /* @__PURE__ */ (0, import_jsx_runtime23.jsxs)("span", { className: TEXT_CELL, children: [
1483
+ /* @__PURE__ */ (0, import_jsx_runtime23.jsx)("span", { className: TITLE_CELL, children: renderSongLink ? renderSongLink(track) : (0, import_genre_tree14.isSong)(track) ? /* @__PURE__ */ (0, import_jsx_runtime23.jsx)(MusicBrainzRecordingLink, { song: track, className: "hover:underline focus-visible:underline", children: track.title }) : track.title }),
1484
+ track.artists?.length ? /* @__PURE__ */ (0, import_jsx_runtime23.jsx)("span", { className: SEPARATOR, children: " \u2014 " }) : null,
1485
+ /* @__PURE__ */ (0, import_jsx_runtime23.jsx)("span", { className: COLUMN_CELL, children: track.artists?.length ? /* @__PURE__ */ (0, import_jsx_runtime23.jsx)(import_genre_tree16.ArtistNames, { artists: track.artists, renderArtistLink }) : null }),
1486
+ track.album && /* @__PURE__ */ (0, import_jsx_runtime23.jsx)("span", { className: ALBUM_CELL, children: track.album.name })
1433
1487
  ] });
1434
1488
  const leading = renderSongLeading?.(track, index);
1435
1489
  const actions = renderSongActions?.(track);
1436
1490
  const isUnplayable = (0, import_genre_tree14.isSong)(track) && (0, import_genre_tree14.songUnplayableReason)(track) !== null;
1437
1491
  if (isUnplayable) {
1438
- return /* @__PURE__ */ (0, import_jsx_runtime22.jsxs)("li", { className: "flex min-h-8 items-center gap-1 px-1 opacity-50", children: [
1492
+ return /* @__PURE__ */ (0, import_jsx_runtime23.jsxs)("li", { className: "flex min-h-8 items-center gap-1 px-1 opacity-50", children: [
1439
1493
  leading,
1440
- /* @__PURE__ */ (0, import_jsx_runtime22.jsx)(
1494
+ /* @__PURE__ */ (0, import_jsx_runtime23.jsx)(
1441
1495
  "button",
1442
1496
  {
1443
1497
  type: "button",
@@ -1445,27 +1499,27 @@ function GenrePlaylistTracks({
1445
1499
  "aria-disabled": "true",
1446
1500
  "aria-label": label,
1447
1501
  className: "flex h-6 w-6 shrink-0 items-center justify-center rounded-full opacity-0 cursor-not-allowed",
1448
- children: /* @__PURE__ */ (0, import_jsx_runtime22.jsx)(import_fa2.FaPlay, { size: 10 })
1502
+ children: /* @__PURE__ */ (0, import_jsx_runtime23.jsx)(import_fa2.FaPlay, { size: 10 })
1449
1503
  }
1450
1504
  ),
1451
1505
  text,
1452
1506
  actions
1453
1507
  ] }, track.uuid);
1454
1508
  }
1455
- return /* @__PURE__ */ (0, import_jsx_runtime22.jsxs)(
1509
+ return /* @__PURE__ */ (0, import_jsx_runtime23.jsxs)(
1456
1510
  "li",
1457
1511
  {
1458
1512
  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)]" : ""}`,
1459
1513
  children: [
1460
1514
  leading,
1461
- /* @__PURE__ */ (0, import_jsx_runtime22.jsx)(
1515
+ /* @__PURE__ */ (0, import_jsx_runtime23.jsx)(
1462
1516
  "button",
1463
1517
  {
1464
1518
  type: "button",
1465
1519
  "aria-label": `${isPlaying ? "Pause" : "Play"} ${label}`,
1466
1520
  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 !== import_player5.PlayStates.STOPPED ? "opacity-100" : "opacity-0 [@media(pointer:coarse)]:opacity-100"}`,
1467
1521
  onClick: () => isCurrent ? handlePlayPauseAction() : play(track.uuid),
1468
- children: isCurrent && playState === import_player5.PlayStates.LOADING ? /* @__PURE__ */ (0, import_jsx_runtime22.jsx)(import_ui5.RingLoader, { size: 10 }) : isPlaying ? /* @__PURE__ */ (0, import_jsx_runtime22.jsx)(import_fa2.FaPause, { size: 10 }) : /* @__PURE__ */ (0, import_jsx_runtime22.jsx)(import_fa2.FaPlay, { size: 10 })
1522
+ children: isCurrent && playState === import_player5.PlayStates.LOADING ? /* @__PURE__ */ (0, import_jsx_runtime23.jsx)(import_ui5.RingLoader, { size: 10 }) : isPlaying ? /* @__PURE__ */ (0, import_jsx_runtime23.jsx)(import_fa2.FaPause, { size: 10 }) : /* @__PURE__ */ (0, import_jsx_runtime23.jsx)(import_fa2.FaPlay, { size: 10 })
1469
1523
  }
1470
1524
  ),
1471
1525
  text,
@@ -1475,7 +1529,7 @@ function GenrePlaylistTracks({
1475
1529
  track.uuid
1476
1530
  );
1477
1531
  }),
1478
- hasNextPage && /* @__PURE__ */ (0, import_jsx_runtime22.jsx)("li", { ref: setSentinel, "aria-hidden": "true", className: "h-4" })
1532
+ hasNextPage && /* @__PURE__ */ (0, import_jsx_runtime23.jsx)("li", { ref: setSentinel, "aria-hidden": "true", className: "h-4" })
1479
1533
  ] })
1480
1534
  ] }) });
1481
1535
  }
@@ -1483,10 +1537,11 @@ function GenrePlaylistTracks({
1483
1537
  // src/genre-tree/GenreDetailExtras.tsx
1484
1538
  var import_track_noun2 = require("@behindthemusictree/app-kit/track-noun");
1485
1539
  var import_si2 = require("react-icons/si");
1486
- var import_jsx_runtime23 = require("react/jsx-runtime");
1540
+ var import_jsx_runtime24 = require("react/jsx-runtime");
1487
1541
  function GenreDetailExtras({
1488
1542
  genreUuid,
1489
1543
  genrePlaylist,
1544
+ genreName,
1490
1545
  selection,
1491
1546
  wikidataId,
1492
1547
  scope,
@@ -1508,17 +1563,17 @@ function GenreDetailExtras({
1508
1563
  criteriaOverviewSchema
1509
1564
  );
1510
1565
  if (isPending) {
1511
- return /* @__PURE__ */ (0, import_jsx_runtime23.jsxs)(import_jsx_runtime23.Fragment, { children: [
1512
- /* @__PURE__ */ (0, import_jsx_runtime23.jsx)(import_ui6.PanelSection, { title: "Summary", children: /* @__PURE__ */ (0, import_jsx_runtime23.jsx)("div", { "aria-busy": "true", "data-testid": "genre-detail-extras-skeleton", children: /* @__PURE__ */ (0, import_jsx_runtime23.jsx)(import_ui6.Skeleton, { className: "h-4 w-3/4" }) }) }),
1566
+ return /* @__PURE__ */ (0, import_jsx_runtime24.jsxs)(import_jsx_runtime24.Fragment, { children: [
1567
+ /* @__PURE__ */ (0, import_jsx_runtime24.jsx)(import_ui6.PanelSection, { title: "Summary", children: /* @__PURE__ */ (0, import_jsx_runtime24.jsx)("div", { "aria-busy": "true", "data-testid": "genre-detail-extras-skeleton", children: /* @__PURE__ */ (0, import_jsx_runtime24.jsx)(import_ui6.Skeleton, { className: "h-4 w-3/4" }) }) }),
1513
1568
  family
1514
1569
  ] });
1515
1570
  }
1516
- if (!overview) return /* @__PURE__ */ (0, import_jsx_runtime23.jsx)(import_jsx_runtime23.Fragment, { children: family });
1571
+ if (!overview) return /* @__PURE__ */ (0, import_jsx_runtime24.jsx)(import_jsx_runtime24.Fragment, { children: family });
1517
1572
  const { summary, summarySourceUrl, essentialSongs } = overview;
1518
1573
  const wikidataUrl = wikidataId ? `https://www.wikidata.org/wiki/${encodeURIComponent(wikidataId)}` : null;
1519
- return /* @__PURE__ */ (0, import_jsx_runtime23.jsxs)(import_jsx_runtime23.Fragment, { children: [
1520
- (summarySourceUrl || wikidataUrl) && /* @__PURE__ */ (0, import_jsx_runtime23.jsx)(import_ui6.PanelSection, { title: "Links", children: /* @__PURE__ */ (0, import_jsx_runtime23.jsxs)("div", { className: "flex flex-wrap gap-x-3 gap-y-1 text-xs text-[var(--color-text)]", children: [
1521
- summarySourceUrl && /* @__PURE__ */ (0, import_jsx_runtime23.jsxs)(
1574
+ return /* @__PURE__ */ (0, import_jsx_runtime24.jsxs)(import_jsx_runtime24.Fragment, { children: [
1575
+ (summarySourceUrl || wikidataUrl) && /* @__PURE__ */ (0, import_jsx_runtime24.jsx)(import_ui6.PanelSection, { title: "Links", children: /* @__PURE__ */ (0, import_jsx_runtime24.jsxs)("div", { className: "flex flex-wrap gap-x-3 gap-y-1 text-xs text-[var(--color-text)]", children: [
1576
+ summarySourceUrl && /* @__PURE__ */ (0, import_jsx_runtime24.jsxs)(
1522
1577
  "a",
1523
1578
  {
1524
1579
  href: summarySourceUrl,
@@ -1526,12 +1581,12 @@ function GenreDetailExtras({
1526
1581
  rel: "noopener noreferrer",
1527
1582
  className: "inline-flex items-center gap-1.5 text-[var(--color-text-muted)] hover:text-[var(--color-text)]",
1528
1583
  children: [
1529
- /* @__PURE__ */ (0, import_jsx_runtime23.jsx)(import_si2.SiWikipedia, { "aria-hidden": "true", className: "h-3.5 w-3.5" }),
1584
+ /* @__PURE__ */ (0, import_jsx_runtime24.jsx)(import_si2.SiWikipedia, { "aria-hidden": "true", className: "h-3.5 w-3.5" }),
1530
1585
  "Wikipedia"
1531
1586
  ]
1532
1587
  }
1533
1588
  ),
1534
- wikidataUrl && /* @__PURE__ */ (0, import_jsx_runtime23.jsxs)(
1589
+ wikidataUrl && /* @__PURE__ */ (0, import_jsx_runtime24.jsxs)(
1535
1590
  "a",
1536
1591
  {
1537
1592
  href: wikidataUrl,
@@ -1539,21 +1594,22 @@ function GenreDetailExtras({
1539
1594
  rel: "noopener noreferrer",
1540
1595
  className: "inline-flex items-center gap-1.5 text-[var(--color-text-muted)] hover:text-[var(--color-text)]",
1541
1596
  children: [
1542
- /* @__PURE__ */ (0, import_jsx_runtime23.jsx)(import_si2.SiWikidata, { "aria-hidden": "true", className: "h-3.5 w-3.5" }),
1597
+ /* @__PURE__ */ (0, import_jsx_runtime24.jsx)(import_si2.SiWikidata, { "aria-hidden": "true", className: "h-3.5 w-3.5" }),
1543
1598
  "Wikidata"
1544
1599
  ]
1545
1600
  }
1546
1601
  )
1547
1602
  ] }) }),
1548
- /* @__PURE__ */ (0, import_jsx_runtime23.jsx)(import_ui6.PanelSection, { title: "Summary", children: renderSummary ? renderSummary(overview) : /* @__PURE__ */ (0, import_jsx_runtime23.jsx)(ClampedText, { className: "text-[0.8125rem] leading-[1.45] text-[var(--color-text)]", children: summary ?? "\u2014" }, genreUuid) }),
1603
+ /* @__PURE__ */ (0, import_jsx_runtime24.jsx)(import_ui6.PanelSection, { title: "Summary", children: renderSummary ? renderSummary(overview) : /* @__PURE__ */ (0, import_jsx_runtime24.jsx)(ClampedText, { className: "text-[0.8125rem] leading-[1.45] text-[var(--color-text)]", children: summary ?? "\u2014" }, genreUuid) }),
1549
1604
  family,
1550
1605
  renderGenreDetailExtras?.(overview),
1551
- (renderEssentialTracks || essentialSongs) && /* @__PURE__ */ (0, import_jsx_runtime23.jsx)(import_ui6.PanelSection, { title: `Essential ${trackNoun.plural}`, children: renderEssentialTracks ? renderEssentialTracks(overview) : essentialSongs && essentialSongs.length > 0 ? /* @__PURE__ */ (0, import_jsx_runtime23.jsx)("ul", { className: "text-[0.8125rem] leading-[1.45] text-[var(--color-text)]", children: essentialSongs.map((track) => /* @__PURE__ */ (0, import_jsx_runtime23.jsx)("li", { className: "py-1", children: track.title }, track.uuid)) }) : /* @__PURE__ */ (0, import_jsx_runtime23.jsx)("p", { className: "text-[0.8125rem] leading-[1.45] text-[var(--color-text)]", children: "\u2014" }) }),
1552
- /* @__PURE__ */ (0, import_jsx_runtime23.jsx)(
1606
+ (renderEssentialTracks || essentialSongs) && /* @__PURE__ */ (0, import_jsx_runtime24.jsx)(import_ui6.PanelSection, { title: `Essential ${trackNoun.plural}`, children: renderEssentialTracks ? renderEssentialTracks(overview) : essentialSongs && essentialSongs.length > 0 ? /* @__PURE__ */ (0, import_jsx_runtime24.jsx)("ul", { className: "text-[0.8125rem] leading-[1.45] text-[var(--color-text)]", children: essentialSongs.map((track) => /* @__PURE__ */ (0, import_jsx_runtime24.jsx)("li", { className: "py-1", children: track.title }, track.uuid)) }) : /* @__PURE__ */ (0, import_jsx_runtime24.jsx)("p", { className: "text-[0.8125rem] leading-[1.45] text-[var(--color-text)]", children: "\u2014" }) }),
1607
+ /* @__PURE__ */ (0, import_jsx_runtime24.jsx)(
1553
1608
  GenrePlaylistTracks,
1554
1609
  {
1555
1610
  genreUuid,
1556
1611
  genrePlaylist,
1612
+ genreName,
1557
1613
  selection,
1558
1614
  scope,
1559
1615
  getBackendBaseUrl,
@@ -1566,8 +1622,8 @@ function GenreDetailExtras({
1566
1622
  }
1567
1623
 
1568
1624
  // src/genre-tree/GenreInfoPanelBody.tsx
1569
- var import_jsx_runtime24 = require("react/jsx-runtime");
1570
- var import_react12 = require("react");
1625
+ var import_jsx_runtime25 = require("react/jsx-runtime");
1626
+ var import_react13 = require("react");
1571
1627
  function GenreNodeActionBand({ genrePlaylist, playback, ...bandProps }) {
1572
1628
  const { isPlaying, setIsPlaying } = (0, import_player6.usePlayer)();
1573
1629
  const isCurrentTrackList = playback.playingNodeId === genrePlaylist.uuid;
@@ -1580,7 +1636,7 @@ function GenreNodeActionBand({ genrePlaylist, playback, ...bandProps }) {
1580
1636
  console.error("Failed to load genre playlist tracks:", error);
1581
1637
  });
1582
1638
  };
1583
- return /* @__PURE__ */ (0, import_jsx_runtime24.jsx)(
1639
+ return /* @__PURE__ */ (0, import_jsx_runtime25.jsx)(
1584
1640
  GenreActionBand,
1585
1641
  {
1586
1642
  ...bandProps,
@@ -1595,6 +1651,7 @@ function GenreInfoPanelBody({
1595
1651
  genreUuid,
1596
1652
  genrePlaylist,
1597
1653
  catalogue,
1654
+ countsContext,
1598
1655
  wikidataId,
1599
1656
  scope,
1600
1657
  getBackendBaseUrl,
@@ -1603,30 +1660,32 @@ function GenreInfoPanelBody({
1603
1660
  extras
1604
1661
  }) {
1605
1662
  const noun = (0, import_track_noun3.useTrackNoun)();
1606
- const summary = scope === "reference" ? import_genre_tree18.GenreCatalogueCountsSchema.parse(genrePlaylist) : void 0;
1663
+ const summary = scope === "reference" ? import_genre_tree18.GenreCatalogueCountsSchema.parse(genrePlaylist ?? catalogue) : void 0;
1607
1664
  const selection = summary ? (0, import_genre_tree18.defaultGenreMusicSelection)(summary) : void 0;
1608
1665
  const describeCounts = (counts) => {
1609
1666
  const scopes = counts.genreAndSubgenresTracksCount;
1610
1667
  return "canonical" in scopes ? `${counts.genreOnlyTracksCount} this genre only \xB7 ${scopes.canonical} with canonical subgenres \xB7 ${scopes.regional} with regional subgenres \xB7 ${scopes.all} with all subgenres` : `${counts.genreOnlyTracksCount} this genre only \xB7 ${scopes.all} with subgenres`;
1611
1668
  };
1612
- return /* @__PURE__ */ (0, import_jsx_runtime24.jsxs)(import_jsx_runtime24.Fragment, { children: [
1669
+ return /* @__PURE__ */ (0, import_jsx_runtime25.jsxs)(import_jsx_runtime25.Fragment, { children: [
1613
1670
  toolbar,
1614
- /* @__PURE__ */ (0, import_jsx_runtime24.jsx)(GenreNodeActionBand, { ...band, genrePlaylist, playback }),
1615
- summary && catalogue && /* @__PURE__ */ (0, import_jsx_runtime24.jsxs)("div", { className: "px-3.5 py-3 text-sm text-[var(--color-text)]", children: [
1616
- /* @__PURE__ */ (0, import_jsx_runtime24.jsxs)("p", { children: [
1671
+ genrePlaylist ? /* @__PURE__ */ (0, import_jsx_runtime25.jsx)(GenreNodeActionBand, { ...band, genrePlaylist, playback }) : /* @__PURE__ */ (0, import_jsx_runtime25.jsx)(GenreActionBand, { ...band }),
1672
+ summary && catalogue && /* @__PURE__ */ (0, import_jsx_runtime25.jsxs)("div", { className: "px-3.5 py-3 text-sm text-[var(--color-text)]", children: [
1673
+ /* @__PURE__ */ (0, import_jsx_runtime25.jsxs)("p", { children: [
1617
1674
  "Catalogue ",
1618
1675
  noun.plural,
1676
+ countsContext ? ` (${countsContext})` : "",
1619
1677
  ": ",
1620
1678
  describeCounts(catalogue)
1621
1679
  ] }),
1622
- /* @__PURE__ */ (0, import_jsx_runtime24.jsxs)("p", { children: [
1680
+ genrePlaylist && /* @__PURE__ */ (0, import_jsx_runtime25.jsxs)("p", { children: [
1623
1681
  "Playlist ",
1624
1682
  noun.plural,
1683
+ countsContext ? ` (${countsContext})` : "",
1625
1684
  ": ",
1626
1685
  describeCounts(summary)
1627
1686
  ] })
1628
1687
  ] }),
1629
- /* @__PURE__ */ (0, import_react12.createElement)(
1688
+ /* @__PURE__ */ (0, import_react13.createElement)(
1630
1689
  GenreDetailExtras,
1631
1690
  {
1632
1691
  ...extras,
@@ -1634,6 +1693,7 @@ function GenreInfoPanelBody({
1634
1693
  family,
1635
1694
  genreUuid,
1636
1695
  genrePlaylist,
1696
+ genreName: band.node.name,
1637
1697
  selection,
1638
1698
  wikidataId,
1639
1699
  scope,
@@ -1643,10 +1703,160 @@ function GenreInfoPanelBody({
1643
1703
  ] });
1644
1704
  }
1645
1705
 
1706
+ // src/genre-tree/GenreDetailPanel.tsx
1707
+ var import_jsx_runtime26 = require("react/jsx-runtime");
1708
+ var RELATIVE_ITEM_COUNT = 1;
1709
+ function toNode(genre, parentId) {
1710
+ return {
1711
+ id: genre.uuid,
1712
+ parentId,
1713
+ name: genre.name,
1714
+ itemCount: RELATIVE_ITEM_COUNT,
1715
+ side: genre.side ?? void 0,
1716
+ wikidataId: genre.wikidataId
1717
+ };
1718
+ }
1719
+ function useGenreDetailContent({
1720
+ genreUuid,
1721
+ scope,
1722
+ getBackendBaseUrl,
1723
+ onSelectGenre,
1724
+ extras,
1725
+ band,
1726
+ playback: hostPlayback,
1727
+ trailing,
1728
+ genrePlaylist: hostPlaylist,
1729
+ expanded
1730
+ }) {
1731
+ const { data: detail, isPending, isError } = (0, import_genre_tree19.useFetchGenreDetail)(genreUuid, scope, getBackendBaseUrl);
1732
+ const defaultPlayback = useGenrePlaylistPlayback({ scope, genrePlaylists: [], artist: extras?.artist });
1733
+ const playback = hostPlayback ?? defaultPlayback;
1734
+ const view = (0, import_react14.useMemo)(() => {
1735
+ if (!detail) return null;
1736
+ const rootColor = (0, import_genre_tree_view4.getGenreTreeColor)(detail.root.wikidataId);
1737
+ const relative = (genre, parentId) => ({
1738
+ node: toNode(genre, parentId),
1739
+ fill: (0, import_genre_tree_view4.tintSurface)(rootColor),
1740
+ textColor: import_genre_tree_view4.TEXT_COLOR
1741
+ });
1742
+ const ancestors2 = [...detail.ascendants].sort((a, b) => b.degree - a.degree);
1743
+ return {
1744
+ node: {
1745
+ id: detail.uuid,
1746
+ parentId: detail.parent?.uuid ?? null,
1747
+ name: detail.name,
1748
+ itemCount: scope === "reference" ? (0, import_genre_tree19.genreMusicCount)(detail.criteriaPlaylist ? import_genre_tree19.GenrePlaylistSummarySchema.parse(detail.criteriaPlaylist) : import_genre_tree19.GenreCatalogueCountsSchema.parse(detail.catalogue)) : detail.tracksCount,
1749
+ wikidataId: detail.wikidataId
1750
+ },
1751
+ fill: rootColor,
1752
+ parentNode: detail.parent ? relative(detail.parent, null) : null,
1753
+ childNodes: detail.children.map((child) => relative(child, detail.uuid)),
1754
+ ancestorNodes: ancestors2.map(
1755
+ ({ ascendant }, index) => relative(ascendant, index === 0 ? null : ancestors2[index - 1].ascendant.uuid)
1756
+ )
1757
+ };
1758
+ }, [detail, scope]);
1759
+ if (isError) return {
1760
+ title: "Genre unavailable",
1761
+ content: /* @__PURE__ */ (0, import_jsx_runtime26.jsx)("p", { role: "alert", className: "p-4 text-sm text-[var(--color-text)]", children: "This genre could not be loaded." })
1762
+ };
1763
+ if (isPending || !detail || !view) return {
1764
+ title: "Loading genre\u2026",
1765
+ content: /* @__PURE__ */ (0, import_jsx_runtime26.jsxs)("div", { className: "flex flex-col gap-3 p-4", "aria-busy": "true", "data-testid": "genre-detail-panel-skeleton", children: [
1766
+ /* @__PURE__ */ (0, import_jsx_runtime26.jsx)(import_ui7.Skeleton, { className: "h-6 w-2/3" }),
1767
+ /* @__PURE__ */ (0, import_jsx_runtime26.jsx)(import_ui7.Skeleton, { className: "h-4 w-full" }),
1768
+ /* @__PURE__ */ (0, import_jsx_runtime26.jsx)(import_ui7.Skeleton, { className: "h-4 w-3/4" })
1769
+ ] })
1770
+ };
1771
+ const selectGenre = (nodeId) => onSelectGenre(import_genre_tree19.GenreUuidSchema.parse(nodeId));
1772
+ const family = /* @__PURE__ */ (0, import_jsx_runtime26.jsx)(
1773
+ import_genre_tree_view4.GenreNodeFamily,
1774
+ {
1775
+ node: view.node,
1776
+ fill: view.fill,
1777
+ textColor: import_genre_tree_view4.ACCENT_TEXT_COLOR,
1778
+ parentNode: view.parentNode,
1779
+ childNodes: view.childNodes,
1780
+ onSelectNode: selectGenre
1781
+ }
1782
+ );
1783
+ const ancestors = /* @__PURE__ */ (0, import_jsx_runtime26.jsx)(import_genre_tree_view4.GenreNodeAncestors, { ancestorNodes: view.ancestorNodes, onSelectNode: selectGenre });
1784
+ return {
1785
+ title: detail.name,
1786
+ headerAccent: { background: view.fill, color: import_genre_tree_view4.ACCENT_TEXT_COLOR },
1787
+ aside: expanded ? ancestors : void 0,
1788
+ content: /* @__PURE__ */ (0, import_jsx_runtime26.jsxs)(import_jsx_runtime26.Fragment, { children: [
1789
+ /* @__PURE__ */ (0, import_jsx_runtime26.jsx)(
1790
+ GenreInfoPanelBody,
1791
+ {
1792
+ family: /* @__PURE__ */ (0, import_jsx_runtime26.jsxs)(import_jsx_runtime26.Fragment, { children: [
1793
+ family,
1794
+ !expanded && ancestors
1795
+ ] }),
1796
+ genreUuid: detail.uuid,
1797
+ genrePlaylist: hostPlaylist ?? (detail.criteriaPlaylist === null ? null : scope === "reference" ? { ...import_genre_tree19.GenrePlaylistSummarySchema.parse(detail.criteriaPlaylist), criteria: import_genre_tree19.CriteriaMinimumSchema.parse(detail) } : { ...detail.criteriaPlaylist, criteria: import_genre_tree19.CriteriaMinimumSchema.parse(detail), tracksCount: detail.tracksCount }),
1798
+ catalogue: detail.catalogue,
1799
+ countsContext: !hostPlaylist && extras?.artist ? "all artists" : void 0,
1800
+ wikidataId: detail.wikidataId,
1801
+ scope,
1802
+ getBackendBaseUrl,
1803
+ playback,
1804
+ band: { node: view.node, ...band, shareTarget: band?.shareTarget?.kind === "genre" ? { ...band.shareTarget, name: detail.name } : band?.shareTarget },
1805
+ extras
1806
+ }
1807
+ ),
1808
+ trailing
1809
+ ] })
1810
+ };
1811
+ }
1812
+ function GenreDetailPanel({
1813
+ genreUuid,
1814
+ scope,
1815
+ getBackendBaseUrl,
1816
+ onClose,
1817
+ onSelectGenre,
1818
+ onZoomIn,
1819
+ shareUrl,
1820
+ shareTarget,
1821
+ collapsed,
1822
+ expanded,
1823
+ onExpandedChange,
1824
+ edgeTab,
1825
+ extras,
1826
+ headerClassName
1827
+ }) {
1828
+ const panel = useGenreDetailContent({
1829
+ genreUuid,
1830
+ scope,
1831
+ getBackendBaseUrl,
1832
+ onSelectGenre,
1833
+ extras,
1834
+ expanded,
1835
+ band: { onZoomIn, shareUrl, shareTarget: shareUrl ? shareTarget : void 0 }
1836
+ });
1837
+ return /* @__PURE__ */ (0, import_jsx_runtime26.jsx)(
1838
+ import_ui7.DetailPanel,
1839
+ {
1840
+ side: "left",
1841
+ title: panel.title,
1842
+ headerAccent: panel.headerAccent,
1843
+ onClose,
1844
+ collapsed,
1845
+ expanded,
1846
+ onExpandedChange,
1847
+ edgeTab,
1848
+ aside: panel.aside,
1849
+ headerClassName,
1850
+ headerActions: onExpandedChange ? /* @__PURE__ */ (0, import_jsx_runtime26.jsx)(import_ui7.IconButton, { "aria-label": expanded ? "Shrink panel" : "Extend panel", onClick: () => onExpandedChange(!expanded), children: expanded ? /* @__PURE__ */ (0, import_jsx_runtime26.jsx)(import_lucide_react8.Minimize2, { "aria-hidden": "true", className: "h-4 w-4" }) : /* @__PURE__ */ (0, import_jsx_runtime26.jsx)(import_lucide_react8.Maximize2, { "aria-hidden": "true", className: "h-4 w-4" }) }) : void 0,
1851
+ children: panel.content
1852
+ }
1853
+ );
1854
+ }
1855
+
1646
1856
  // src/genre-tree/ChangeParentPopup.tsx
1647
- var import_react13 = require("react");
1648
- var import_genre_tree19 = require("@behindthemusictree/app-kit/genre-tree");
1649
- var import_jsx_runtime25 = require("react/jsx-runtime");
1857
+ var import_react15 = require("react");
1858
+ var import_genre_tree20 = require("@behindthemusictree/app-kit/genre-tree");
1859
+ var import_jsx_runtime27 = require("react/jsx-runtime");
1650
1860
  function ChangeParentPopup({
1651
1861
  genrePlaylist,
1652
1862
  genrePlaylists,
@@ -1655,9 +1865,9 @@ function ChangeParentPopup({
1655
1865
  onClose,
1656
1866
  onParentChanged
1657
1867
  }) {
1658
- const { mutate, isPending, formErrors } = (0, import_genre_tree19.useUpdateGenre)(scope, getBackendBaseUrl);
1659
- const candidates = (0, import_react13.useMemo)(
1660
- () => (0, import_genre_tree19.getReparentCandidates)(genrePlaylists, genrePlaylist.uuid),
1868
+ const { mutate, isPending, formErrors } = (0, import_genre_tree20.useUpdateGenre)(scope, getBackendBaseUrl);
1869
+ const candidates = (0, import_react15.useMemo)(
1870
+ () => (0, import_genre_tree20.getReparentCandidates)(genrePlaylists, genrePlaylist.uuid),
1661
1871
  [genrePlaylists, genrePlaylist.uuid]
1662
1872
  );
1663
1873
  const handleSelect = (newParent) => {
@@ -1674,19 +1884,19 @@ function ChangeParentPopup({
1674
1884
  }
1675
1885
  );
1676
1886
  };
1677
- (0, import_react13.useEffect)(() => {
1887
+ (0, import_react15.useEffect)(() => {
1678
1888
  const handleKeyDown = (event) => {
1679
1889
  if (event.key === "Escape") onClose();
1680
1890
  };
1681
1891
  window.addEventListener("keydown", handleKeyDown);
1682
1892
  return () => window.removeEventListener("keydown", handleKeyDown);
1683
1893
  }, [onClose]);
1684
- return /* @__PURE__ */ (0, import_jsx_runtime25.jsxs)(BasePopup_default, { title: `Change parent of ${genrePlaylist.name}`, onClose, showCancelButton: true, fitContent: true, children: [
1685
- /* @__PURE__ */ (0, import_jsx_runtime25.jsxs)("p", { className: "mb-3 text-sm text-[var(--color-text-muted)]", children: [
1894
+ return /* @__PURE__ */ (0, import_jsx_runtime27.jsxs)(BasePopup_default, { title: `Change parent of ${genrePlaylist.name}`, onClose, showCancelButton: true, fitContent: true, children: [
1895
+ /* @__PURE__ */ (0, import_jsx_runtime27.jsxs)("p", { className: "mb-3 text-sm text-[var(--color-text-muted)]", children: [
1686
1896
  "Current parent: ",
1687
1897
  genrePlaylist.parent?.name ?? "none"
1688
1898
  ] }),
1689
- /* @__PURE__ */ (0, import_jsx_runtime25.jsx)("div", { "aria-busy": isPending, className: isPending ? "pointer-events-none opacity-60" : void 0, children: /* @__PURE__ */ (0, import_jsx_runtime25.jsx)(
1899
+ /* @__PURE__ */ (0, import_jsx_runtime27.jsx)("div", { "aria-busy": isPending, className: isPending ? "pointer-events-none opacity-60" : void 0, children: /* @__PURE__ */ (0, import_jsx_runtime27.jsx)(
1690
1900
  GenreSearch,
1691
1901
  {
1692
1902
  genrePlaylists: candidates,
@@ -1694,24 +1904,24 @@ function ChangeParentPopup({
1694
1904
  placeholder: "Search a new parent\u2026"
1695
1905
  }
1696
1906
  ) }),
1697
- formErrors.length > 0 && /* @__PURE__ */ (0, import_jsx_runtime25.jsx)("p", { role: "alert", className: "mt-3 text-sm text-[var(--color-red-500)]", children: formErrors.map(({ message }) => message).join(" ") })
1907
+ formErrors.length > 0 && /* @__PURE__ */ (0, import_jsx_runtime27.jsx)("p", { role: "alert", className: "mt-3 text-sm text-[var(--color-red-500)]", children: formErrors.map(({ message }) => message).join(" ") })
1698
1908
  ] });
1699
1909
  }
1700
1910
 
1701
1911
  // src/genre-tree/artist/useArtistPanel.tsx
1702
- var import_ui12 = require("@behindthemusictree/ui");
1703
- var import_genre_tree24 = require("@behindthemusictree/app-kit/genre-tree");
1912
+ var import_ui13 = require("@behindthemusictree/ui");
1913
+ var import_genre_tree25 = require("@behindthemusictree/app-kit/genre-tree");
1704
1914
 
1705
1915
  // src/genre-tree/artist/ArtistHeader.tsx
1706
- var import_ui8 = require("@behindthemusictree/ui");
1916
+ var import_ui9 = require("@behindthemusictree/ui");
1707
1917
 
1708
1918
  // src/genre-tree/artist/ArtistLinks.tsx
1709
- var import_lucide_react8 = require("lucide-react");
1919
+ var import_lucide_react9 = require("lucide-react");
1710
1920
  var import_si3 = require("react-icons/si");
1711
- var import_ui7 = require("@behindthemusictree/ui");
1712
- var import_jsx_runtime26 = require("react/jsx-runtime");
1921
+ var import_ui8 = require("@behindthemusictree/ui");
1922
+ var import_jsx_runtime28 = require("react/jsx-runtime");
1713
1923
  var LINK_KINDS = {
1714
- official: { label: "Official website", Icon: import_lucide_react8.Globe },
1924
+ official: { label: "Official website", Icon: import_lucide_react9.Globe },
1715
1925
  wikipedia: { label: "Wikipedia", Icon: import_si3.SiWikipedia },
1716
1926
  wikidata: { label: "Wikidata", Icon: import_si3.SiWikidata },
1717
1927
  spotify: { label: "Spotify", Icon: import_si3.SiSpotify },
@@ -1719,7 +1929,7 @@ var LINK_KINDS = {
1719
1929
  bandcamp: { label: "Bandcamp", Icon: import_si3.SiBandcamp },
1720
1930
  soundcloud: { label: "SoundCloud", Icon: import_si3.SiSoundcloud },
1721
1931
  discogs: { label: "Discogs", Icon: import_si3.SiDiscogs },
1722
- social: { label: "Social", Icon: import_lucide_react8.Share2 }
1932
+ social: { label: "Social", Icon: import_lucide_react9.Share2 }
1723
1933
  };
1724
1934
  function socialLabel(url) {
1725
1935
  try {
@@ -1746,7 +1956,7 @@ function artistLinks(artist) {
1746
1956
  return links;
1747
1957
  }
1748
1958
  function ArtistLinks({ links }) {
1749
- return /* @__PURE__ */ (0, import_jsx_runtime26.jsx)("ul", { className: "flex flex-wrap gap-1.5", children: links.map(({ key, url, Icon, label }) => /* @__PURE__ */ (0, import_jsx_runtime26.jsx)("li", { children: /* @__PURE__ */ (0, import_jsx_runtime26.jsx)(import_ui7.Chip, { href: url, icon: /* @__PURE__ */ (0, import_jsx_runtime26.jsx)(Icon, { className: "h-3.5 w-3.5 shrink-0", "aria-hidden": "true" }), children: label }) }, key)) });
1959
+ return /* @__PURE__ */ (0, import_jsx_runtime28.jsx)("ul", { className: "flex flex-wrap gap-1.5", children: links.map(({ key, url, Icon, label }) => /* @__PURE__ */ (0, import_jsx_runtime28.jsx)("li", { children: /* @__PURE__ */ (0, import_jsx_runtime28.jsx)(import_ui8.Chip, { href: url, icon: /* @__PURE__ */ (0, import_jsx_runtime28.jsx)(Icon, { className: "h-3.5 w-3.5 shrink-0", "aria-hidden": "true" }), children: label }) }, key)) });
1750
1960
  }
1751
1961
 
1752
1962
  // src/genre-tree/artist/formatYears.ts
@@ -1757,7 +1967,7 @@ function formatYears(beginYear, endYear, ended) {
1757
1967
  }
1758
1968
 
1759
1969
  // src/genre-tree/artist/ArtistHeader.tsx
1760
- var import_jsx_runtime27 = require("react/jsx-runtime");
1970
+ var import_jsx_runtime29 = require("react/jsx-runtime");
1761
1971
  var countryNames = new Intl.DisplayNames(["en"], { type: "region" });
1762
1972
  function ArtistHeader({ artist }) {
1763
1973
  const fields = [];
@@ -1767,35 +1977,35 @@ function ArtistHeader({ artist }) {
1767
1977
  const years = formatYears(artist.beginYear, artist.endYear, artist.ended);
1768
1978
  if (years) fields.push({ label: "Active", value: years });
1769
1979
  const links = artistLinks(artist);
1770
- if (links.length > 0) fields.push({ label: "Links", value: /* @__PURE__ */ (0, import_jsx_runtime27.jsx)(ArtistLinks, { links }) });
1980
+ if (links.length > 0) fields.push({ label: "Links", value: /* @__PURE__ */ (0, import_jsx_runtime29.jsx)(ArtistLinks, { links }) });
1771
1981
  if (fields.length === 0) return null;
1772
- return /* @__PURE__ */ (0, import_jsx_runtime27.jsx)(import_ui8.FieldList, { items: fields });
1982
+ return /* @__PURE__ */ (0, import_jsx_runtime29.jsx)(import_ui9.FieldList, { items: fields });
1773
1983
  }
1774
1984
 
1775
1985
  // src/genre-tree/artist/ArtistPhoto.tsx
1776
- var import_jsx_runtime28 = require("react/jsx-runtime");
1986
+ var import_jsx_runtime30 = require("react/jsx-runtime");
1777
1987
  function ExternalLink({ href, children }) {
1778
- return /* @__PURE__ */ (0, import_jsx_runtime28.jsx)("a", { href, target: "_blank", rel: "noopener noreferrer", className: "underline-offset-2 hover:underline", children });
1988
+ return /* @__PURE__ */ (0, import_jsx_runtime30.jsx)("a", { href, target: "_blank", rel: "noopener noreferrer", className: "underline-offset-2 hover:underline", children });
1779
1989
  }
1780
1990
  function ArtistPhoto({ image, name }) {
1781
- return /* @__PURE__ */ (0, import_jsx_runtime28.jsxs)("figure", { className: "flex flex-col gap-1.5 px-6 py-2.5", children: [
1782
- /* @__PURE__ */ (0, import_jsx_runtime28.jsx)("img", { src: image.url, alt: name, className: "h-64 w-full object-contain bg-[var(--color-surface-muted)] rounded" }),
1783
- /* @__PURE__ */ (0, import_jsx_runtime28.jsxs)("figcaption", { className: "text-xs text-[var(--color-text-muted)]", children: [
1991
+ return /* @__PURE__ */ (0, import_jsx_runtime30.jsxs)("figure", { className: "flex flex-col gap-1.5 px-6 py-2.5", children: [
1992
+ /* @__PURE__ */ (0, import_jsx_runtime30.jsx)("img", { src: image.url, alt: name, className: "h-64 w-full object-contain bg-[var(--color-surface-muted)] rounded" }),
1993
+ /* @__PURE__ */ (0, import_jsx_runtime30.jsxs)("figcaption", { className: "text-xs text-[var(--color-text-muted)]", children: [
1784
1994
  "Photo: ",
1785
- /* @__PURE__ */ (0, import_jsx_runtime28.jsx)(ExternalLink, { href: image.pageUrl, children: image.author ?? "Wikimedia Commons" }),
1995
+ /* @__PURE__ */ (0, import_jsx_runtime30.jsx)(ExternalLink, { href: image.pageUrl, children: image.author ?? "Wikimedia Commons" }),
1786
1996
  ",",
1787
1997
  " ",
1788
- image.licenseUrl ? /* @__PURE__ */ (0, import_jsx_runtime28.jsx)(ExternalLink, { href: image.licenseUrl, children: image.license }) : image.license
1998
+ image.licenseUrl ? /* @__PURE__ */ (0, import_jsx_runtime30.jsx)(ExternalLink, { href: image.licenseUrl, children: image.license }) : image.license
1789
1999
  ] })
1790
2000
  ] });
1791
2001
  }
1792
2002
 
1793
2003
  // src/genre-tree/artist/ArtistMembers.tsx
1794
- var import_ui9 = require("@behindthemusictree/ui");
1795
- var import_jsx_runtime29 = require("react/jsx-runtime");
2004
+ var import_ui10 = require("@behindthemusictree/ui");
2005
+ var import_jsx_runtime31 = require("react/jsx-runtime");
1796
2006
  function MembershipList({ title, memberships, renderArtistLink }) {
1797
2007
  if (memberships.length === 0) return null;
1798
- return /* @__PURE__ */ (0, import_jsx_runtime29.jsx)(import_ui9.PanelGroup, { title, children: /* @__PURE__ */ (0, import_jsx_runtime29.jsx)("ul", { children: memberships.map((membership, index) => {
2008
+ return /* @__PURE__ */ (0, import_jsx_runtime31.jsx)(import_ui10.PanelGroup, { title, children: /* @__PURE__ */ (0, import_jsx_runtime31.jsx)("ul", { children: memberships.map((membership, index) => {
1799
2009
  const details = [
1800
2010
  formatYears(membership.beginYear, membership.endYear, membership.ended),
1801
2011
  ...membership.roles
@@ -1803,9 +2013,9 @@ function MembershipList({ title, memberships, renderArtistLink }) {
1803
2013
  const detailsText = details.filter(Boolean).join(" \xB7 ");
1804
2014
  return (
1805
2015
  // A member can have several periods in the same group.
1806
- /* @__PURE__ */ (0, import_jsx_runtime29.jsxs)("li", { className: "truncate py-0.5", title: detailsText || void 0, children: [
1807
- /* @__PURE__ */ (0, import_jsx_runtime29.jsx)("span", { className: "font-medium text-[var(--color-text)]", children: renderArtistLink(membership.artist) }),
1808
- detailsText && /* @__PURE__ */ (0, import_jsx_runtime29.jsxs)("span", { className: "text-[var(--color-text-muted)]", children: [
2016
+ /* @__PURE__ */ (0, import_jsx_runtime31.jsxs)("li", { className: "truncate py-0.5", title: detailsText || void 0, children: [
2017
+ /* @__PURE__ */ (0, import_jsx_runtime31.jsx)("span", { className: "font-medium text-[var(--color-text)]", children: renderArtistLink(membership.artist) }),
2018
+ detailsText && /* @__PURE__ */ (0, import_jsx_runtime31.jsxs)("span", { className: "text-[var(--color-text-muted)]", children: [
1809
2019
  " \u2014 ",
1810
2020
  detailsText
1811
2021
  ] })
@@ -1816,9 +2026,9 @@ function MembershipList({ title, memberships, renderArtistLink }) {
1816
2026
  function ArtistMembers({ title, memberships, renderArtistLink }) {
1817
2027
  if (memberships.length === 0) return null;
1818
2028
  const isCurrent = (membership) => !membership.ended && membership.endYear === null;
1819
- return /* @__PURE__ */ (0, import_jsx_runtime29.jsxs)(import_ui9.PanelSection, { title, children: [
1820
- /* @__PURE__ */ (0, import_jsx_runtime29.jsx)(MembershipList, { title: "Current", memberships: memberships.filter(isCurrent), renderArtistLink }),
1821
- /* @__PURE__ */ (0, import_jsx_runtime29.jsx)(
2029
+ return /* @__PURE__ */ (0, import_jsx_runtime31.jsxs)(import_ui10.PanelSection, { title, children: [
2030
+ /* @__PURE__ */ (0, import_jsx_runtime31.jsx)(MembershipList, { title: "Current", memberships: memberships.filter(isCurrent), renderArtistLink }),
2031
+ /* @__PURE__ */ (0, import_jsx_runtime31.jsx)(
1822
2032
  MembershipList,
1823
2033
  {
1824
2034
  title: "Past",
@@ -1830,8 +2040,8 @@ function ArtistMembers({ title, memberships, renderArtistLink }) {
1830
2040
  }
1831
2041
 
1832
2042
  // src/genre-tree/artist/ArtistDiscography.tsx
1833
- var import_ui10 = require("@behindthemusictree/ui");
1834
- var import_jsx_runtime30 = require("react/jsx-runtime");
2043
+ var import_ui11 = require("@behindthemusictree/ui");
2044
+ var import_jsx_runtime32 = require("react/jsx-runtime");
1835
2045
  var GROUPS = ["Albums", "EPs", "Singles", "Live", "Compilations", "Other"];
1836
2046
  var PRIMARY_GROUPS = { album: "Albums", ep: "EPs", single: "Singles" };
1837
2047
  function discographyGroup({ primaryType, secondaryTypes }) {
@@ -1851,34 +2061,34 @@ function byYear(a, b) {
1851
2061
  function ArtistDiscography({ albums }) {
1852
2062
  if (albums.length === 0) return null;
1853
2063
  const sorted = [...albums].sort(byYear);
1854
- return /* @__PURE__ */ (0, import_jsx_runtime30.jsx)(import_ui10.PanelSection, { title: "Discography", children: GROUPS.map((group) => {
2064
+ return /* @__PURE__ */ (0, import_jsx_runtime32.jsx)(import_ui11.PanelSection, { title: "Discography", children: GROUPS.map((group) => {
1855
2065
  const groupAlbums = sorted.filter((album) => discographyGroup(album) === group);
1856
2066
  if (groupAlbums.length === 0) return null;
1857
- return /* @__PURE__ */ (0, import_jsx_runtime30.jsx)(import_ui10.PanelGroup, { title: group, children: /* @__PURE__ */ (0, import_jsx_runtime30.jsx)("ul", { children: groupAlbums.map((album) => /* @__PURE__ */ (0, import_jsx_runtime30.jsxs)("li", { className: "grid grid-cols-[2.5rem_minmax(0,1fr)] gap-2 py-0.5", children: [
1858
- /* @__PURE__ */ (0, import_jsx_runtime30.jsx)("span", { className: "tabular-nums text-[var(--color-text-muted)]", children: album.year ?? "\u2014" }),
1859
- /* @__PURE__ */ (0, import_jsx_runtime30.jsx)("span", { className: "truncate text-[var(--color-text)]", title: album.name, children: album.name })
2067
+ return /* @__PURE__ */ (0, import_jsx_runtime32.jsx)(import_ui11.PanelGroup, { title: group, children: /* @__PURE__ */ (0, import_jsx_runtime32.jsx)("ul", { children: groupAlbums.map((album) => /* @__PURE__ */ (0, import_jsx_runtime32.jsxs)("li", { className: "grid grid-cols-[2.5rem_minmax(0,1fr)] gap-2 py-0.5", children: [
2068
+ /* @__PURE__ */ (0, import_jsx_runtime32.jsx)("span", { className: "tabular-nums text-[var(--color-text-muted)]", children: album.year ?? "\u2014" }),
2069
+ /* @__PURE__ */ (0, import_jsx_runtime32.jsx)("span", { className: "truncate text-[var(--color-text)]", title: album.name, children: album.name })
1860
2070
  ] }, album.uuid)) }) }, group);
1861
2071
  }) });
1862
2072
  }
1863
2073
 
1864
2074
  // src/genre-tree/artist/ArtistSongs.tsx
1865
- var import_react14 = require("react");
2075
+ var import_react16 = require("react");
1866
2076
  var import_react_query2 = require("@tanstack/react-query");
1867
- var import_lucide_react9 = require("lucide-react");
1868
- var import_ui11 = require("@behindthemusictree/ui");
2077
+ var import_lucide_react10 = require("lucide-react");
2078
+ var import_ui12 = require("@behindthemusictree/ui");
1869
2079
  var import_player10 = require("@behindthemusictree/app-kit/player");
1870
- var import_genre_tree23 = require("@behindthemusictree/app-kit/genre-tree");
2080
+ var import_genre_tree24 = require("@behindthemusictree/app-kit/genre-tree");
1871
2081
  var import_transport7 = require("@behindthemusictree/app-kit/transport");
1872
2082
 
1873
2083
  // src/genre-tree/track-list-sidebar/TrackItem.tsx
1874
2084
  var import_player9 = require("@behindthemusictree/app-kit/player");
1875
- var import_genre_tree20 = require("@behindthemusictree/app-kit/genre-tree");
2085
+ var import_genre_tree21 = require("@behindthemusictree/app-kit/genre-tree");
1876
2086
 
1877
2087
  // src/genre-tree/TrackPositionPlayPause.tsx
1878
2088
  var import_fa3 = require("react-icons/fa");
1879
2089
  var import_player7 = require("@behindthemusictree/app-kit/player");
1880
2090
  var import_player8 = require("@behindthemusictree/app-kit/player");
1881
- var import_jsx_runtime31 = require("react/jsx-runtime");
2091
+ var import_jsx_runtime33 = require("react/jsx-runtime");
1882
2092
  function TrackPositionPlayPause({
1883
2093
  position,
1884
2094
  uuid,
@@ -1892,14 +2102,14 @@ function TrackPositionPlayPause({
1892
2102
  const isPlaying = isThisTrack && isConfirmedPlayable && playState === import_player8.PlayStates.PLAYING;
1893
2103
  const isPaused = isThisTrack && isConfirmedPlayable && playState === import_player8.PlayStates.PAUSED;
1894
2104
  const isCurrentTrackActive = isPlaying || isPaused;
1895
- return /* @__PURE__ */ (0, import_jsx_runtime31.jsxs)(
2105
+ return /* @__PURE__ */ (0, import_jsx_runtime33.jsxs)(
1896
2106
  "div",
1897
2107
  {
1898
2108
  className: "track-position-play-pause relative flex items-center justify-center gap-2 text-lg w-16 cursor-pointer",
1899
2109
  style: { minWidth: "4rem", maxWidth: "4rem" },
1900
2110
  onClick: handlePlayPauseClick,
1901
2111
  children: [
1902
- /* @__PURE__ */ (0, import_jsx_runtime31.jsx)("div", { className: "track-position-play-pause__status group-hover:hidden flex items-center justify-center", children: isAwaitingPlayback ? /* @__PURE__ */ (0, import_jsx_runtime31.jsx)("span", { className: "track-position-play-pause__spinner", role: "status", "aria-label": "Loading track" }) : isCurrentTrackActive ? /* @__PURE__ */ (0, import_jsx_runtime31.jsxs)(
2112
+ /* @__PURE__ */ (0, import_jsx_runtime33.jsx)("div", { className: "track-position-play-pause__status group-hover:hidden flex items-center justify-center", children: isAwaitingPlayback ? /* @__PURE__ */ (0, import_jsx_runtime33.jsx)("span", { className: "track-position-play-pause__spinner", role: "status", "aria-label": "Loading track" }) : isCurrentTrackActive ? /* @__PURE__ */ (0, import_jsx_runtime33.jsxs)(
1903
2113
  "span",
1904
2114
  {
1905
2115
  className: "track-position-play-pause__bars",
@@ -1907,18 +2117,18 @@ function TrackPositionPlayPause({
1907
2117
  role: "img",
1908
2118
  "aria-label": isPlaying ? "Playing" : "Paused",
1909
2119
  children: [
1910
- /* @__PURE__ */ (0, import_jsx_runtime31.jsx)("span", { className: "track-position-play-pause__bar" }),
1911
- /* @__PURE__ */ (0, import_jsx_runtime31.jsx)("span", { className: "track-position-play-pause__bar" }),
1912
- /* @__PURE__ */ (0, import_jsx_runtime31.jsx)("span", { className: "track-position-play-pause__bar" })
2120
+ /* @__PURE__ */ (0, import_jsx_runtime33.jsx)("span", { className: "track-position-play-pause__bar" }),
2121
+ /* @__PURE__ */ (0, import_jsx_runtime33.jsx)("span", { className: "track-position-play-pause__bar" }),
2122
+ /* @__PURE__ */ (0, import_jsx_runtime33.jsx)("span", { className: "track-position-play-pause__bar" })
1913
2123
  ]
1914
2124
  }
1915
2125
  ) : position }),
1916
- /* @__PURE__ */ (0, import_jsx_runtime31.jsx)(
2126
+ /* @__PURE__ */ (0, import_jsx_runtime33.jsx)(
1917
2127
  "div",
1918
2128
  {
1919
2129
  className: `hidden group-hover:flex items-center justify-center ${isCurrentTrackActive ? "" : "absolute inset-0"}`,
1920
2130
  "aria-hidden": "true",
1921
- children: isAwaitingPlayback ? /* @__PURE__ */ (0, import_jsx_runtime31.jsx)("span", { className: "track-position-play-pause__spinner" }) : isPlaying ? /* @__PURE__ */ (0, import_jsx_runtime31.jsx)(import_fa3.FaPause, {}) : /* @__PURE__ */ (0, import_jsx_runtime31.jsx)(import_fa3.FaPlay, {})
2131
+ children: isAwaitingPlayback ? /* @__PURE__ */ (0, import_jsx_runtime33.jsx)("span", { className: "track-position-play-pause__spinner" }) : isPlaying ? /* @__PURE__ */ (0, import_jsx_runtime33.jsx)(import_fa3.FaPause, {}) : /* @__PURE__ */ (0, import_jsx_runtime33.jsx)(import_fa3.FaPlay, {})
1922
2132
  }
1923
2133
  )
1924
2134
  ]
@@ -1927,9 +2137,9 @@ function TrackPositionPlayPause({
1927
2137
  }
1928
2138
 
1929
2139
  // src/genre-tree/track-list-sidebar/TrackItem.tsx
1930
- var import_genre_tree21 = require("@behindthemusictree/app-kit/genre-tree");
1931
2140
  var import_genre_tree22 = require("@behindthemusictree/app-kit/genre-tree");
1932
- var import_jsx_runtime32 = require("react/jsx-runtime");
2141
+ var import_genre_tree23 = require("@behindthemusictree/app-kit/genre-tree");
2142
+ var import_jsx_runtime34 = require("react/jsx-runtime");
1933
2143
  function TrackItem({
1934
2144
  track,
1935
2145
  position,
@@ -1939,11 +2149,11 @@ function TrackItem({
1939
2149
  renderSongLink
1940
2150
  }) {
1941
2151
  const { handlePlayPauseAction, playerTrackObject, loadTrackForPlayer } = (0, import_player9.usePlayer)();
1942
- const { trackList, selectedTrack, toTrackAtPosition, runtimeUnplayableTrackIds } = (0, import_genre_tree20.useTrackList)();
2152
+ const { trackList, selectedTrack, toTrackAtPosition, runtimeUnplayableTrackIds } = (0, import_genre_tree21.useTrackList)();
1943
2153
  const scope = trackList?.origin?.scope ?? null;
1944
2154
  const isCurrentTrack = playerTrackObject?.track.id === track.uuid;
1945
2155
  const hasPlaybackError = isCurrentTrack && !!playerTrackObject?.loadError;
1946
- const isUnplayable = hasPlaybackError || runtimeUnplayableTrackIds?.has(track.uuid) === true || (0, import_genre_tree21.isSong)(track) && (0, import_genre_tree21.songUnplayableReason)(track) !== null;
2156
+ const isUnplayable = hasPlaybackError || runtimeUnplayableTrackIds?.has(track.uuid) === true || (0, import_genre_tree22.isSong)(track) && (0, import_genre_tree22.songUnplayableReason)(track) !== null;
1947
2157
  const handlePlayPauseClick = (event) => {
1948
2158
  event.stopPropagation();
1949
2159
  if (isCurrentTrack) {
@@ -1953,12 +2163,12 @@ function TrackItem({
1953
2163
  loadTrackForPlayer(track.uuid);
1954
2164
  }
1955
2165
  };
1956
- return /* @__PURE__ */ (0, import_jsx_runtime32.jsxs)(
2166
+ return /* @__PURE__ */ (0, import_jsx_runtime34.jsxs)(
1957
2167
  "div",
1958
2168
  {
1959
2169
  className: `track-item flex h-14 text-[var(--color-text)] ${isUnplayable ? "opacity-50" : "hover:bg-[var(--color-surface-muted)] group"}`,
1960
2170
  children: [
1961
- isUnplayable ? /* @__PURE__ */ (0, import_jsx_runtime32.jsx)(
2171
+ isUnplayable ? /* @__PURE__ */ (0, import_jsx_runtime34.jsx)(
1962
2172
  "div",
1963
2173
  {
1964
2174
  className: "flex items-center justify-center text-lg w-16",
@@ -1966,7 +2176,7 @@ function TrackItem({
1966
2176
  "aria-disabled": "true",
1967
2177
  children: position
1968
2178
  }
1969
- ) : /* @__PURE__ */ (0, import_jsx_runtime32.jsx)(
2179
+ ) : /* @__PURE__ */ (0, import_jsx_runtime34.jsx)(
1970
2180
  TrackPositionPlayPause,
1971
2181
  {
1972
2182
  position,
@@ -1975,12 +2185,12 @@ function TrackItem({
1975
2185
  isSelected: selectedTrack?.uuid === track.uuid
1976
2186
  }
1977
2187
  ),
1978
- /* @__PURE__ */ (0, import_jsx_runtime32.jsxs)("div", { className: "title-artist-container flex flex-col items-start justify-center min-w-0 flex-1", children: [
1979
- /* @__PURE__ */ (0, import_jsx_runtime32.jsx)("div", { className: "flex max-w-full items-center gap-2", children: /* @__PURE__ */ (0, import_jsx_runtime32.jsx)("div", { className: "title text-lg font-bold text-[var(--color-text)] max-w-full truncate", children: renderSongLink ? renderSongLink(track) : (0, import_genre_tree21.isSong)(track) ? /* @__PURE__ */ (0, import_jsx_runtime32.jsx)(MusicBrainzRecordingLink, { song: track, className: "hover:underline focus-visible:underline", children: track.title }) : track.title }) }),
1980
- track.artists && track.artists.length > 0 ? /* @__PURE__ */ (0, import_jsx_runtime32.jsx)("div", { className: "artist text-base max-w-full truncate text-[var(--color-text-muted)]", children: /* @__PURE__ */ (0, import_jsx_runtime32.jsx)(import_genre_tree22.ArtistNames, { artists: track.artists, renderArtistLink }) }) : ""
2188
+ /* @__PURE__ */ (0, import_jsx_runtime34.jsxs)("div", { className: "title-artist-container flex flex-col items-start justify-center min-w-0 flex-1", children: [
2189
+ /* @__PURE__ */ (0, import_jsx_runtime34.jsx)("div", { className: "flex max-w-full items-center gap-2", children: /* @__PURE__ */ (0, import_jsx_runtime34.jsx)("div", { className: "title text-lg font-bold text-[var(--color-text)] max-w-full truncate", children: renderSongLink ? renderSongLink(track) : (0, import_genre_tree22.isSong)(track) ? /* @__PURE__ */ (0, import_jsx_runtime34.jsx)(MusicBrainzRecordingLink, { song: track, className: "hover:underline focus-visible:underline", children: track.title }) : track.title }) }),
2190
+ track.artists && track.artists.length > 0 ? /* @__PURE__ */ (0, import_jsx_runtime34.jsx)("div", { className: "artist text-base max-w-full truncate text-[var(--color-text-muted)]", children: /* @__PURE__ */ (0, import_jsx_runtime34.jsx)(import_genre_tree23.ArtistNames, { artists: track.artists, renderArtistLink }) }) : ""
1981
2191
  ] }),
1982
- /* @__PURE__ */ (0, import_jsx_runtime32.jsx)("div", { className: "genre-name-container flex items-center justify-start w-[21.6%] min-w-0 shrink-0", children: track.genre ? /* @__PURE__ */ (0, import_jsx_runtime32.jsx)("div", { className: "genre-name max-w-full whitespace-normal break-words p-1 text-xs font-bold", children: track.genre.name }) : "" }),
1983
- renderDuration && /* @__PURE__ */ (0, import_jsx_runtime32.jsx)(
2192
+ /* @__PURE__ */ (0, import_jsx_runtime34.jsx)("div", { className: "genre-name-container flex items-center justify-start w-[21.6%] min-w-0 shrink-0", children: track.genre ? /* @__PURE__ */ (0, import_jsx_runtime34.jsx)("div", { className: "genre-name max-w-full whitespace-normal break-words p-1 text-xs font-bold", children: track.genre.name }) : "" }),
2193
+ renderDuration && /* @__PURE__ */ (0, import_jsx_runtime34.jsx)(
1984
2194
  "div",
1985
2195
  {
1986
2196
  className: "duration flex text-base w-16 items-center justify-center",
@@ -1988,28 +2198,28 @@ function TrackItem({
1988
2198
  children: renderDuration(track)
1989
2199
  }
1990
2200
  ),
1991
- renderActions && /* @__PURE__ */ (0, import_jsx_runtime32.jsx)("div", { className: "edit flex text-base w-6 items-center justify-center mr-2 cursor-pointer", children: renderActions(track) })
2201
+ renderActions && /* @__PURE__ */ (0, import_jsx_runtime34.jsx)("div", { className: "edit flex text-base w-6 items-center justify-center mr-2 cursor-pointer", children: renderActions(track) })
1992
2202
  ]
1993
2203
  }
1994
2204
  );
1995
2205
  }
1996
2206
 
1997
2207
  // src/genre-tree/artist/ArtistSongs.tsx
1998
- var import_jsx_runtime33 = require("react/jsx-runtime");
1999
- var SongPageSchema2 = (0, import_transport7.PaginatedResponseSchema)(import_genre_tree23.SongDetailedSchema);
2208
+ var import_jsx_runtime35 = require("react/jsx-runtime");
2209
+ var SongPageSchema2 = (0, import_transport7.PaginatedResponseSchema)(import_genre_tree24.SongDetailedSchema);
2000
2210
  var SORTS = [
2001
2211
  { ordering: "title", label: "Title" },
2002
2212
  { ordering: "-albumYear", label: "Year" }
2003
2213
  ];
2004
2214
  function ArtistSongs({ artist, getBackendBaseUrl, renderArtistLink, renderSongLink }) {
2005
- const [page, setPage] = (0, import_react14.useState)(1);
2006
- const [ordering, setOrdering] = (0, import_react14.useState)("title");
2215
+ const [page, setPage] = (0, import_react16.useState)(1);
2216
+ const [ordering, setOrdering] = (0, import_react16.useState)("title");
2007
2217
  const { fetch } = (0, import_transport7.useFetchWrapper)(getBackendBaseUrl);
2008
- const { playNewTrackListFromArtist } = (0, import_genre_tree23.useTrackList)();
2218
+ const { playNewTrackListFromArtist } = (0, import_genre_tree24.useTrackList)();
2009
2219
  const { playerTrackObject } = (0, import_player10.usePlayer)();
2010
2220
  const { data, isPending, isError } = (0, import_transport7.useQueryWithParse)({
2011
2221
  queryKey: ["referenceSongs", "artist", artist.uuid, ordering, page],
2012
- queryFn: () => fetch(import_genre_tree23.libraryEndpoints.reference.songs.list(), true, false, void 0, {
2222
+ queryFn: () => fetch(import_genre_tree24.libraryEndpoints.reference.songs.list(), true, false, void 0, {
2013
2223
  artist: artist.uuid,
2014
2224
  ordering,
2015
2225
  page
@@ -2024,25 +2234,25 @@ function ArtistSongs({ artist, getBackendBaseUrl, renderArtistLink, renderSongLi
2024
2234
  });
2025
2235
  const handleRowClickCapture = (event, song) => {
2026
2236
  if (!(event.target instanceof Element) || !event.target.closest(".track-position-play-pause")) return;
2027
- if (playerTrackObject?.track.id === song.uuid || (0, import_genre_tree23.songUnplayableReason)(song) !== null) return;
2237
+ if (playerTrackObject?.track.id === song.uuid || (0, import_genre_tree24.songUnplayableReason)(song) !== null) return;
2028
2238
  event.stopPropagation();
2029
2239
  void play(song.uuid);
2030
2240
  };
2031
- return /* @__PURE__ */ (0, import_jsx_runtime33.jsx)(import_ui11.PanelSection, { title: "Songs", children: isPending ? /* @__PURE__ */ (0, import_jsx_runtime33.jsx)(import_ui11.Skeleton, { className: "h-14", "aria-busy": "true" }) : isError ? /* @__PURE__ */ (0, import_jsx_runtime33.jsx)("p", { className: "text-red-600", children: "Could not load songs." }) : data.results.length === 0 ? /* @__PURE__ */ (0, import_jsx_runtime33.jsx)("p", { className: "text-[var(--color-text-muted)]", children: "No songs yet." }) : /* @__PURE__ */ (0, import_jsx_runtime33.jsxs)(import_jsx_runtime33.Fragment, { children: [
2032
- /* @__PURE__ */ (0, import_jsx_runtime33.jsxs)("div", { className: "flex flex-wrap items-center justify-between gap-2 mb-2", children: [
2033
- /* @__PURE__ */ (0, import_jsx_runtime33.jsxs)(
2241
+ return /* @__PURE__ */ (0, import_jsx_runtime35.jsx)(import_ui12.PanelSection, { title: "Songs", children: isPending ? /* @__PURE__ */ (0, import_jsx_runtime35.jsx)(import_ui12.Skeleton, { className: "h-14", "aria-busy": "true" }) : isError ? /* @__PURE__ */ (0, import_jsx_runtime35.jsx)("p", { className: "text-red-600", children: "Could not load songs." }) : data.results.length === 0 ? /* @__PURE__ */ (0, import_jsx_runtime35.jsx)("p", { className: "text-[var(--color-text-muted)]", children: "No songs yet." }) : /* @__PURE__ */ (0, import_jsx_runtime35.jsxs)(import_jsx_runtime35.Fragment, { children: [
2242
+ /* @__PURE__ */ (0, import_jsx_runtime35.jsxs)("div", { className: "flex flex-wrap items-center justify-between gap-2 mb-2", children: [
2243
+ /* @__PURE__ */ (0, import_jsx_runtime35.jsxs)(
2034
2244
  "button",
2035
2245
  {
2036
2246
  type: "button",
2037
2247
  onClick: () => void play(),
2038
2248
  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",
2039
2249
  children: [
2040
- /* @__PURE__ */ (0, import_jsx_runtime33.jsx)(import_lucide_react9.Play, { className: "h-3.5 w-3.5", "aria-hidden": "true" }),
2250
+ /* @__PURE__ */ (0, import_jsx_runtime35.jsx)(import_lucide_react10.Play, { className: "h-3.5 w-3.5", "aria-hidden": "true" }),
2041
2251
  "Play all"
2042
2252
  ]
2043
2253
  }
2044
2254
  ),
2045
- /* @__PURE__ */ (0, import_jsx_runtime33.jsx)("div", { role: "group", "aria-label": "Sort songs by", className: "flex gap-1 text-xs", children: SORTS.map((sort) => /* @__PURE__ */ (0, import_jsx_runtime33.jsx)(
2255
+ /* @__PURE__ */ (0, import_jsx_runtime35.jsx)("div", { role: "group", "aria-label": "Sort songs by", className: "flex gap-1 text-xs", children: SORTS.map((sort) => /* @__PURE__ */ (0, import_jsx_runtime35.jsx)(
2046
2256
  "button",
2047
2257
  {
2048
2258
  type: "button",
@@ -2057,7 +2267,7 @@ function ArtistSongs({ artist, getBackendBaseUrl, renderArtistLink, renderSongLi
2057
2267
  sort.ordering
2058
2268
  )) })
2059
2269
  ] }),
2060
- /* @__PURE__ */ (0, import_jsx_runtime33.jsx)("ul", { className: "overflow-hidden mb-2 bg-zinc-950 rounded-lg", children: data.results.map((song, index) => /* @__PURE__ */ (0, import_jsx_runtime33.jsx)("li", { onClickCapture: (event) => handleRowClickCapture(event, song), children: /* @__PURE__ */ (0, import_jsx_runtime33.jsx)(
2270
+ /* @__PURE__ */ (0, import_jsx_runtime35.jsx)("ul", { className: "overflow-hidden mb-2 bg-zinc-950 rounded-lg", children: data.results.map((song, index) => /* @__PURE__ */ (0, import_jsx_runtime35.jsx)("li", { onClickCapture: (event) => handleRowClickCapture(event, song), children: /* @__PURE__ */ (0, import_jsx_runtime35.jsx)(
2061
2271
  TrackItem,
2062
2272
  {
2063
2273
  track: song,
@@ -2066,12 +2276,12 @@ function ArtistSongs({ artist, getBackendBaseUrl, renderArtistLink, renderSongLi
2066
2276
  renderSongLink
2067
2277
  }
2068
2278
  ) }, song.uuid)) }),
2069
- /* @__PURE__ */ (0, import_jsx_runtime33.jsx)(import_ui11.Pagination, { currentPage: page, totalPages: data.totalPages, onPageChange: setPage })
2279
+ /* @__PURE__ */ (0, import_jsx_runtime35.jsx)(import_ui12.Pagination, { currentPage: page, totalPages: data.totalPages, onPageChange: setPage })
2070
2280
  ] }) });
2071
2281
  }
2072
2282
 
2073
2283
  // src/genre-tree/artist/useArtistPanel.tsx
2074
- var import_jsx_runtime34 = require("react/jsx-runtime");
2284
+ var import_jsx_runtime36 = require("react/jsx-runtime");
2075
2285
  function useArtistPanel({
2076
2286
  uuid,
2077
2287
  scope,
@@ -2083,7 +2293,7 @@ function useArtistPanel({
2083
2293
  onExpandedChange,
2084
2294
  onClose
2085
2295
  }) {
2086
- const result = (0, import_genre_tree24.useArtist)(uuid, getBackendBaseUrl);
2296
+ const result = (0, import_genre_tree25.useArtist)(uuid, getBackendBaseUrl);
2087
2297
  const panel = { expanded, onExpandedChange, onClose };
2088
2298
  switch (result.status) {
2089
2299
  case "idle":
@@ -2092,26 +2302,26 @@ function useArtistPanel({
2092
2302
  return {
2093
2303
  ...panel,
2094
2304
  title: "Artist",
2095
- content: /* @__PURE__ */ (0, import_jsx_runtime34.jsxs)("div", { "aria-busy": "true", className: "flex flex-col gap-2.5 px-6 py-2.5", children: [
2096
- /* @__PURE__ */ (0, import_jsx_runtime34.jsx)(import_ui12.Skeleton, { className: "h-3 w-16" }),
2097
- /* @__PURE__ */ (0, import_jsx_runtime34.jsx)(import_ui12.Skeleton, { className: "h-4 w-32" }),
2098
- /* @__PURE__ */ (0, import_jsx_runtime34.jsx)(import_ui12.Skeleton, { className: "h-3 w-16" }),
2099
- /* @__PURE__ */ (0, import_jsx_runtime34.jsx)(import_ui12.Skeleton, { className: "h-4 w-24" }),
2100
- /* @__PURE__ */ (0, import_jsx_runtime34.jsx)(import_ui12.Skeleton, { className: "h-16 mt-2.5" }),
2101
- /* @__PURE__ */ (0, import_jsx_runtime34.jsx)(import_ui12.Skeleton, { className: "h-16" })
2305
+ content: /* @__PURE__ */ (0, import_jsx_runtime36.jsxs)("div", { "aria-busy": "true", className: "flex flex-col gap-2.5 px-6 py-2.5", children: [
2306
+ /* @__PURE__ */ (0, import_jsx_runtime36.jsx)(import_ui13.Skeleton, { className: "h-3 w-16" }),
2307
+ /* @__PURE__ */ (0, import_jsx_runtime36.jsx)(import_ui13.Skeleton, { className: "h-4 w-32" }),
2308
+ /* @__PURE__ */ (0, import_jsx_runtime36.jsx)(import_ui13.Skeleton, { className: "h-3 w-16" }),
2309
+ /* @__PURE__ */ (0, import_jsx_runtime36.jsx)(import_ui13.Skeleton, { className: "h-4 w-24" }),
2310
+ /* @__PURE__ */ (0, import_jsx_runtime36.jsx)(import_ui13.Skeleton, { className: "h-16 mt-2.5" }),
2311
+ /* @__PURE__ */ (0, import_jsx_runtime36.jsx)(import_ui13.Skeleton, { className: "h-16" })
2102
2312
  ] })
2103
2313
  };
2104
2314
  case "not-found":
2105
2315
  return {
2106
2316
  ...panel,
2107
2317
  title: "Artist not found",
2108
- content: /* @__PURE__ */ (0, import_jsx_runtime34.jsx)("p", { className: "px-6 py-2.5 text-[var(--color-text-muted)]", children: "This artist does not exist." })
2318
+ content: /* @__PURE__ */ (0, import_jsx_runtime36.jsx)("p", { className: "px-6 py-2.5 text-[var(--color-text-muted)]", children: "This artist does not exist." })
2109
2319
  };
2110
2320
  case "error":
2111
2321
  return {
2112
2322
  ...panel,
2113
2323
  title: "Artist",
2114
- content: /* @__PURE__ */ (0, import_jsx_runtime34.jsx)("p", { className: "px-6 py-2.5 text-red-600", children: "Could not load this artist." })
2324
+ content: /* @__PURE__ */ (0, import_jsx_runtime36.jsx)("p", { className: "px-6 py-2.5 text-red-600", children: "Could not load this artist." })
2115
2325
  };
2116
2326
  case "success": {
2117
2327
  const { artist } = result;
@@ -2120,13 +2330,13 @@ function useArtistPanel({
2120
2330
  return {
2121
2331
  ...panel,
2122
2332
  title: artist.name,
2123
- content: /* @__PURE__ */ (0, import_jsx_runtime34.jsxs)(import_jsx_runtime34.Fragment, { children: [
2124
- shareUrl && /* @__PURE__ */ (0, import_jsx_runtime34.jsx)("div", { className: "genre-action-band border-b border-[var(--color-border)]", children: /* @__PURE__ */ (0, import_jsx_runtime34.jsx)("div", { className: "flex flex-wrap gap-1 px-3.5 py-3", children: shareTarget && /* @__PURE__ */ (0, import_jsx_runtime34.jsx)(ShareActionButton, { target: shareTarget, url: shareUrl }) }) }),
2125
- artist.image && /* @__PURE__ */ (0, import_jsx_runtime34.jsx)(ArtistPhoto, { image: artist.image, name: artist.name }),
2126
- /* @__PURE__ */ (0, import_jsx_runtime34.jsx)(ArtistHeader, { artist }),
2127
- /* @__PURE__ */ (0, import_jsx_runtime34.jsx)(ArtistMembers, { title: "Members", memberships: artist.members, renderArtistLink }),
2128
- /* @__PURE__ */ (0, import_jsx_runtime34.jsx)(ArtistMembers, { title: "Member of", memberships: artist.memberOf, renderArtistLink }),
2129
- artist.tracksCount > 0 && /* @__PURE__ */ (0, import_jsx_runtime34.jsx)(
2333
+ content: /* @__PURE__ */ (0, import_jsx_runtime36.jsxs)(import_jsx_runtime36.Fragment, { children: [
2334
+ shareUrl && /* @__PURE__ */ (0, import_jsx_runtime36.jsx)("div", { className: "genre-action-band border-b border-[var(--color-border)]", children: /* @__PURE__ */ (0, import_jsx_runtime36.jsx)("div", { className: "flex flex-wrap gap-1 px-3.5 py-3", children: shareTarget && /* @__PURE__ */ (0, import_jsx_runtime36.jsx)(ShareActionButton, { target: shareTarget, url: shareUrl }) }) }),
2335
+ artist.image && /* @__PURE__ */ (0, import_jsx_runtime36.jsx)(ArtistPhoto, { image: artist.image, name: artist.name }),
2336
+ /* @__PURE__ */ (0, import_jsx_runtime36.jsx)(ArtistHeader, { artist }),
2337
+ /* @__PURE__ */ (0, import_jsx_runtime36.jsx)(ArtistMembers, { title: "Members", memberships: artist.members, renderArtistLink }),
2338
+ /* @__PURE__ */ (0, import_jsx_runtime36.jsx)(ArtistMembers, { title: "Member of", memberships: artist.memberOf, renderArtistLink }),
2339
+ artist.tracksCount > 0 && /* @__PURE__ */ (0, import_jsx_runtime36.jsx)(
2130
2340
  ArtistSongs,
2131
2341
  {
2132
2342
  artist: { uuid: artist.uuid, name: artist.name },
@@ -2136,56 +2346,56 @@ function useArtistPanel({
2136
2346
  }
2137
2347
  )
2138
2348
  ] }),
2139
- extraContent: artist.albums.length > 0 ? /* @__PURE__ */ (0, import_jsx_runtime34.jsx)(ArtistDiscography, { albums: artist.albums }) : void 0
2349
+ extraContent: artist.albums.length > 0 ? /* @__PURE__ */ (0, import_jsx_runtime36.jsx)(ArtistDiscography, { albums: artist.albums }) : void 0
2140
2350
  };
2141
2351
  }
2142
2352
  }
2143
2353
  }
2144
2354
 
2145
2355
  // src/genre-tree/song/useSongPanel.tsx
2146
- var import_ui14 = require("@behindthemusictree/ui");
2147
- var import_genre_tree26 = require("@behindthemusictree/app-kit/genre-tree");
2356
+ var import_ui15 = require("@behindthemusictree/ui");
2357
+ var import_genre_tree27 = require("@behindthemusictree/app-kit/genre-tree");
2148
2358
 
2149
2359
  // src/genre-tree/song/SongPanelContent.tsx
2150
- var import_react15 = require("react");
2151
- var import_lucide_react10 = require("lucide-react");
2152
- var import_ui13 = require("@behindthemusictree/ui");
2360
+ var import_react17 = require("react");
2361
+ var import_lucide_react11 = require("lucide-react");
2362
+ var import_ui14 = require("@behindthemusictree/ui");
2153
2363
  var import_player11 = require("@behindthemusictree/app-kit/player");
2154
- var import_genre_tree25 = require("@behindthemusictree/app-kit/genre-tree");
2155
- var import_jsx_runtime35 = require("react/jsx-runtime");
2364
+ var import_genre_tree26 = require("@behindthemusictree/app-kit/genre-tree");
2365
+ var import_jsx_runtime37 = require("react/jsx-runtime");
2156
2366
  var numberFormat = new Intl.NumberFormat("en");
2157
2367
  var dateFormat = new Intl.DateTimeFormat("en", { dateStyle: "medium" });
2158
2368
  var externalLink = "text-[var(--color-primary)] hover:underline focus-visible:underline";
2159
2369
  function SongHistory({ uuid, getBackendBaseUrl }) {
2160
- const [open, setOpen] = (0, import_react15.useState)(false);
2161
- const history = (0, import_genre_tree25.useSongHistory)(uuid, getBackendBaseUrl, open);
2370
+ const [open, setOpen] = (0, import_react17.useState)(false);
2371
+ const history = (0, import_genre_tree26.useSongHistory)(uuid, getBackendBaseUrl, open);
2162
2372
  let body = null;
2163
- if (history.status === "loading") body = /* @__PURE__ */ (0, import_jsx_runtime35.jsx)(import_ui13.Skeleton, { className: "h-10", "aria-busy": "true" });
2164
- else if (history.status === "error") body = /* @__PURE__ */ (0, import_jsx_runtime35.jsx)("p", { className: "text-red-600", children: "Could not load the history." });
2373
+ if (history.status === "loading") body = /* @__PURE__ */ (0, import_jsx_runtime37.jsx)(import_ui14.Skeleton, { className: "h-10", "aria-busy": "true" });
2374
+ else if (history.status === "error") body = /* @__PURE__ */ (0, import_jsx_runtime37.jsx)("p", { className: "text-red-600", children: "Could not load the history." });
2165
2375
  else if (history.status === "success")
2166
- body = history.entries.length === 0 ? /* @__PURE__ */ (0, import_jsx_runtime35.jsx)("p", { className: "text-[var(--color-text-muted)]", children: "No changes yet." }) : /* @__PURE__ */ (0, import_jsx_runtime35.jsx)("ul", { className: "divide-y divide-[var(--color-border)]", children: history.entries.map((entry) => /* @__PURE__ */ (0, import_jsx_runtime35.jsxs)("li", { className: "py-1.5 text-sm", children: [
2167
- /* @__PURE__ */ (0, import_jsx_runtime35.jsx)("span", { className: "font-medium", children: entry.action }),
2168
- entry.oldValue !== null || entry.newValue !== null ? /* @__PURE__ */ (0, import_jsx_runtime35.jsxs)("span", { className: "text-[var(--color-text-muted)]", children: [
2376
+ body = history.entries.length === 0 ? /* @__PURE__ */ (0, import_jsx_runtime37.jsx)("p", { className: "text-[var(--color-text-muted)]", children: "No changes yet." }) : /* @__PURE__ */ (0, import_jsx_runtime37.jsx)("ul", { className: "divide-y divide-[var(--color-border)]", children: history.entries.map((entry) => /* @__PURE__ */ (0, import_jsx_runtime37.jsxs)("li", { className: "py-1.5 text-sm", children: [
2377
+ /* @__PURE__ */ (0, import_jsx_runtime37.jsx)("span", { className: "font-medium", children: entry.action }),
2378
+ entry.oldValue !== null || entry.newValue !== null ? /* @__PURE__ */ (0, import_jsx_runtime37.jsxs)("span", { className: "text-[var(--color-text-muted)]", children: [
2169
2379
  " ",
2170
2380
  "\u2014 ",
2171
2381
  entry.oldValue ?? "none",
2172
2382
  " \u2192 ",
2173
2383
  entry.newValue ?? "none"
2174
2384
  ] }) : null,
2175
- /* @__PURE__ */ (0, import_jsx_runtime35.jsxs)("span", { className: "block text-xs text-[var(--color-text-muted)]", children: [
2385
+ /* @__PURE__ */ (0, import_jsx_runtime37.jsxs)("span", { className: "block text-xs text-[var(--color-text-muted)]", children: [
2176
2386
  entry.actorPseudo ?? "Anonymous",
2177
2387
  " \xB7 ",
2178
2388
  dateFormat.format(new Date(entry.createdOn))
2179
2389
  ] })
2180
2390
  ] }, entry.uuid)) });
2181
- return /* @__PURE__ */ (0, import_jsx_runtime35.jsxs)(
2391
+ return /* @__PURE__ */ (0, import_jsx_runtime37.jsxs)(
2182
2392
  "details",
2183
2393
  {
2184
2394
  onToggle: (event) => setOpen(event.currentTarget.open),
2185
2395
  className: "px-6 pb-2.5 mt-2.5 border-t border-[var(--color-border)]",
2186
2396
  children: [
2187
- /* @__PURE__ */ (0, import_jsx_runtime35.jsx)("summary", { className: "mt-2.5 cursor-pointer text-[0.6875rem] font-semibold uppercase text-[var(--color-text-muted)]", children: "History" }),
2188
- /* @__PURE__ */ (0, import_jsx_runtime35.jsx)("div", { className: "mt-1.5", children: body })
2397
+ /* @__PURE__ */ (0, import_jsx_runtime37.jsx)("summary", { className: "mt-2.5 cursor-pointer text-[0.6875rem] font-semibold uppercase text-[var(--color-text-muted)]", children: "History" }),
2398
+ /* @__PURE__ */ (0, import_jsx_runtime37.jsx)("div", { className: "mt-1.5", children: body })
2189
2399
  ]
2190
2400
  }
2191
2401
  );
@@ -2199,25 +2409,25 @@ function SongPanelContent({
2199
2409
  genrePath,
2200
2410
  onSelectGenre
2201
2411
  }) {
2202
- const { playNewTrackListFromTrackUuid } = (0, import_genre_tree25.useTrackList)();
2412
+ const { playNewTrackListFromTrackUuid } = (0, import_genre_tree26.useTrackList)();
2203
2413
  const { playerTrackObject, playState, handlePlayPauseAction } = (0, import_player11.usePlayer)();
2204
- const unplayableReason = (0, import_genre_tree25.songUnplayableReason)(song);
2414
+ const unplayableReason = (0, import_genre_tree26.songUnplayableReason)(song);
2205
2415
  const isCurrent = playerTrackObject?.track.id === song.uuid;
2206
2416
  const isPlaying = isCurrent && playState === import_player11.PlayStates.PLAYING;
2207
2417
  const shareTarget = { kind: "song", uuid: song.uuid, name: song.title, scope };
2208
- const musicBrainzUrl = (0, import_genre_tree25.musicBrainzRecordingUrl)(song);
2418
+ const musicBrainzUrl = (0, import_genre_tree26.musicBrainzRecordingUrl)(song);
2209
2419
  const fields = [];
2210
2420
  if (song.genre) {
2211
2421
  const path = genrePath.length > 0 ? genrePath : null;
2212
2422
  fields.push({
2213
2423
  label: "Genre",
2214
- value: path ? /* @__PURE__ */ (0, import_jsx_runtime35.jsx)("span", { className: "flex flex-wrap items-center gap-x-1", children: path.map((genrePlaylist, index) => /* @__PURE__ */ (0, import_jsx_runtime35.jsxs)("span", { children: [
2215
- index > 0 && /* @__PURE__ */ (0, import_jsx_runtime35.jsx)("span", { "aria-hidden": "true", children: "\u203A " }),
2216
- /* @__PURE__ */ (0, import_jsx_runtime35.jsx)("button", { type: "button", onClick: () => onSelectGenre(genrePlaylist), className: "hover:underline", children: genrePlaylist.name })
2424
+ value: path ? /* @__PURE__ */ (0, import_jsx_runtime37.jsx)("span", { className: "flex flex-wrap items-center gap-x-1", children: path.map((genrePlaylist, index) => /* @__PURE__ */ (0, import_jsx_runtime37.jsxs)("span", { children: [
2425
+ index > 0 && /* @__PURE__ */ (0, import_jsx_runtime37.jsx)("span", { "aria-hidden": "true", children: "\u203A " }),
2426
+ /* @__PURE__ */ (0, import_jsx_runtime37.jsx)("button", { type: "button", onClick: () => onSelectGenre(genrePlaylist), className: "hover:underline", children: genrePlaylist.name })
2217
2427
  ] }, genrePlaylist.uuid)) }) : song.genre.name
2218
2428
  });
2219
2429
  } else {
2220
- fields.push({ label: "Genre", value: /* @__PURE__ */ (0, import_jsx_runtime35.jsx)("span", { className: "text-[var(--color-text-muted)]", children: "No genre" }) });
2430
+ fields.push({ label: "Genre", value: /* @__PURE__ */ (0, import_jsx_runtime37.jsx)("span", { className: "text-[var(--color-text-muted)]", children: "No genre" }) });
2221
2431
  }
2222
2432
  if (song.firstReleaseYear) fields.push({ label: "First released", value: song.firstReleaseYear });
2223
2433
  const viewCount = song.youtubeVideo?.viewCount;
@@ -2227,11 +2437,11 @@ function SongPanelContent({
2227
2437
  const links = [];
2228
2438
  if (musicBrainzUrl)
2229
2439
  links.push(
2230
- /* @__PURE__ */ (0, import_jsx_runtime35.jsx)("a", { href: musicBrainzUrl, target: "_blank", rel: "noopener noreferrer", className: externalLink, children: "MusicBrainz" }, "mb")
2440
+ /* @__PURE__ */ (0, import_jsx_runtime37.jsx)("a", { href: musicBrainzUrl, target: "_blank", rel: "noopener noreferrer", className: externalLink, children: "MusicBrainz" }, "mb")
2231
2441
  );
2232
2442
  if (song.youtubeVideo)
2233
2443
  links.push(
2234
- /* @__PURE__ */ (0, import_jsx_runtime35.jsx)(
2444
+ /* @__PURE__ */ (0, import_jsx_runtime37.jsx)(
2235
2445
  "a",
2236
2446
  {
2237
2447
  href: `https://www.youtube.com/watch?v=${encodeURIComponent(song.youtubeVideo.id)}`,
@@ -2246,33 +2456,33 @@ function SongPanelContent({
2246
2456
  if (links.length > 0)
2247
2457
  fields.push({
2248
2458
  label: "Links",
2249
- value: /* @__PURE__ */ (0, import_jsx_runtime35.jsx)("span", { className: "flex gap-3", children: links })
2459
+ value: /* @__PURE__ */ (0, import_jsx_runtime37.jsx)("span", { className: "flex gap-3", children: links })
2250
2460
  });
2251
- return /* @__PURE__ */ (0, import_jsx_runtime35.jsxs)(import_jsx_runtime35.Fragment, { children: [
2252
- /* @__PURE__ */ (0, import_jsx_runtime35.jsx)("div", { className: "genre-action-band border-b border-[var(--color-border)]", children: /* @__PURE__ */ (0, import_jsx_runtime35.jsxs)("div", { className: "flex flex-wrap gap-1 px-3.5 py-3", children: [
2253
- unplayableReason ? /* @__PURE__ */ (0, import_jsx_runtime35.jsx)("p", { className: "flex items-center px-2.5 text-sm text-[var(--color-text-muted)]", children: unplayableReason }) : /* @__PURE__ */ (0, import_jsx_runtime35.jsxs)(
2461
+ return /* @__PURE__ */ (0, import_jsx_runtime37.jsxs)(import_jsx_runtime37.Fragment, { children: [
2462
+ /* @__PURE__ */ (0, import_jsx_runtime37.jsx)("div", { className: "genre-action-band border-b border-[var(--color-border)]", children: /* @__PURE__ */ (0, import_jsx_runtime37.jsxs)("div", { className: "flex flex-wrap gap-1 px-3.5 py-3", children: [
2463
+ unplayableReason ? /* @__PURE__ */ (0, import_jsx_runtime37.jsx)("p", { className: "flex items-center px-2.5 text-sm text-[var(--color-text-muted)]", children: unplayableReason }) : /* @__PURE__ */ (0, import_jsx_runtime37.jsxs)(
2254
2464
  "button",
2255
2465
  {
2256
2466
  type: "button",
2257
2467
  onClick: () => isCurrent ? handlePlayPauseAction() : playNewTrackListFromTrackUuid(song, scope),
2258
2468
  className: "group flex flex-col items-center gap-1 w-16 text-xs text-center text-[var(--color-text)] focus-visible:outline-none",
2259
2469
  children: [
2260
- /* @__PURE__ */ (0, import_jsx_runtime35.jsx)("span", { className: "flex items-center justify-center w-10 h-10 rounded-full group-focus-visible:ring-2 group-focus-visible:ring-[var(--color-focus-ring)] bg-[var(--color-surface-muted)] transition-colors group-hover:bg-[var(--color-border)]", children: isPlaying ? /* @__PURE__ */ (0, import_jsx_runtime35.jsx)(import_lucide_react10.Pause, { "aria-hidden": "true", className: "w-5 h-5" }) : /* @__PURE__ */ (0, import_jsx_runtime35.jsx)(import_lucide_react10.Play, { "aria-hidden": "true", className: "w-5 h-5" }) }),
2470
+ /* @__PURE__ */ (0, import_jsx_runtime37.jsx)("span", { className: "flex items-center justify-center w-10 h-10 rounded-full group-focus-visible:ring-2 group-focus-visible:ring-[var(--color-focus-ring)] bg-[var(--color-surface-muted)] transition-colors group-hover:bg-[var(--color-border)]", children: isPlaying ? /* @__PURE__ */ (0, import_jsx_runtime37.jsx)(import_lucide_react11.Pause, { "aria-hidden": "true", className: "w-5 h-5" }) : /* @__PURE__ */ (0, import_jsx_runtime37.jsx)(import_lucide_react11.Play, { "aria-hidden": "true", className: "w-5 h-5" }) }),
2261
2471
  isPlaying ? "Pause" : "Play"
2262
2472
  ]
2263
2473
  }
2264
2474
  ),
2265
- shareUrl && /* @__PURE__ */ (0, import_jsx_runtime35.jsx)(ShareActionButton, { target: shareTarget, url: shareUrl })
2475
+ shareUrl && /* @__PURE__ */ (0, import_jsx_runtime37.jsx)(ShareActionButton, { target: shareTarget, url: shareUrl })
2266
2476
  ] }) }),
2267
- song.artists && song.artists.length > 0 && /* @__PURE__ */ (0, import_jsx_runtime35.jsx)("p", { className: "px-6 pt-2.5 text-[var(--color-text-muted)]", children: /* @__PURE__ */ (0, import_jsx_runtime35.jsx)(import_genre_tree25.ArtistNames, { artists: song.artists, renderArtistLink }) }),
2268
- /* @__PURE__ */ (0, import_jsx_runtime35.jsx)(import_ui13.FieldList, { items: fields }),
2269
- song.essentialGenres && song.essentialGenres.length > 0 && /* @__PURE__ */ (0, import_jsx_runtime35.jsx)(import_ui13.PanelSection, { title: "Essential in", children: /* @__PURE__ */ (0, import_jsx_runtime35.jsx)("ul", { children: song.essentialGenres.map((genre) => /* @__PURE__ */ (0, import_jsx_runtime35.jsx)("li", { className: "py-0.5", children: genre.name }, genre.uuid)) }) }),
2270
- /* @__PURE__ */ (0, import_jsx_runtime35.jsx)(SongHistory, { uuid: song.uuid, getBackendBaseUrl })
2477
+ song.artists && song.artists.length > 0 && /* @__PURE__ */ (0, import_jsx_runtime37.jsx)("p", { className: "px-6 pt-2.5 text-[var(--color-text-muted)]", children: /* @__PURE__ */ (0, import_jsx_runtime37.jsx)(import_genre_tree26.ArtistNames, { artists: song.artists, renderArtistLink }) }),
2478
+ /* @__PURE__ */ (0, import_jsx_runtime37.jsx)(import_ui14.FieldList, { items: fields }),
2479
+ song.essentialGenres && song.essentialGenres.length > 0 && /* @__PURE__ */ (0, import_jsx_runtime37.jsx)(import_ui14.PanelSection, { title: "Essential in", children: /* @__PURE__ */ (0, import_jsx_runtime37.jsx)("ul", { children: song.essentialGenres.map((genre) => /* @__PURE__ */ (0, import_jsx_runtime37.jsx)("li", { className: "py-0.5", children: genre.name }, genre.uuid)) }) }),
2480
+ /* @__PURE__ */ (0, import_jsx_runtime37.jsx)(SongHistory, { uuid: song.uuid, getBackendBaseUrl })
2271
2481
  ] });
2272
2482
  }
2273
2483
 
2274
2484
  // src/genre-tree/song/useSongPanel.tsx
2275
- var import_jsx_runtime36 = require("react/jsx-runtime");
2485
+ var import_jsx_runtime38 = require("react/jsx-runtime");
2276
2486
  function useSongPanel({
2277
2487
  uuid,
2278
2488
  scope,
@@ -2285,7 +2495,7 @@ function useSongPanel({
2285
2495
  onExpandedChange,
2286
2496
  onClose
2287
2497
  }) {
2288
- const result = (0, import_genre_tree26.useSong)(uuid, getBackendBaseUrl);
2498
+ const result = (0, import_genre_tree27.useSong)(uuid, getBackendBaseUrl);
2289
2499
  const panel = { expanded, onExpandedChange, onClose };
2290
2500
  switch (result.status) {
2291
2501
  case "idle":
@@ -2294,24 +2504,24 @@ function useSongPanel({
2294
2504
  return {
2295
2505
  ...panel,
2296
2506
  title: "Song",
2297
- content: /* @__PURE__ */ (0, import_jsx_runtime36.jsxs)("div", { "aria-busy": "true", className: "flex flex-col gap-2.5 px-6 py-2.5", children: [
2298
- /* @__PURE__ */ (0, import_jsx_runtime36.jsx)(import_ui14.Skeleton, { className: "h-3 w-16" }),
2299
- /* @__PURE__ */ (0, import_jsx_runtime36.jsx)(import_ui14.Skeleton, { className: "h-4 w-32" }),
2300
- /* @__PURE__ */ (0, import_jsx_runtime36.jsx)(import_ui14.Skeleton, { className: "h-3 w-16" }),
2301
- /* @__PURE__ */ (0, import_jsx_runtime36.jsx)(import_ui14.Skeleton, { className: "h-4 w-24" })
2507
+ content: /* @__PURE__ */ (0, import_jsx_runtime38.jsxs)("div", { "aria-busy": "true", className: "flex flex-col gap-2.5 px-6 py-2.5", children: [
2508
+ /* @__PURE__ */ (0, import_jsx_runtime38.jsx)(import_ui15.Skeleton, { className: "h-3 w-16" }),
2509
+ /* @__PURE__ */ (0, import_jsx_runtime38.jsx)(import_ui15.Skeleton, { className: "h-4 w-32" }),
2510
+ /* @__PURE__ */ (0, import_jsx_runtime38.jsx)(import_ui15.Skeleton, { className: "h-3 w-16" }),
2511
+ /* @__PURE__ */ (0, import_jsx_runtime38.jsx)(import_ui15.Skeleton, { className: "h-4 w-24" })
2302
2512
  ] })
2303
2513
  };
2304
2514
  case "not-found":
2305
2515
  return {
2306
2516
  ...panel,
2307
2517
  title: "Song not found",
2308
- content: /* @__PURE__ */ (0, import_jsx_runtime36.jsx)("p", { className: "px-6 py-2.5 text-[var(--color-text-muted)]", children: "This song does not exist." })
2518
+ content: /* @__PURE__ */ (0, import_jsx_runtime38.jsx)("p", { className: "px-6 py-2.5 text-[var(--color-text-muted)]", children: "This song does not exist." })
2309
2519
  };
2310
2520
  case "error":
2311
2521
  return {
2312
2522
  ...panel,
2313
2523
  title: "Song",
2314
- content: /* @__PURE__ */ (0, import_jsx_runtime36.jsx)("p", { className: "px-6 py-2.5 text-red-600", children: "Could not load this song." })
2524
+ content: /* @__PURE__ */ (0, import_jsx_runtime38.jsx)("p", { className: "px-6 py-2.5 text-red-600", children: "Could not load this song." })
2315
2525
  };
2316
2526
  case "success": {
2317
2527
  const { song } = result;
@@ -2319,7 +2529,7 @@ function useSongPanel({
2319
2529
  return {
2320
2530
  ...panel,
2321
2531
  title: song.title,
2322
- content: /* @__PURE__ */ (0, import_jsx_runtime36.jsx)(
2532
+ content: /* @__PURE__ */ (0, import_jsx_runtime38.jsx)(
2323
2533
  SongPanelContent,
2324
2534
  {
2325
2535
  song,
@@ -2338,29 +2548,29 @@ function useSongPanel({
2338
2548
 
2339
2549
  // src/genre-tree/SizeMetricToggle.tsx
2340
2550
  var import_md = require("react-icons/md");
2341
- var import_ui15 = require("@behindthemusictree/ui");
2342
- var import_jsx_runtime37 = require("react/jsx-runtime");
2551
+ var import_ui16 = require("@behindthemusictree/ui");
2552
+ var import_jsx_runtime39 = require("react/jsx-runtime");
2343
2553
  function SizeMetricToggle({
2344
2554
  value,
2345
2555
  onChange
2346
2556
  }) {
2347
- return /* @__PURE__ */ (0, import_jsx_runtime37.jsx)(
2348
- import_ui15.SegmentedControl,
2557
+ return /* @__PURE__ */ (0, import_jsx_runtime39.jsx)(
2558
+ import_ui16.SegmentedControl,
2349
2559
  {
2350
2560
  variant: "floating",
2351
2561
  "aria-label": "Size nodes by",
2352
2562
  value,
2353
2563
  onChange,
2354
2564
  options: [
2355
- { value: "songs", label: /* @__PURE__ */ (0, import_jsx_runtime37.jsx)(import_md.MdMusicNote, { size: 18, "aria-hidden": true }), "aria-label": "Size by songs", title: "Node size by song count" },
2356
- { value: "views", label: /* @__PURE__ */ (0, import_jsx_runtime37.jsx)(import_md.MdVisibility, { size: 18, "aria-hidden": true }), "aria-label": "Size by views", title: "Node size by song views" }
2565
+ { value: "songs", label: /* @__PURE__ */ (0, import_jsx_runtime39.jsx)(import_md.MdMusicNote, { size: 18, "aria-hidden": true }), "aria-label": "Size by songs", title: "Node size by song count" },
2566
+ { value: "views", label: /* @__PURE__ */ (0, import_jsx_runtime39.jsx)(import_md.MdVisibility, { size: 18, "aria-hidden": true }), "aria-label": "Size by views", title: "Node size by song views" }
2357
2567
  ]
2358
2568
  }
2359
2569
  );
2360
2570
  }
2361
2571
 
2362
2572
  // src/genre-tree/GenreTreeView.tsx
2363
- var import_jsx_runtime38 = require("react/jsx-runtime");
2573
+ var import_jsx_runtime40 = require("react/jsx-runtime");
2364
2574
  var NO_POP_ROOT_TITLE = "This genre tree has no 'Mainstream Pop' root yet";
2365
2575
  var REGIONS_CLASS = "[--gtv-center-left:var(--layout-center-left)] [--gtv-center-right:var(--layout-center-right)] [--gtv-center-top:var(--layout-band-top)]";
2366
2576
  var noop = () => {
@@ -2373,8 +2583,8 @@ function GenreTreeView({
2373
2583
  getBackendBaseUrl,
2374
2584
  additionalActions,
2375
2585
  viewMode: controlledViewMode,
2376
- selectedNodeId: controlledSelectedNodeId,
2377
- onSelectedNodeIdChange,
2586
+ selectedGenreUuid: controlledSelectedGenreUuid,
2587
+ onSelectedGenreUuidChange,
2378
2588
  defaultViewport,
2379
2589
  viewportRestore,
2380
2590
  onViewportChange,
@@ -2395,6 +2605,7 @@ function GenreTreeView({
2395
2605
  onArtistExpandedChange,
2396
2606
  getShareUrl,
2397
2607
  extraSearchGroups = [],
2608
+ SearchGroups,
2398
2609
  onSearchQueryChange,
2399
2610
  onSearchFocus,
2400
2611
  artistSearch = true,
@@ -2421,21 +2632,21 @@ function GenreTreeView({
2421
2632
  placement = "default",
2422
2633
  ...focus
2423
2634
  }) {
2424
- const getShareUrlRef = (0, import_react16.useRef)(getShareUrl);
2635
+ const getShareUrlRef = (0, import_react18.useRef)(getShareUrl);
2425
2636
  getShareUrlRef.current = getShareUrl;
2426
- const [internalArtistUuid, setInternalArtistUuid] = (0, import_react16.useState)(null);
2637
+ const [internalArtistUuid, setInternalArtistUuid] = (0, import_react18.useState)(null);
2427
2638
  const isArtistControlled = controlledArtistUuid !== void 0;
2428
2639
  const artistUuid = isArtistControlled ? controlledArtistUuid : internalArtistUuid;
2429
- const [internalSongUuid, setInternalSongUuid] = (0, import_react16.useState)(null);
2640
+ const [internalSongUuid, setInternalSongUuid] = (0, import_react18.useState)(null);
2430
2641
  const isSongControlled = controlledSongUuid !== void 0;
2431
2642
  const songUuid = isSongControlled ? controlledSongUuid : internalSongUuid;
2432
2643
  const changeSong = (uuid) => {
2433
2644
  if (!isSongControlled) setInternalSongUuid(uuid);
2434
2645
  onSongUuidChange?.(uuid);
2435
2646
  };
2436
- const [internalSizeMetric, setInternalSizeMetric] = (0, import_react16.useState)("songs");
2647
+ const [internalSizeMetric, setInternalSizeMetric] = (0, import_react18.useState)("songs");
2437
2648
  const sizeMetric = controlledSizeMetric ?? internalSizeMetric;
2438
- const deferredSizeMetric = (0, import_react16.useDeferredValue)(sizeMetric);
2649
+ const deferredSizeMetric = (0, import_react18.useDeferredValue)(sizeMetric);
2439
2650
  const changeSizeMetric = (metric) => {
2440
2651
  if (controlledSizeMetric === void 0) setInternalSizeMetric(metric);
2441
2652
  onSizeMetricChange?.(metric);
@@ -2450,6 +2661,8 @@ function GenreTreeView({
2450
2661
  genrePlaylistByNodeId,
2451
2662
  visibleGenrePlaylists,
2452
2663
  groupedGenrePlaylistsByRoot,
2664
+ selectedGenreUuid,
2665
+ selectGenre,
2453
2666
  selectedNodeId,
2454
2667
  selectedGenrePlaylist,
2455
2668
  selectNode,
@@ -2462,17 +2675,17 @@ function GenreTreeView({
2462
2675
  allowWheelRotation: internalAllowWheelRotation,
2463
2676
  toggleWheelRotation,
2464
2677
  onNodeHover: handleNodeHover
2465
- } = (0, import_genre_tree27.useGenreTreeViewState)({
2678
+ } = (0, import_genre_tree28.useGenreTreeViewState)({
2466
2679
  scope,
2467
2680
  getBackendBaseUrl,
2468
2681
  viewMode: controlledViewMode,
2469
- selectedNodeId: controlledSelectedNodeId,
2470
- onSelectedNodeIdChange,
2682
+ selectedGenreUuid: controlledSelectedGenreUuid,
2683
+ onSelectedGenreUuidChange,
2471
2684
  criteriaOverviewSchema,
2472
2685
  artistUuid
2473
2686
  });
2474
2687
  const restoreRevision = viewportRestore?.revision;
2475
- const [stackedRestoreOwner, setStackedRestoreOwner] = (0, import_react16.useState)({ revision: restoreRevision, rootUuid: selectedGenrePlaylist?.root.uuid });
2688
+ const [stackedRestoreOwner, setStackedRestoreOwner] = (0, import_react18.useState)({ revision: restoreRevision, rootUuid: selectedGenrePlaylist?.root.uuid });
2476
2689
  let restoreRootUuid = stackedRestoreOwner.rootUuid;
2477
2690
  if (stackedRestoreOwner.revision !== restoreRevision) {
2478
2691
  restoreRootUuid = selectedGenrePlaylist?.root.uuid;
@@ -2481,36 +2694,36 @@ function GenreTreeView({
2481
2694
  restoreRootUuid = selectedGenrePlaylist.root.uuid;
2482
2695
  setStackedRestoreOwner({ revision: restoreRevision, rootUuid: restoreRootUuid });
2483
2696
  }
2484
- const isGenreUpdating = (0, import_react_query3.useIsMutating)({ mutationKey: import_genre_tree27.genreUpdateMutationKey }) > 0;
2485
- const { mutate: updateGenre } = (0, import_genre_tree27.useUpdateGenre)(scope, getBackendBaseUrl);
2697
+ const isGenreUpdating = (0, import_react_query3.useIsMutating)({ mutationKey: import_genre_tree28.genreUpdateMutationKey }) > 0;
2698
+ const { mutate: updateGenre } = (0, import_genre_tree28.useUpdateGenre)(scope, getBackendBaseUrl);
2486
2699
  const busy = deferredSizeMetric !== sizeMetric || isTreeFetching || isGenreUpdating;
2487
2700
  const isArtistFiltered = artistUuid !== null;
2488
2701
  const canSizeByViews = !isArtistFiltered && hasViewsCount(visibleGenrePlaylists);
2489
- const artistResult = (0, import_genre_tree27.useArtist)(artistUuid, artistSearchBaseUrl ?? getBackendBaseUrl);
2702
+ const artistResult = (0, import_genre_tree28.useArtist)(artistUuid, artistSearchBaseUrl ?? getBackendBaseUrl);
2490
2703
  const filterArtistName = artistResult.status === "success" ? artistResult.artist.name : null;
2491
- const filterArtist = (0, import_react16.useMemo)(
2704
+ const filterArtist = (0, import_react18.useMemo)(
2492
2705
  () => artistUuid !== null && filterArtistName !== null ? { uuid: artistUuid, name: filterArtistName } : null,
2493
2706
  [artistUuid, filterArtistName]
2494
2707
  );
2495
2708
  const hasSelectedGenreNode = selectedGenrePlaylist !== void 0 && visibleGenrePlaylists.some(({ uuid }) => uuid === selectedGenrePlaylist.uuid);
2496
2709
  const playback = useGenrePlaylistPlayback({ scope, genrePlaylists: visibleGenrePlaylists, artist: filterArtist });
2497
2710
  const openArtist = (uuid) => {
2498
- if (selectedNodeId !== null) clearSelection();
2711
+ if (selectedGenreUuid !== null) clearSelection();
2499
2712
  if (songUuid !== null) changeSong(null);
2500
2713
  changeArtist(uuid);
2501
2714
  };
2502
2715
  const openSong = (uuid) => {
2503
- if (selectedNodeId !== null) clearSelection();
2716
+ if (selectedGenreUuid !== null) clearSelection();
2504
2717
  changeSong(uuid);
2505
2718
  };
2506
- const openSongRef = (0, import_react16.useRef)(openSong);
2719
+ const openSongRef = (0, import_react18.useRef)(openSong);
2507
2720
  openSongRef.current = openSong;
2508
- const resolvedRenderSongLink = (0, import_react16.useCallback)(
2509
- (song) => renderSongLink ? renderSongLink(song) : /* @__PURE__ */ (0, import_jsx_runtime38.jsx)("button", { type: "button", onClick: () => openSongRef.current(song.uuid), className: "hover:underline", children: song.title }),
2721
+ const resolvedRenderSongLink = (0, import_react18.useCallback)(
2722
+ (song) => renderSongLink ? renderSongLink(song) : /* @__PURE__ */ (0, import_jsx_runtime40.jsx)("button", { type: "button", onClick: () => openSongRef.current(song.uuid), className: "hover:underline", children: song.title }),
2510
2723
  [renderSongLink]
2511
2724
  );
2512
- const [expandedSongUuid, setExpandedSongUuid] = (0, import_react16.useState)(null);
2513
- const [expandedArtistUuid, setExpandedArtistUuid] = (0, import_react16.useState)(null);
2725
+ const [expandedSongUuid, setExpandedSongUuid] = (0, import_react18.useState)(null);
2726
+ const [expandedArtistUuid, setExpandedArtistUuid] = (0, import_react18.useState)(null);
2514
2727
  const artistBaseUrl = artistSearchBaseUrl ?? getBackendBaseUrl;
2515
2728
  const songBaseUrl = getBackendBaseUrl;
2516
2729
  const artistPanel = useArtistPanel({
@@ -2518,7 +2731,7 @@ function GenreTreeView({
2518
2731
  scope,
2519
2732
  getBackendBaseUrl: artistBaseUrl,
2520
2733
  getShareUrl: getShareUrlRef.current,
2521
- renderArtistLink: renderArtistLink ?? ((artist) => artist.isSpecialPurpose ? artist.name : /* @__PURE__ */ (0, import_jsx_runtime38.jsx)("button", { type: "button", onClick: () => openArtist(artist.uuid), className: "hover:underline", children: artist.name })),
2734
+ renderArtistLink: renderArtistLink ?? ((artist) => artist.isSpecialPurpose ? artist.name : /* @__PURE__ */ (0, import_jsx_runtime40.jsx)("button", { type: "button", onClick: () => openArtist(artist.uuid), className: "hover:underline", children: artist.name })),
2522
2735
  expanded: controlledArtistExpanded ?? (artistUuid !== null && expandedArtistUuid === artistUuid),
2523
2736
  onExpandedChange: (on) => {
2524
2737
  if (controlledArtistExpanded === void 0) setExpandedArtistUuid(on ? artistUuid : null);
@@ -2527,7 +2740,7 @@ function GenreTreeView({
2527
2740
  renderSongLink: resolvedRenderSongLink,
2528
2741
  onClose: () => changeArtist(null)
2529
2742
  });
2530
- const resolvedRenderArtistLink = renderArtistLink ?? ((artist) => artist.isSpecialPurpose ? artist.name : /* @__PURE__ */ (0, import_jsx_runtime38.jsx)("button", { type: "button", onClick: () => openArtist(artist.uuid), className: "hover:underline", children: artist.name }));
2743
+ const resolvedRenderArtistLink = renderArtistLink ?? ((artist) => artist.isSpecialPurpose ? artist.name : /* @__PURE__ */ (0, import_jsx_runtime40.jsx)("button", { type: "button", onClick: () => openArtist(artist.uuid), className: "hover:underline", children: artist.name }));
2531
2744
  const getGenrePath = (genre) => {
2532
2745
  const path = [];
2533
2746
  let current = visibleGenrePlaylists.find((playlist) => playlist.criteria?.uuid === genre.uuid);
@@ -2545,9 +2758,9 @@ function GenreTreeView({
2545
2758
  getShareUrl: getShareUrlRef.current,
2546
2759
  renderArtistLink: resolvedRenderArtistLink,
2547
2760
  getGenrePath,
2548
- onSelectGenre: (genrePlaylist) => {
2761
+ onSelectGenre: (genrePlaylist2) => {
2549
2762
  changeSong(null);
2550
- selectGenrePlaylist(genrePlaylist);
2763
+ selectGenrePlaylist(genrePlaylist2);
2551
2764
  },
2552
2765
  expanded: controlledSongExpanded ?? (songUuid !== null && expandedSongUuid === songUuid),
2553
2766
  onExpandedChange: (on) => {
@@ -2556,11 +2769,11 @@ function GenreTreeView({
2556
2769
  },
2557
2770
  onClose: () => changeSong(null)
2558
2771
  });
2559
- const builtInSongPanel = overlayPanel || hasSelectedGenreNode ? void 0 : songPanel;
2560
- const builtInArtistPanel = overlayPanel || hasSelectedGenreNode || builtInSongPanel ? void 0 : artistPanel;
2772
+ const builtInSongPanel = overlayPanel || selectedGenreUuid !== null ? void 0 : songPanel;
2773
+ const builtInArtistPanel = overlayPanel || selectedGenreUuid !== null || builtInSongPanel ? void 0 : artistPanel;
2561
2774
  const builtInPanel = builtInSongPanel ?? builtInArtistPanel;
2562
- const [uncontrolledCollapsed, setUncontrolledCollapsed] = (0, import_react16.useState)(false);
2563
- const [uncontrolledExpanded, setUncontrolledExpanded] = (0, import_react16.useState)(false);
2775
+ const [uncontrolledCollapsed, setUncontrolledCollapsed] = (0, import_react18.useState)(false);
2776
+ const [uncontrolledExpanded, setUncontrolledExpanded] = (0, import_react18.useState)(false);
2564
2777
  const isCollapsed = collapsed ?? uncontrolledCollapsed;
2565
2778
  const isExpanded = expanded ?? uncontrolledExpanded;
2566
2779
  const changeCollapsed = (next) => {
@@ -2571,177 +2784,114 @@ function GenreTreeView({
2571
2784
  setUncontrolledExpanded(next);
2572
2785
  onExpandedChange?.(next);
2573
2786
  };
2574
- (0, import_react16.useEffect)(() => {
2787
+ (0, import_react18.useEffect)(() => {
2575
2788
  setUncontrolledCollapsed(false);
2576
- }, [selectedNodeId, artistUuid, songUuid]);
2577
- const [isWheelRevealed, setIsWheelRevealed] = (0, import_react16.useState)(false);
2789
+ }, [selectedGenreUuid, artistUuid, songUuid]);
2790
+ const [isWheelRevealed, setIsWheelRevealed] = (0, import_react18.useState)(false);
2578
2791
  const usesHandoff = viewMode === "wheel" || viewMode === "pop-core";
2579
2792
  const isTreeReady = !isLoading && (!usesHandoff || isWheelRevealed);
2580
- const [resolveStyle, setResolveStyle] = (0, import_react16.useState)(null);
2581
- const handleNodeStyleResolverChange = (0, import_react16.useCallback)(
2793
+ const [resolveStyle, setResolveStyle] = (0, import_react18.useState)(null);
2794
+ const handleNodeStyleResolverChange = (0, import_react18.useCallback)(
2582
2795
  (resolver) => setResolveStyle(() => resolver),
2583
2796
  []
2584
2797
  );
2585
- const panelRef = (0, import_react16.useRef)(null);
2586
- const obscuredInset = (0, import_ui16.useObscuredInset)(panelRef);
2798
+ const panelRef = (0, import_react18.useRef)(null);
2799
+ const obscuredInset = (0, import_ui17.useObscuredInset)(panelRef);
2587
2800
  const hostPanel = { obscuredInset, onNodeStyleResolverChange: handleNodeStyleResolverChange };
2588
2801
  const nodeMetric = isArtistFiltered || !canSizeByViews ? "songs" : deferredSizeMetric;
2589
- const treeNodes = (0, import_react16.useMemo)(() => toTreeNodes(visibleGenrePlaylists, nodeMetric), [visibleGenrePlaylists, nodeMetric]);
2590
- const relations = (0, import_genre_tree_view4.useGenreNodeRelations)(
2802
+ const treeNodes = (0, import_react18.useMemo)(() => toTreeNodes(visibleGenrePlaylists, nodeMetric), [visibleGenrePlaylists, nodeMetric]);
2803
+ const relations = (0, import_genre_tree_view5.useGenreNodeRelations)(
2591
2804
  treeNodes,
2592
2805
  !overlayPanel && hasSelectedGenreNode ? selectedNodeId : null,
2593
2806
  resolveStyle
2594
2807
  );
2595
- const [selectionHistory, setSelectionHistory] = (0, import_react16.useState)({
2596
- current: selectedNodeId,
2597
- previous: null
2598
- });
2599
- if (selectionHistory.current !== selectedNodeId) {
2600
- setSelectionHistory({
2601
- current: selectedNodeId,
2602
- previous: selectionHistory.current ?? selectionHistory.previous
2603
- });
2808
+ const [selectionHistory, setSelectionHistory] = (0, import_react18.useState)({ current: selectedGenreUuid, previous: null });
2809
+ if (selectionHistory.current !== selectedGenreUuid) {
2810
+ setSelectionHistory({ current: selectedGenreUuid, previous: selectionHistory.current ?? selectionHistory.previous });
2604
2811
  }
2605
- const previousPlaylist = selectionHistory.previous !== null && selectionHistory.previous !== selectedNodeId ? genrePlaylistByNodeId.get(selectionHistory.previous) : void 0;
2606
- const handlePrevious = previousPlaylist && visibleGenrePlaylists.includes(previousPlaylist) ? () => handleGenreSearchSelect(previousPlaylist) : void 0;
2812
+ const handlePrevious = selectionHistory.previous ? () => selectGenre(selectionHistory.previous) : void 0;
2607
2813
  const { showPopup, hidePopup } = (0, import_popup5.usePopup)();
2608
- const [searchText, setSearchText] = (0, import_react16.useState)("");
2609
- const artistGroup = useArtistSearchGroup(
2610
- artistBaseUrl,
2611
- searchText,
2612
- (artist) => {
2613
- openArtist(artist.uuid);
2614
- onArtistSelect?.(artist);
2615
- },
2616
- Boolean(artistSearch)
2617
- );
2618
- const songGroup = useSongSearchGroup(
2619
- songBaseUrl,
2620
- searchText,
2621
- (song) => {
2622
- openSong(song.uuid);
2623
- onSongSelect?.(song);
2624
- },
2625
- Boolean(songSearch)
2626
- );
2627
- const extraGroups = [...extraSearchGroups, artistGroup, songGroup].filter(
2628
- (group) => group !== void 0
2629
- );
2814
+ const reopenPanel = () => {
2815
+ setUncontrolledCollapsed(false);
2816
+ onCollapsedChange?.(false);
2817
+ };
2630
2818
  const handleSelectedNodeChange = (node) => {
2819
+ reopenPanel();
2631
2820
  if (node !== null && songUuid !== null) changeSong(null);
2632
2821
  selectNode(node);
2633
2822
  };
2634
2823
  const handleGenreSearchClear = clearSelection;
2635
- const handleGenreSearchSelect = (genrePlaylist) => {
2824
+ const handleGenreSearchSelect = (genrePlaylist2) => {
2825
+ reopenPanel();
2636
2826
  if (songUuid !== null) changeSong(null);
2637
- selectGenrePlaylist(genrePlaylist);
2827
+ selectGenrePlaylist(genrePlaylist2);
2638
2828
  };
2639
2829
  const selectedUuid = selectedGenrePlaylist?.uuid;
2640
- const selectedChildren = selectedUuid ? visibleGenrePlaylists.filter((genrePlaylist) => genrePlaylist.parent?.uuid === selectedUuid) : [];
2830
+ const selectedChildren = selectedUuid ? visibleGenrePlaylists.filter((genrePlaylist2) => genrePlaylist2.parent?.uuid === selectedUuid) : [];
2641
2831
  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;
2642
2832
  const handleGenreSearchZoomIn = () => {
2643
2833
  if (selectedUuid) focus.onFocusGenreChange?.(selectedUuid, "zoom-in");
2644
2834
  };
2645
- const zoomInRef = (0, import_react16.useRef)(void 0);
2835
+ const zoomInRef = (0, import_react18.useRef)(void 0);
2646
2836
  zoomInRef.current = canZoomIn ? handleGenreSearchZoomIn : void 0;
2647
- const selectGenreFromPanel = (nodeId) => {
2648
- const genrePlaylist = genrePlaylistByNodeId.get(nodeId);
2649
- if (genrePlaylist) handleGenreSearchSelect(genrePlaylist);
2650
- };
2651
- const renderGenreBody = (relation) => {
2652
- const { node } = relation;
2653
- const family = /* @__PURE__ */ (0, import_jsx_runtime38.jsx)(
2654
- import_genre_tree_view4.GenreNodeFamily,
2655
- {
2656
- node,
2657
- fill: relation.fill,
2658
- textColor: relation.textColor,
2659
- parentNode: relation.parentNode,
2660
- childNodes: relation.childNodes,
2661
- onSelectNode: selectGenreFromPanel
2662
- }
2663
- );
2664
- const ancestors = /* @__PURE__ */ (0, import_jsx_runtime38.jsx)(import_genre_tree_view4.GenreNodeAncestors, { ancestorNodes: relation.ancestorNodes, onSelectNode: selectGenreFromPanel });
2665
- const genrePlaylist = genrePlaylistByNodeId.get(node.id);
2666
- const criteria = genrePlaylist?.criteria;
2667
- if (!genrePlaylist || !criteria) return { primary: /* @__PURE__ */ (0, import_jsx_runtime38.jsxs)(import_jsx_runtime38.Fragment, { children: [
2668
- family,
2669
- ancestors
2670
- ] }) };
2671
- const genreUuid = criteria.uuid;
2672
- const parentPlaylist = visibleGenrePlaylists.find(({ uuid }) => uuid === genrePlaylist.parent?.uuid);
2673
- const canToggleSide = !readOnly && parentPlaylist !== void 0 && parentPlaylist.parent === null;
2674
- const side = criteria.side ?? "core";
2675
- const shareTarget = {
2676
- kind: "genre",
2677
- genrePlaylistUuid: genrePlaylist.uuid,
2678
- name: criteria.name,
2679
- scope,
2680
- artistUuid
2681
- };
2682
- const shareUrl = getShareUrlRef.current?.(shareTarget) ?? void 0;
2683
- const popSibling = canToggleSide && side === "core" ? visibleGenrePlaylists.find(
2684
- (sibling) => sibling.parent?.uuid === parentPlaylist.uuid && sibling.uuid !== genrePlaylist.uuid && sibling.criteria?.side === "pop"
2685
- ) : void 0;
2686
- return {
2687
- primary: /* @__PURE__ */ (0, import_jsx_runtime38.jsxs)(import_jsx_runtime38.Fragment, { children: [
2688
- /* @__PURE__ */ (0, import_jsx_runtime38.jsx)(
2689
- GenreInfoPanelBody,
2837
+ const genrePlaylist = selectedGenrePlaylist;
2838
+ const criteria = genrePlaylist?.criteria;
2839
+ const parentPlaylist = visibleGenrePlaylists.find(({ uuid }) => uuid === genrePlaylist?.parent?.uuid);
2840
+ const canToggleSide = !readOnly && criteria !== void 0 && criteria !== null && parentPlaylist !== void 0 && parentPlaylist.parent === null;
2841
+ const side = criteria?.side ?? "core";
2842
+ const popSibling = canToggleSide && side === "core" ? visibleGenrePlaylists.find((sibling) => sibling.parent?.uuid === parentPlaylist.uuid && sibling.uuid !== genrePlaylist?.uuid && sibling.criteria?.side === "pop") : void 0;
2843
+ const shareTarget = selectedGenreUuid ? { kind: "genre", genreUuid: selectedGenreUuid, name: criteria?.name ?? "Genre", scope, artistUuid } : void 0;
2844
+ const shareUrl = shareTarget ? getShareUrlRef.current?.(shareTarget) ?? void 0 : void 0;
2845
+ const genreContent = useGenreDetailContent({
2846
+ genreUuid: selectedGenreUuid,
2847
+ scope,
2848
+ getBackendBaseUrl,
2849
+ onSelectGenre: (uuid) => {
2850
+ reopenPanel();
2851
+ selectGenre(uuid);
2852
+ },
2853
+ expanded: isExpanded,
2854
+ playback,
2855
+ genrePlaylist: selectedGenrePlaylist,
2856
+ band: {
2857
+ ...relations ? { node: relations.node } : {},
2858
+ shareTarget: shareUrl ? shareTarget : void 0,
2859
+ shareUrl,
2860
+ onZoomIn: zoomInRef.current,
2861
+ onAddChild: readOnly || !criteria ? void 0 : () => handleGenreCreationAction(criteria),
2862
+ onRename: readOnly || !criteria ? void 0 : () => handleGenreRenameAction(criteria),
2863
+ onChangeParent: readOnly || !genrePlaylist || !criteria ? void 0 : () => showPopup(
2864
+ /* @__PURE__ */ (0, import_jsx_runtime40.jsx)(
2865
+ ChangeParentPopup,
2690
2866
  {
2691
- family: /* @__PURE__ */ (0, import_jsx_runtime38.jsxs)(import_jsx_runtime38.Fragment, { children: [
2692
- family,
2693
- ancestors
2694
- ] }),
2695
- genreUuid,
2696
- genrePlaylist,
2697
- catalogue: genrePlaylist.catalogue,
2698
- wikidataId: criteria.wikidataId,
2867
+ genrePlaylist: { ...genrePlaylist, criteria },
2868
+ genrePlaylists: visibleGenrePlaylists,
2699
2869
  scope,
2700
2870
  getBackendBaseUrl,
2701
- playback,
2702
- band: {
2703
- node,
2704
- shareTarget: shareUrl ? shareTarget : void 0,
2705
- shareUrl,
2706
- onZoomIn: zoomInRef.current,
2707
- onAddChild: readOnly ? void 0 : () => handleGenreCreationAction(criteria),
2708
- onRename: readOnly ? void 0 : () => handleGenreRenameAction(criteria),
2709
- onChangeParent: readOnly ? void 0 : () => showPopup(
2710
- /* @__PURE__ */ (0, import_jsx_runtime38.jsx)(
2711
- ChangeParentPopup,
2712
- {
2713
- genrePlaylist: { ...genrePlaylist, criteria },
2714
- genrePlaylists: visibleGenrePlaylists,
2715
- scope,
2716
- getBackendBaseUrl,
2717
- onClose: hidePopup,
2718
- onParentChanged
2719
- }
2720
- )
2721
- ),
2722
- side,
2723
- onToggleSide: canToggleSide ? () => updateGenre({ uuid: genreUuid, data: { side: side === "pop" ? "core" : "pop" } }) : void 0,
2724
- toggleSideDisabledReason: popSibling ? `${popSibling.name} is already the pop side of ${parentPlaylist?.name}` : void 0,
2725
- additionalActions
2726
- },
2727
- extras: {
2728
- criteriaOverviewSchema,
2729
- renderGenreDetailExtras,
2730
- renderArtistLink,
2731
- renderSongLink: resolvedRenderSongLink,
2732
- artist: filterArtist,
2733
- renderSummary,
2734
- renderEssentialTracks,
2735
- renderSongLeading,
2736
- renderSongActions,
2737
- renderTracksToolbar
2738
- }
2871
+ onClose: hidePopup,
2872
+ onParentChanged
2739
2873
  }
2740
- ),
2741
- renderDangerZone?.(node)
2742
- ] })
2743
- };
2744
- };
2874
+ )
2875
+ ),
2876
+ side,
2877
+ onToggleSide: canToggleSide && criteria ? () => updateGenre({ uuid: criteria.uuid, data: { side: side === "pop" ? "core" : "pop" } }) : void 0,
2878
+ toggleSideDisabledReason: popSibling ? `${popSibling.name} is already the pop side of ${parentPlaylist?.name}` : void 0,
2879
+ additionalActions
2880
+ },
2881
+ extras: {
2882
+ criteriaOverviewSchema,
2883
+ renderGenreDetailExtras,
2884
+ renderArtistLink,
2885
+ renderSongLink: resolvedRenderSongLink,
2886
+ artist: filterArtist,
2887
+ renderSummary,
2888
+ renderEssentialTracks,
2889
+ renderSongLeading,
2890
+ renderSongActions,
2891
+ renderTracksToolbar
2892
+ },
2893
+ trailing: relations ? renderDangerZone?.(relations.node) : void 0
2894
+ });
2745
2895
  const isRotationControlled = controlledAllowWheelRotation !== void 0;
2746
2896
  const allowWheelRotation = controlledAllowWheelRotation ?? internalAllowWheelRotation;
2747
2897
  let activePanel = null;
@@ -2755,16 +2905,16 @@ function GenreTreeView({
2755
2905
  onClose: overlayPanel.onClose,
2756
2906
  compact: false
2757
2907
  };
2758
- } else if (relations) {
2759
- const { primary } = renderGenreBody(relations);
2908
+ } else if (selectedGenreUuid) {
2760
2909
  activePanel = {
2761
- title: relations.node.name,
2762
- content: primary,
2910
+ title: genreContent.title,
2911
+ content: genreContent.content,
2912
+ aside: genreContent.aside,
2763
2913
  expanded: isExpanded,
2764
2914
  onExpandedChange: changeExpanded,
2765
2915
  onClose: clearSelection,
2766
- compact: true,
2767
- headerAccent: { background: relations.fill, color: relations.textColor }
2916
+ compact: false,
2917
+ headerAccent: relations ? { background: relations.fill, color: relations.textColor } : genreContent.headerAccent
2768
2918
  };
2769
2919
  } else if (builtInPanel) {
2770
2920
  activePanel = {
@@ -2777,36 +2927,36 @@ function GenreTreeView({
2777
2927
  compact: true
2778
2928
  };
2779
2929
  }
2780
- const isPanelShown = isTreeReady && activePanel !== null;
2930
+ const isPanelShown = activePanel !== null && (selectedGenreUuid !== null || isTreeReady);
2781
2931
  const detailTab = placement === "regions" && activePanel?.onExpandedChange ? {
2782
2932
  edge: activePanel.expanded ? { label: "Shrink panel", onClick: () => activePanel.onExpandedChange?.(false) } : isCollapsed ? null : { label: "Hide panel", onClick: () => changeCollapsed(true) },
2783
2933
  center: activePanel.expanded ? null : isCollapsed ? { label: "Show panel", onClick: () => changeCollapsed(false) } : activePanel.onExpandedChange ? { label: "Extend panel", onClick: () => activePanel.onExpandedChange?.(true) } : null
2784
2934
  } : void 0;
2785
- const expandButton = placement === "default" && activePanel?.onExpandedChange ? /* @__PURE__ */ (0, import_jsx_runtime38.jsx)(
2786
- import_ui16.IconButton,
2935
+ const expandButton = placement === "default" && activePanel?.onExpandedChange ? /* @__PURE__ */ (0, import_jsx_runtime40.jsx)(
2936
+ import_ui17.IconButton,
2787
2937
  {
2788
2938
  "aria-label": activePanel.expanded ? "Shrink panel" : "Extend panel",
2789
2939
  style: activePanel.headerAccent && { color: "inherit" },
2790
2940
  onClick: () => activePanel.onExpandedChange?.(!activePanel.expanded),
2791
- children: activePanel.expanded ? /* @__PURE__ */ (0, import_jsx_runtime38.jsx)(import_lucide_react12.Minimize2, { "aria-hidden": "true", className: "h-4 w-4" }) : /* @__PURE__ */ (0, import_jsx_runtime38.jsx)(import_lucide_react12.Maximize2, { "aria-hidden": "true", className: "h-4 w-4" })
2941
+ children: activePanel.expanded ? /* @__PURE__ */ (0, import_jsx_runtime40.jsx)(import_lucide_react13.Minimize2, { "aria-hidden": "true", className: "h-4 w-4" }) : /* @__PURE__ */ (0, import_jsx_runtime40.jsx)(import_lucide_react13.Maximize2, { "aria-hidden": "true", className: "h-4 w-4" })
2792
2942
  }
2793
2943
  ) : null;
2794
2944
  const renderArtistFilter = () => {
2795
2945
  if (isLoading || !filterArtist) return null;
2796
- return /* @__PURE__ */ (0, import_jsx_runtime38.jsxs)("span", { className: "inline-flex items-center overflow-hidden rounded-full bg-[var(--color-neutral-100)] text-sm", children: [
2797
- /* @__PURE__ */ (0, import_jsx_runtime38.jsx)(
2946
+ return /* @__PURE__ */ (0, import_jsx_runtime40.jsxs)("span", { className: "inline-flex items-center overflow-hidden rounded-full bg-[var(--color-neutral-100)] text-sm", children: [
2947
+ /* @__PURE__ */ (0, import_jsx_runtime40.jsx)(
2798
2948
  "button",
2799
2949
  {
2800
2950
  type: "button",
2801
2951
  className: "py-1 pl-3 pr-2 [@media(pointer:coarse)]:min-h-[var(--layout-touch-target)] hover:underline",
2802
2952
  "aria-label": `Show artist ${filterArtist.name}`,
2803
2953
  onClick: () => {
2804
- if (selectedNodeId !== null) clearSelection();
2954
+ if (selectedGenreUuid !== null) clearSelection();
2805
2955
  },
2806
2956
  children: filterArtist.name
2807
2957
  }
2808
2958
  ),
2809
- /* @__PURE__ */ (0, import_jsx_runtime38.jsx)(
2959
+ /* @__PURE__ */ (0, import_jsx_runtime40.jsx)(
2810
2960
  "button",
2811
2961
  {
2812
2962
  type: "button",
@@ -2818,27 +2968,42 @@ function GenreTreeView({
2818
2968
  )
2819
2969
  ] });
2820
2970
  };
2821
- const actions = /* @__PURE__ */ (0, import_jsx_runtime38.jsxs)(import_jsx_runtime38.Fragment, { children: [
2822
- !isLoading && /* @__PURE__ */ (0, import_jsx_runtime38.jsx)(
2823
- GenreSearch,
2971
+ const actions = /* @__PURE__ */ (0, import_jsx_runtime40.jsxs)(import_jsx_runtime40.Fragment, { children: [
2972
+ !isLoading && /* @__PURE__ */ (0, import_jsx_runtime40.jsx)(
2973
+ GenreTreeSearch,
2824
2974
  {
2975
+ artistBaseUrl,
2976
+ songBaseUrl,
2977
+ artistSearch: Boolean(artistSearch),
2978
+ songSearch: Boolean(songSearch),
2979
+ onArtistSelect: (artist) => {
2980
+ openArtist(artist.uuid);
2981
+ onArtistSelect?.(artist);
2982
+ },
2983
+ onSongSelect: (song) => {
2984
+ openSong(song.uuid);
2985
+ onSongSelect?.(song);
2986
+ },
2987
+ SearchGroups,
2825
2988
  genrePlaylists: visibleGenrePlaylists,
2826
2989
  onSelect: handleGenreSearchSelect,
2827
- selectedName: selectedGenrePlaylist?.name ?? (typeof builtInPanel?.title === "string" ? builtInPanel.title : null),
2990
+ onSelectGenre: (uuid) => {
2991
+ reopenPanel();
2992
+ if (songUuid) changeSong(null);
2993
+ selectGenre(uuid);
2994
+ },
2995
+ selectedName: (selectedGenreUuid ? genreContent.title : null) ?? (typeof builtInPanel?.title === "string" ? builtInPanel.title : null),
2828
2996
  onClear: builtInSongPanel ? () => changeSong(null) : builtInArtistPanel ? () => changeArtist(null) : handleGenreSearchClear,
2829
2997
  onPrevious: handlePrevious,
2830
- onQueryChange: (text) => {
2831
- setSearchText(text);
2832
- onSearchQueryChange?.(text);
2833
- },
2998
+ onQueryChange: onSearchQueryChange,
2834
2999
  onFocus: onSearchFocus,
2835
- extraGroups
3000
+ extraGroups: extraSearchGroups
2836
3001
  }
2837
3002
  ),
2838
- !isLoading && filterArtist && /* @__PURE__ */ (0, import_jsx_runtime38.jsx)("div", { className: "hidden w-full justify-center max-md:flex", children: renderArtistFilter() }),
2839
- /* @__PURE__ */ (0, import_jsx_runtime38.jsxs)("div", { className: "contents max-md:flex max-md:w-full max-md:items-center max-md:gap-2 max-md:overflow-x-auto max-md:[scrollbar-width:none] max-md:[&>*]:shrink-0", children: [
2840
- !isLoading && !isControlled && /* @__PURE__ */ (0, import_jsx_runtime38.jsx)(
2841
- import_ui16.SegmentedControl,
3003
+ !isLoading && filterArtist && /* @__PURE__ */ (0, import_jsx_runtime40.jsx)("div", { className: "hidden w-full justify-center max-md:flex", children: renderArtistFilter() }),
3004
+ /* @__PURE__ */ (0, import_jsx_runtime40.jsxs)("div", { className: "contents max-md:flex max-md:w-full max-md:items-center max-md:gap-2 max-md:overflow-x-auto max-md:[scrollbar-width:none] max-md:[&>*]:shrink-0", children: [
3005
+ !isLoading && !isControlled && /* @__PURE__ */ (0, import_jsx_runtime40.jsx)(
3006
+ import_ui17.SegmentedControl,
2842
3007
  {
2843
3008
  "aria-label": "Tree view mode",
2844
3009
  value: viewMode,
@@ -2851,14 +3016,14 @@ function GenreTreeView({
2851
3016
  ]
2852
3017
  }
2853
3018
  ),
2854
- !isLoading && showDisplayOptions && /* @__PURE__ */ (0, import_jsx_runtime38.jsx)(
3019
+ !isLoading && showDisplayOptions && /* @__PURE__ */ (0, import_jsx_runtime40.jsx)(
2855
3020
  "div",
2856
3021
  {
2857
3022
  className: "flex items-center gap-1",
2858
3023
  role: "group",
2859
3024
  "aria-label": "Tree display options",
2860
- children: !isRotationControlled && viewMode !== "stacked" && viewMode !== "outline" && /* @__PURE__ */ (0, import_jsx_runtime38.jsx)(
2861
- import_ui16.Button,
3025
+ children: !isRotationControlled && viewMode !== "stacked" && viewMode !== "outline" && /* @__PURE__ */ (0, import_jsx_runtime40.jsx)(
3026
+ import_ui17.Button,
2862
3027
  {
2863
3028
  variant: allowWheelRotation ? "default" : "outline",
2864
3029
  size: "sm",
@@ -2868,44 +3033,44 @@ function GenreTreeView({
2868
3033
  )
2869
3034
  }
2870
3035
  ),
2871
- !isLoading && !readOnly && showAddRoot && /* @__PURE__ */ (0, import_jsx_runtime38.jsx)(
2872
- import_ui16.IconTextButton,
3036
+ !isLoading && !readOnly && showAddRoot && /* @__PURE__ */ (0, import_jsx_runtime40.jsx)(
3037
+ import_ui17.IconTextButton,
2873
3038
  {
2874
- icon: import_lucide_react11.Plus,
3039
+ icon: import_lucide_react12.Plus,
2875
3040
  text: "Add root",
2876
3041
  onClick: () => handleGenreCreationAction(null)
2877
3042
  }
2878
3043
  )
2879
3044
  ] })
2880
3045
  ] });
2881
- return /* @__PURE__ */ (0, import_jsx_runtime38.jsxs)(
3046
+ return /* @__PURE__ */ (0, import_jsx_runtime40.jsxs)(
2882
3047
  "div",
2883
3048
  {
2884
3049
  "data-left-panel": isPanelShown && !isCollapsed ? void 0 : "hidden",
2885
3050
  className: ["genre-tree-view relative flex flex-col h-full", placement === "regions" && REGIONS_CLASS, className2].filter(Boolean).join(" "),
2886
3051
  children: [
2887
- /* @__PURE__ */ (0, import_jsx_runtime38.jsx)(
3052
+ /* @__PURE__ */ (0, import_jsx_runtime40.jsx)(
2888
3053
  "div",
2889
3054
  {
2890
3055
  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"}`,
2891
3056
  children: actions
2892
3057
  }
2893
3058
  ),
2894
- !isLoading && filterArtist && /* @__PURE__ */ (0, import_jsx_runtime38.jsx)(
3059
+ !isLoading && filterArtist && /* @__PURE__ */ (0, import_jsx_runtime40.jsx)(
2895
3060
  "div",
2896
3061
  {
2897
3062
  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"}`,
2898
3063
  children: renderArtistFilter()
2899
3064
  }
2900
3065
  ),
2901
- /* @__PURE__ */ (0, import_jsx_runtime38.jsx)("div", { className: "content-container flex-1 min-h-0 flex flex-row gap-4", children: /* @__PURE__ */ (0, import_jsx_runtime38.jsxs)("div", { className: "tree-view-container relative flex-1 min-w-0 flex flex-col h-full", children: [
2902
- !isLoading && viewMode === "outline" && floatingControlsLeading && /* @__PURE__ */ (0, import_jsx_runtime38.jsx)("div", { className: "absolute bottom-3 right-3 z-10", children: floatingControlsLeading }),
2903
- isLoading ? /* @__PURE__ */ (0, import_jsx_runtime38.jsx)(import_genre_tree_view4.GenreTreeViewSkeleton, { viewMode }) : viewMode === "wheel" ? (
3066
+ /* @__PURE__ */ (0, import_jsx_runtime40.jsx)("div", { className: "content-container flex-1 min-h-0 flex flex-row gap-4", children: /* @__PURE__ */ (0, import_jsx_runtime40.jsxs)("div", { className: "tree-view-container relative flex-1 min-w-0 flex flex-col h-full", children: [
3067
+ !isLoading && viewMode === "outline" && floatingControlsLeading && /* @__PURE__ */ (0, import_jsx_runtime40.jsx)("div", { className: "absolute bottom-3 right-3 z-10", children: floatingControlsLeading }),
3068
+ isLoading ? /* @__PURE__ */ (0, import_jsx_runtime40.jsx)(import_genre_tree_view5.GenreTreeViewSkeleton, { viewMode }) : viewMode === "wheel" ? (
2904
3069
  // The info panel's top edge aligns with the tree's; the search bar floats `--layout-panel-inset` below it,
2905
3070
  // and the header inset (the bar's height plus that inset) extends the header's fill behind the bar,
2906
3071
  // Google Maps style; the 8px bottom padding mirrors the inset above the bar, and the hidden title's height is zeroed.
2907
3072
  // The panel's own Zoom in button is hidden: the search bar carries it.
2908
- /* @__PURE__ */ (0, import_jsx_runtime38.jsx)("div", { className: "tree-container search-overlay relative flex-1 w-full min-h-0 ", children: /* @__PURE__ */ (0, import_jsx_runtime38.jsx)(GenreTreeWheelHandoff, { skeleton: /* @__PURE__ */ (0, import_jsx_runtime38.jsx)(import_genre_tree_view4.GenreTreeWheelSkeleton, {}), onRevealedChange: setIsWheelRevealed, children: /* @__PURE__ */ (0, import_jsx_runtime38.jsx)(
3073
+ /* @__PURE__ */ (0, import_jsx_runtime40.jsx)("div", { className: "tree-container search-overlay relative flex-1 w-full min-h-0 ", children: /* @__PURE__ */ (0, import_jsx_runtime40.jsx)(GenreTreeWheelHandoff, { skeleton: /* @__PURE__ */ (0, import_jsx_runtime40.jsx)(import_genre_tree_view5.GenreTreeWheelSkeleton, {}), onRevealedChange: setIsWheelRevealed, children: /* @__PURE__ */ (0, import_jsx_runtime40.jsx)(
2909
3074
  GenrePlaylistTreeWheel,
2910
3075
  {
2911
3076
  scope,
@@ -2922,9 +3087,9 @@ function GenreTreeView({
2922
3087
  allowWheelRotation,
2923
3088
  sizeMetric: isArtistFiltered || !canSizeByViews ? "songs" : deferredSizeMetric,
2924
3089
  busy,
2925
- floatingControlsLeading: floatingControlsLeading || canSizeByViews ? /* @__PURE__ */ (0, import_jsx_runtime38.jsxs)("div", { className: "flex items-center gap-2", children: [
3090
+ floatingControlsLeading: floatingControlsLeading || canSizeByViews ? /* @__PURE__ */ (0, import_jsx_runtime40.jsxs)("div", { className: "flex items-center gap-2", children: [
2926
3091
  floatingControlsLeading,
2927
- canSizeByViews && /* @__PURE__ */ (0, import_jsx_runtime38.jsx)(SizeMetricToggle, { value: sizeMetric, onChange: changeSizeMetric })
3092
+ canSizeByViews && /* @__PURE__ */ (0, import_jsx_runtime40.jsx)(SizeMetricToggle, { value: sizeMetric, onChange: changeSizeMetric })
2928
3093
  ] }) : void 0,
2929
3094
  defaultViewport,
2930
3095
  viewportRestore,
@@ -2933,7 +3098,7 @@ function GenreTreeView({
2933
3098
  ) }) })
2934
3099
  ) : viewMode === "pop-core" ? (
2935
3100
  // Same as the wheel: the header inset extends its fill behind the floating search bar, Google Maps style.
2936
- /* @__PURE__ */ (0, import_jsx_runtime38.jsx)("div", { className: "tree-container search-overlay relative flex-1 w-full min-h-0 ", children: /* @__PURE__ */ (0, import_jsx_runtime38.jsx)(GenreTreeWheelHandoff, { skeleton: /* @__PURE__ */ (0, import_jsx_runtime38.jsx)(import_genre_tree_view4.GenreTreeWheelSkeleton, {}), onRevealedChange: setIsWheelRevealed, children: /* @__PURE__ */ (0, import_jsx_runtime38.jsx)(
3101
+ /* @__PURE__ */ (0, import_jsx_runtime40.jsx)("div", { className: "tree-container search-overlay relative flex-1 w-full min-h-0 ", children: /* @__PURE__ */ (0, import_jsx_runtime40.jsx)(GenreTreeWheelHandoff, { skeleton: /* @__PURE__ */ (0, import_jsx_runtime40.jsx)(import_genre_tree_view5.GenreTreeWheelSkeleton, {}), onRevealedChange: setIsWheelRevealed, children: /* @__PURE__ */ (0, import_jsx_runtime40.jsx)(
2937
3102
  GenrePlaylistTreeWheelRadialPopCore,
2938
3103
  {
2939
3104
  scope,
@@ -2950,9 +3115,9 @@ function GenreTreeView({
2950
3115
  allowWheelRotation,
2951
3116
  sizeMetric: isArtistFiltered || !canSizeByViews ? "songs" : deferredSizeMetric,
2952
3117
  busy,
2953
- floatingControlsLeading: floatingControlsLeading || canSizeByViews ? /* @__PURE__ */ (0, import_jsx_runtime38.jsxs)("div", { className: "flex items-center gap-2", children: [
3118
+ floatingControlsLeading: floatingControlsLeading || canSizeByViews ? /* @__PURE__ */ (0, import_jsx_runtime40.jsxs)("div", { className: "flex items-center gap-2", children: [
2954
3119
  floatingControlsLeading,
2955
- canSizeByViews && /* @__PURE__ */ (0, import_jsx_runtime38.jsx)(SizeMetricToggle, { value: sizeMetric, onChange: changeSizeMetric })
3120
+ canSizeByViews && /* @__PURE__ */ (0, import_jsx_runtime40.jsx)(SizeMetricToggle, { value: sizeMetric, onChange: changeSizeMetric })
2956
3121
  ] }) : void 0,
2957
3122
  defaultViewport,
2958
3123
  viewportRestore,
@@ -2960,7 +3125,7 @@ function GenreTreeView({
2960
3125
  ...focus
2961
3126
  }
2962
3127
  ) }) })
2963
- ) : viewMode === "outline" ? /* @__PURE__ */ (0, import_jsx_runtime38.jsx)("div", { className: "tree-container flex-1 min-h-0 w-full overflow-y-auto relative", children: /* @__PURE__ */ (0, import_jsx_runtime38.jsx)(
3128
+ ) : viewMode === "outline" ? /* @__PURE__ */ (0, import_jsx_runtime40.jsx)("div", { className: "tree-container flex-1 min-h-0 w-full overflow-y-auto relative", children: /* @__PURE__ */ (0, import_jsx_runtime40.jsx)(
2964
3129
  GenrePlaylistTreeWheelRadialPopCore,
2965
3130
  {
2966
3131
  outline: true,
@@ -2976,13 +3141,13 @@ function GenreTreeView({
2976
3141
  selectedNodeId,
2977
3142
  readOnly
2978
3143
  }
2979
- ) }) : /* @__PURE__ */ (0, import_jsx_runtime38.jsx)("div", { className: "tree-container relative flex flex-col gap-4 w-full pt-14 overflow-x-auto overflow-y-auto text-[var(--color-text)]", children: Object.entries(groupedGenrePlaylistsByRoot).map(
3144
+ ) }) : /* @__PURE__ */ (0, import_jsx_runtime40.jsx)("div", { className: "tree-container relative flex flex-col gap-4 w-full pt-14 overflow-x-auto overflow-y-auto text-[var(--color-text)]", children: Object.entries(groupedGenrePlaylistsByRoot).map(
2980
3145
  ([uuid, genrePlaylistTreePerRoot]) => {
2981
- return /* @__PURE__ */ (0, import_jsx_runtime38.jsx)(
3146
+ return /* @__PURE__ */ (0, import_jsx_runtime40.jsx)(
2982
3147
  "div",
2983
3148
  {
2984
3149
  className: "tree-per-root-container relative shrink-0 h-[28rem] mt-2 mr-16 p-2 bg-[var(--color-neutral-100)] rounded-lg",
2985
- children: /* @__PURE__ */ (0, import_jsx_runtime38.jsx)("div", { className: "graph-container relative z-10 h-full", children: /* @__PURE__ */ (0, import_jsx_runtime38.jsx)(
3150
+ children: /* @__PURE__ */ (0, import_jsx_runtime40.jsx)("div", { className: "graph-container relative z-10 h-full", children: /* @__PURE__ */ (0, import_jsx_runtime40.jsx)(
2986
3151
  GenrePlaylistTreePerRoot,
2987
3152
  {
2988
3153
  scope,
@@ -3008,8 +3173,8 @@ function GenreTreeView({
3008
3173
  );
3009
3174
  }
3010
3175
  ) }),
3011
- /* @__PURE__ */ (0, import_jsx_runtime38.jsx)(
3012
- import_ui16.DetailPanel,
3176
+ /* @__PURE__ */ (0, import_jsx_runtime40.jsx)(
3177
+ import_ui17.DetailPanel,
3013
3178
  {
3014
3179
  ref: panelRef,
3015
3180
  side: "left",
@@ -3019,13 +3184,13 @@ function GenreTreeView({
3019
3184
  headerAccent: activePanel?.headerAccent,
3020
3185
  headerActions: expandButton,
3021
3186
  onClose: activePanel?.onClose ?? noop,
3022
- collapsed: isCollapsed || !isTreeReady || !activePanel,
3187
+ collapsed: isCollapsed || !isPanelShown,
3023
3188
  expanded: activePanel?.expanded,
3024
3189
  onExpandedChange: activePanel?.onExpandedChange,
3025
3190
  aside: activePanel?.aside,
3026
3191
  edgeTab: detailTab,
3027
- headerClassName: activePanel?.compact ? "pt-[calc(var(--layout-search-h)+var(--layout-panel-inset)+0.5rem)]" : void 0,
3028
- children: activePanel?.aside && !activePanel.expanded ? /* @__PURE__ */ (0, import_jsx_runtime38.jsxs)(import_jsx_runtime38.Fragment, { children: [
3192
+ headerClassName: "pt-[calc(var(--layout-search-h)+var(--layout-panel-inset)+0.5rem)]",
3193
+ children: activePanel?.aside && !activePanel.expanded ? /* @__PURE__ */ (0, import_jsx_runtime40.jsxs)(import_jsx_runtime40.Fragment, { children: [
3029
3194
  activePanel.content,
3030
3195
  activePanel.aside
3031
3196
  ] }) : activePanel?.content
@@ -3037,145 +3202,17 @@ function GenreTreeView({
3037
3202
  );
3038
3203
  }
3039
3204
 
3040
- // src/genre-tree/GenreDetailPanel.tsx
3041
- var import_react17 = require("react");
3042
- var import_genre_tree_view5 = require("@behindthemusictree/genre-tree-view");
3043
- var import_ui17 = require("@behindthemusictree/ui");
3044
- var import_lucide_react13 = require("lucide-react");
3045
- var import_genre_tree28 = require("@behindthemusictree/app-kit/genre-tree");
3046
- var import_jsx_runtime39 = require("react/jsx-runtime");
3047
- var RELATIVE_ITEM_COUNT = 1;
3048
- function toNode(genre, parentId) {
3049
- return {
3050
- id: genre.uuid,
3051
- parentId,
3052
- name: genre.name,
3053
- itemCount: RELATIVE_ITEM_COUNT,
3054
- side: genre.side ?? void 0,
3055
- wikidataId: genre.wikidataId
3056
- };
3057
- }
3058
- function GenreDetailPanel({
3059
- genreUuid,
3060
- scope,
3061
- getBackendBaseUrl,
3062
- onClose,
3063
- onSelectGenre,
3064
- onZoomIn,
3065
- shareUrl,
3066
- shareTarget,
3067
- collapsed,
3068
- expanded,
3069
- onExpandedChange,
3070
- edgeTab,
3071
- extras
3072
- }) {
3073
- const { data: detail, isPending, isError } = (0, import_genre_tree28.useFetchGenreDetail)(genreUuid, scope, getBackendBaseUrl);
3074
- const playback = useGenrePlaylistPlayback({ scope, genrePlaylists: [] });
3075
- const view = (0, import_react17.useMemo)(() => {
3076
- if (!detail) return null;
3077
- const rootColor = (0, import_genre_tree_view5.getGenreTreeColor)(detail.root.wikidataId);
3078
- const relative = (genre, parentId) => ({
3079
- node: toNode(genre, parentId),
3080
- fill: (0, import_genre_tree_view5.tintSurface)(rootColor),
3081
- textColor: import_genre_tree_view5.TEXT_COLOR
3082
- });
3083
- const ancestors2 = [...detail.ascendants].sort((a, b) => b.degree - a.degree);
3084
- return {
3085
- node: {
3086
- id: detail.uuid,
3087
- parentId: detail.parent?.uuid ?? null,
3088
- name: detail.name,
3089
- itemCount: scope === "reference" ? (0, import_genre_tree28.genreMusicCount)(import_genre_tree28.GenrePlaylistSummarySchema.parse(detail.criteriaPlaylist)) : detail.tracksCount,
3090
- wikidataId: detail.wikidataId
3091
- },
3092
- fill: rootColor,
3093
- parentNode: detail.parent ? relative(detail.parent, null) : null,
3094
- childNodes: detail.children.map((child) => relative(child, detail.uuid)),
3095
- ancestorNodes: ancestors2.map(
3096
- ({ ascendant }, index) => relative(ascendant, index === 0 ? null : ancestors2[index - 1].ascendant.uuid)
3097
- )
3098
- };
3099
- }, [detail, scope]);
3100
- if (isError) {
3101
- return /* @__PURE__ */ (0, import_jsx_runtime39.jsx)("div", { role: "alert", className: "p-4 text-sm text-[var(--color-text)]", children: "This genre could not be loaded." });
3102
- }
3103
- if (isPending || !detail || !view) {
3104
- return /* @__PURE__ */ (0, import_jsx_runtime39.jsxs)("div", { className: "flex flex-col gap-3 p-4", "aria-busy": "true", "data-testid": "genre-detail-panel-skeleton", children: [
3105
- /* @__PURE__ */ (0, import_jsx_runtime39.jsx)(import_ui17.Skeleton, { className: "h-6 w-2/3" }),
3106
- /* @__PURE__ */ (0, import_jsx_runtime39.jsx)(import_ui17.Skeleton, { className: "h-4 w-full" }),
3107
- /* @__PURE__ */ (0, import_jsx_runtime39.jsx)(import_ui17.Skeleton, { className: "h-4 w-3/4" })
3108
- ] });
3109
- }
3110
- const selectGenre = (nodeId) => onSelectGenre(nodeId);
3111
- const family = /* @__PURE__ */ (0, import_jsx_runtime39.jsx)(
3112
- import_genre_tree_view5.GenreNodeFamily,
3113
- {
3114
- node: view.node,
3115
- fill: view.fill,
3116
- textColor: import_genre_tree_view5.ACCENT_TEXT_COLOR,
3117
- parentNode: view.parentNode,
3118
- childNodes: view.childNodes,
3119
- onSelectNode: selectGenre
3120
- }
3121
- );
3122
- const ancestors = /* @__PURE__ */ (0, import_jsx_runtime39.jsx)(import_genre_tree_view5.GenreNodeAncestors, { ancestorNodes: view.ancestorNodes, onSelectNode: selectGenre });
3123
- return /* @__PURE__ */ (0, import_jsx_runtime39.jsxs)(
3124
- import_ui17.DetailPanel,
3125
- {
3126
- side: "left",
3127
- title: view.node.name,
3128
- headerAccent: { background: view.fill, color: import_genre_tree_view5.ACCENT_TEXT_COLOR },
3129
- headerActions: onExpandedChange ? /* @__PURE__ */ (0, import_jsx_runtime39.jsx)(
3130
- import_ui17.IconButton,
3131
- {
3132
- "aria-label": expanded ? "Shrink panel" : "Extend panel",
3133
- style: { color: "inherit" },
3134
- onClick: () => onExpandedChange(!expanded),
3135
- children: expanded ? /* @__PURE__ */ (0, import_jsx_runtime39.jsx)(import_lucide_react13.Minimize2, { "aria-hidden": "true", className: "h-4 w-4" }) : /* @__PURE__ */ (0, import_jsx_runtime39.jsx)(import_lucide_react13.Maximize2, { "aria-hidden": "true", className: "h-4 w-4" })
3136
- }
3137
- ) : void 0,
3138
- onClose,
3139
- collapsed,
3140
- expanded,
3141
- onExpandedChange,
3142
- edgeTab,
3143
- children: [
3144
- /* @__PURE__ */ (0, import_jsx_runtime39.jsx)(
3145
- GenreInfoPanelBody,
3146
- {
3147
- family: /* @__PURE__ */ (0, import_jsx_runtime39.jsxs)(import_jsx_runtime39.Fragment, { children: [
3148
- family,
3149
- ancestors
3150
- ] }),
3151
- genreUuid: detail.uuid,
3152
- genrePlaylist: scope === "reference" ? import_genre_tree28.GenrePlaylistSummarySchema.parse(detail.criteriaPlaylist) : { ...detail.criteriaPlaylist, tracksCount: detail.tracksCount },
3153
- catalogue: detail.catalogue,
3154
- wikidataId: detail.wikidataId,
3155
- scope,
3156
- getBackendBaseUrl,
3157
- playback,
3158
- band: { node: view.node, onZoomIn, shareUrl, shareTarget: shareUrl ? shareTarget : void 0 },
3159
- extras
3160
- }
3161
- ),
3162
- !expanded && ancestors
3163
- ]
3164
- }
3165
- );
3166
- }
3167
-
3168
3205
  // src/genre-tree/index.ts
3169
3206
  var import_genre_tree_view6 = require("@behindthemusictree/genre-tree-view");
3170
3207
 
3171
3208
  // src/genre-tree/track-list-sidebar/TrackListSidebar.tsx
3172
- var import_react18 = require("react");
3209
+ var import_react19 = require("react");
3173
3210
  var import_lucide_react14 = require("lucide-react");
3174
3211
  var import_genre_tree29 = require("@behindthemusictree/app-kit/genre-tree");
3175
3212
  var import_genre_tree30 = require("@behindthemusictree/app-kit/genre-tree");
3176
3213
  var import_track_noun4 = require("@behindthemusictree/app-kit/track-noun");
3177
- var import_jsx_runtime40 = require("react/jsx-runtime");
3178
- var useBrowserLayoutEffect = typeof window === "undefined" ? import_react18.useEffect : import_react18.useLayoutEffect;
3214
+ var import_jsx_runtime41 = require("react/jsx-runtime");
3215
+ var useBrowserLayoutEffect = typeof window === "undefined" ? import_react19.useEffect : import_react19.useLayoutEffect;
3179
3216
  function TrackListSidebar({
3180
3217
  className: className2 = "",
3181
3218
  renderDuration,
@@ -3189,16 +3226,16 @@ function TrackListSidebar({
3189
3226
  }) {
3190
3227
  const { trackList, loadMore, autoAdvanceScrollRequest } = (0, import_genre_tree29.useTrackList)();
3191
3228
  const trackNoun = (0, import_track_noun4.useTrackNoun)();
3192
- const trackListRef = (0, import_react18.useRef)(null);
3193
- const previousOriginRef = (0, import_react18.useRef)(trackList?.origin ?? null);
3194
- const handledAutoAdvanceRequestIdRef = (0, import_react18.useRef)(null);
3195
- const shouldFollowPlaybackRef = (0, import_react18.useRef)(true);
3196
- const programmaticScrollTopRef = (0, import_react18.useRef)(null);
3197
- const userScrollIntentRef = (0, import_react18.useRef)(false);
3198
- const userScrollIntentTimerRef = (0, import_react18.useRef)(null);
3199
- const loadMoreSentinelRef = (0, import_react18.useRef)(null);
3229
+ const trackListRef = (0, import_react19.useRef)(null);
3230
+ const previousOriginRef = (0, import_react19.useRef)(trackList?.origin ?? null);
3231
+ const handledAutoAdvanceRequestIdRef = (0, import_react19.useRef)(null);
3232
+ const shouldFollowPlaybackRef = (0, import_react19.useRef)(true);
3233
+ const programmaticScrollTopRef = (0, import_react19.useRef)(null);
3234
+ const userScrollIntentRef = (0, import_react19.useRef)(false);
3235
+ const userScrollIntentTimerRef = (0, import_react19.useRef)(null);
3236
+ const loadMoreSentinelRef = (0, import_react19.useRef)(null);
3200
3237
  const hasMore = !!trackList && trackList.nextPage !== null;
3201
- (0, import_react18.useEffect)(() => {
3238
+ (0, import_react19.useEffect)(() => {
3202
3239
  const sentinel = loadMoreSentinelRef.current;
3203
3240
  if (!hasMore || !sentinel) return;
3204
3241
  const observer = new IntersectionObserver((entries) => {
@@ -3218,11 +3255,11 @@ function TrackListSidebar({
3218
3255
  programmaticScrollTopRef.current = null;
3219
3256
  if (trackListRef.current) trackListRef.current.scrollTop = 0;
3220
3257
  }, [trackList?.origin]);
3221
- (0, import_react18.useEffect)(() => {
3258
+ (0, import_react19.useEffect)(() => {
3222
3259
  const list = trackListRef.current;
3223
3260
  if (!isCollapsed && list?.scrollTop === 0) shouldFollowPlaybackRef.current = true;
3224
3261
  }, [isCollapsed]);
3225
- (0, import_react18.useEffect)(
3262
+ (0, import_react19.useEffect)(
3226
3263
  () => () => {
3227
3264
  if (userScrollIntentTimerRef.current !== null) clearTimeout(userScrollIntentTimerRef.current);
3228
3265
  },
@@ -3276,25 +3313,25 @@ function TrackListSidebar({
3276
3313
  }
3277
3314
  };
3278
3315
  const positionClasses = layout === "inline" ? "relative w-full flex h-full flex-col" : "fixed right-0 w-[50rem] flex flex-col";
3279
- return trackList ? /* @__PURE__ */ (0, import_jsx_runtime40.jsxs)(
3316
+ return trackList ? /* @__PURE__ */ (0, import_jsx_runtime41.jsxs)(
3280
3317
  "div",
3281
3318
  {
3282
3319
  "data-theme": "dark",
3283
3320
  className: `track-list-sidebar ${positionClasses} rounded-2xl bg-[var(--color-surface)] pb-1 ${className2}`,
3284
3321
  style: layout === "fixed" ? { top: "63.5px", maxHeight: "calc(100vh - 63.5px - 79px)" } : void 0,
3285
3322
  children: [
3286
- /* @__PURE__ */ (0, import_jsx_runtime40.jsxs)("div", { className: "header flex h-16 items-center px-4 py-2 text-[var(--color-text)]", children: [
3287
- /* @__PURE__ */ (0, import_jsx_runtime40.jsxs)("div", { className: "min-w-0 flex flex-1 flex-col justify-center", children: [
3288
- /* @__PURE__ */ (0, import_jsx_runtime40.jsxs)("div", { className: "origin flex text-xl", children: [
3289
- /* @__PURE__ */ (0, import_jsx_runtime40.jsx)("div", { className: "from h-auto flex flex-col justify-center items-center mr-2", children: "From" }),
3290
- /* @__PURE__ */ (0, import_jsx_runtime40.jsx)("div", { className: "name-container flex flex-col justify-center items-center text-[var(--color-text)] font-bold pr-2 max-w-[27.5rem]", children: /* @__PURE__ */ (0, import_jsx_runtime40.jsx)("div", { className: "name max-w-full truncate", children: trackList.origin.label }) })
3323
+ /* @__PURE__ */ (0, import_jsx_runtime41.jsxs)("div", { className: "header flex h-16 items-center px-4 py-2 text-[var(--color-text)]", children: [
3324
+ /* @__PURE__ */ (0, import_jsx_runtime41.jsxs)("div", { className: "min-w-0 flex flex-1 flex-col justify-center", children: [
3325
+ /* @__PURE__ */ (0, import_jsx_runtime41.jsxs)("div", { className: "origin flex text-xl", children: [
3326
+ /* @__PURE__ */ (0, import_jsx_runtime41.jsx)("div", { className: "from h-auto flex flex-col justify-center items-center mr-2", children: "From" }),
3327
+ /* @__PURE__ */ (0, import_jsx_runtime41.jsx)("div", { className: "name-container flex flex-col justify-center items-center text-[var(--color-text)] font-bold pr-2 max-w-[27.5rem]", children: /* @__PURE__ */ (0, import_jsx_runtime41.jsx)("div", { className: "name max-w-full truncate", children: trackList.origin.label }) })
3291
3328
  ] }),
3292
- /* @__PURE__ */ (0, import_jsx_runtime40.jsxs)("div", { className: "info flex items-center text-sm text-[var(--color-text-muted)]", children: [
3329
+ /* @__PURE__ */ (0, import_jsx_runtime41.jsxs)("div", { className: "info flex items-center text-sm text-[var(--color-text-muted)]", children: [
3293
3330
  trackList.origin.type === import_genre_tree30.TrackListOriginType.GENRE_PLAYLIST ? "Genre \u2022 " : trackList.origin.type === import_genre_tree30.TrackListOriginType.ARTIST ? "Artist \u2022 " : `${trackNoun.singular} playlist \u2022 `,
3294
3331
  `${trackList.total} ${trackList.total > 1 ? trackNoun.plural : trackNoun.singular}`
3295
3332
  ] })
3296
3333
  ] }),
3297
- (isCollapsed && onExpand || !isCollapsed && onMinimize) && /* @__PURE__ */ (0, import_jsx_runtime40.jsx)(
3334
+ (isCollapsed && onExpand || !isCollapsed && onMinimize) && /* @__PURE__ */ (0, import_jsx_runtime41.jsx)(
3298
3335
  "button",
3299
3336
  {
3300
3337
  type: "button",
@@ -3303,11 +3340,11 @@ function TrackListSidebar({
3303
3340
  "aria-expanded": !isCollapsed,
3304
3341
  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"}`,
3305
3342
  onClick: isCollapsed ? onExpand : onMinimize,
3306
- children: /* @__PURE__ */ (0, import_jsx_runtime40.jsx)(import_lucide_react14.ListMusic, { "aria-hidden": "true", className: "h-5 w-5" })
3343
+ children: /* @__PURE__ */ (0, import_jsx_runtime41.jsx)(import_lucide_react14.ListMusic, { "aria-hidden": "true", className: "h-5 w-5" })
3307
3344
  }
3308
3345
  )
3309
3346
  ] }),
3310
- !isCollapsed && /* @__PURE__ */ (0, import_jsx_runtime40.jsxs)(
3347
+ !isCollapsed && /* @__PURE__ */ (0, import_jsx_runtime41.jsxs)(
3311
3348
  "ul",
3312
3349
  {
3313
3350
  ref: trackListRef,
@@ -3317,7 +3354,7 @@ function TrackListSidebar({
3317
3354
  onPointerDown: markUserScrollIntent,
3318
3355
  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",
3319
3356
  children: [
3320
- trackList.tracks.map((track, index) => /* @__PURE__ */ (0, import_jsx_runtime40.jsx)("li", { "data-track-uuid": track.uuid, children: /* @__PURE__ */ (0, import_jsx_runtime40.jsx)(
3357
+ trackList.tracks.map((track, index) => /* @__PURE__ */ (0, import_jsx_runtime41.jsx)("li", { "data-track-uuid": track.uuid, children: /* @__PURE__ */ (0, import_jsx_runtime41.jsx)(
3321
3358
  TrackItem,
3322
3359
  {
3323
3360
  track,
@@ -3328,7 +3365,7 @@ function TrackListSidebar({
3328
3365
  renderSongLink
3329
3366
  }
3330
3367
  ) }, track.uuid)),
3331
- hasMore && /* @__PURE__ */ (0, import_jsx_runtime40.jsx)("li", { ref: loadMoreSentinelRef, "aria-hidden": "true", className: "h-px" })
3368
+ hasMore && /* @__PURE__ */ (0, import_jsx_runtime41.jsx)("li", { ref: loadMoreSentinelRef, "aria-hidden": "true", className: "h-px" })
3332
3369
  ]
3333
3370
  }
3334
3371
  )
@@ -3338,7 +3375,7 @@ function TrackListSidebar({
3338
3375
  }
3339
3376
 
3340
3377
  // src/genre-tree/PlayerPanel.tsx
3341
- var import_react19 = require("react");
3378
+ var import_react20 = require("react");
3342
3379
  var import_tailwind_merge = require("tailwind-merge");
3343
3380
  var import_player13 = require("@behindthemusictree/app-kit/player");
3344
3381
  var import_genre_tree32 = require("@behindthemusictree/app-kit/genre-tree");
@@ -3348,7 +3385,7 @@ var import_ui18 = require("@behindthemusictree/ui");
3348
3385
  var import_lucide_react15 = require("lucide-react");
3349
3386
  var import_genre_tree31 = require("@behindthemusictree/app-kit/genre-tree");
3350
3387
  var import_player12 = require("@behindthemusictree/app-kit/player");
3351
- var import_jsx_runtime41 = require("react/jsx-runtime");
3388
+ var import_jsx_runtime42 = require("react/jsx-runtime");
3352
3389
  function PlayerControls() {
3353
3390
  const {
3354
3391
  duration,
@@ -3367,15 +3404,15 @@ function PlayerControls() {
3367
3404
  const safeDuration = Number.isFinite(duration) && duration > 0 ? duration : 0;
3368
3405
  const safeCurrentTime = Number.isFinite(currentTime) ? Math.max(0, Math.min(currentTime, safeDuration)) : 0;
3369
3406
  const isPlaying = playState === import_player12.PlayStates.PLAYING;
3370
- return /* @__PURE__ */ (0, import_jsx_runtime41.jsxs)(
3407
+ return /* @__PURE__ */ (0, import_jsx_runtime42.jsxs)(
3371
3408
  "section",
3372
3409
  {
3373
3410
  "aria-label": "Player controls",
3374
3411
  className: "player-controls flex shrink-0 flex-col gap-2 border-b border-zinc-800 px-3 py-2 text-zinc-200",
3375
3412
  children: [
3376
- /* @__PURE__ */ (0, import_jsx_runtime41.jsxs)("div", { className: "flex min-w-0 items-center gap-2 text-xs tabular-nums text-zinc-400", children: [
3377
- /* @__PURE__ */ (0, import_jsx_runtime41.jsx)("span", { "aria-label": "Elapsed time", children: (0, import_genre_tree31.formatTime)(safeCurrentTime) }),
3378
- /* @__PURE__ */ (0, import_jsx_runtime41.jsx)(
3413
+ /* @__PURE__ */ (0, import_jsx_runtime42.jsxs)("div", { className: "flex min-w-0 items-center gap-2 text-xs tabular-nums text-zinc-400", children: [
3414
+ /* @__PURE__ */ (0, import_jsx_runtime42.jsx)("span", { "aria-label": "Elapsed time", children: (0, import_genre_tree31.formatTime)(safeCurrentTime) }),
3415
+ /* @__PURE__ */ (0, import_jsx_runtime42.jsx)(
3379
3416
  "input",
3380
3417
  {
3381
3418
  "aria-label": "Seek",
@@ -3389,11 +3426,11 @@ function PlayerControls() {
3389
3426
  onChange: (event) => seekTo(Number(event.currentTarget.value))
3390
3427
  }
3391
3428
  ),
3392
- /* @__PURE__ */ (0, import_jsx_runtime41.jsx)("span", { "aria-label": "Duration", children: (0, import_genre_tree31.formatTime)(safeDuration) })
3429
+ /* @__PURE__ */ (0, import_jsx_runtime42.jsx)("span", { "aria-label": "Duration", children: (0, import_genre_tree31.formatTime)(safeDuration) })
3393
3430
  ] }),
3394
- /* @__PURE__ */ (0, import_jsx_runtime41.jsxs)("div", { className: "grid min-h-9 grid-cols-[1fr_auto_1fr] items-center gap-3", children: [
3395
- /* @__PURE__ */ (0, import_jsx_runtime41.jsxs)("div", { className: "col-start-2 flex items-center justify-center gap-2", children: [
3396
- /* @__PURE__ */ (0, import_jsx_runtime41.jsx)(
3431
+ /* @__PURE__ */ (0, import_jsx_runtime42.jsxs)("div", { className: "grid min-h-9 grid-cols-[1fr_auto_1fr] items-center gap-3", children: [
3432
+ /* @__PURE__ */ (0, import_jsx_runtime42.jsxs)("div", { className: "col-start-2 flex items-center justify-center gap-2", children: [
3433
+ /* @__PURE__ */ (0, import_jsx_runtime42.jsx)(
3397
3434
  "button",
3398
3435
  {
3399
3436
  type: "button",
@@ -3402,10 +3439,10 @@ function PlayerControls() {
3402
3439
  disabled: !isReady || !hasPreviousTrack,
3403
3440
  onClick: playPreviousTrack,
3404
3441
  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",
3405
- children: /* @__PURE__ */ (0, import_jsx_runtime41.jsx)(import_lucide_react15.SkipBack, { "aria-hidden": "true", className: "h-4 w-4" })
3442
+ children: /* @__PURE__ */ (0, import_jsx_runtime42.jsx)(import_lucide_react15.SkipBack, { "aria-hidden": "true", className: "h-4 w-4" })
3406
3443
  }
3407
3444
  ),
3408
- /* @__PURE__ */ (0, import_jsx_runtime41.jsx)(
3445
+ /* @__PURE__ */ (0, import_jsx_runtime42.jsx)(
3409
3446
  "button",
3410
3447
  {
3411
3448
  type: "button",
@@ -3414,10 +3451,10 @@ function PlayerControls() {
3414
3451
  disabled: !isReady,
3415
3452
  onClick: handlePlayPauseAction,
3416
3453
  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",
3417
- children: isPlaying ? /* @__PURE__ */ (0, import_jsx_runtime41.jsx)(import_lucide_react15.Pause, { "aria-hidden": "true", className: "h-4 w-4", fill: "currentColor" }) : /* @__PURE__ */ (0, import_jsx_runtime41.jsx)(import_lucide_react15.Play, { "aria-hidden": "true", className: "h-4 w-4", fill: "currentColor" })
3454
+ children: isPlaying ? /* @__PURE__ */ (0, import_jsx_runtime42.jsx)(import_lucide_react15.Pause, { "aria-hidden": "true", className: "h-4 w-4", fill: "currentColor" }) : /* @__PURE__ */ (0, import_jsx_runtime42.jsx)(import_lucide_react15.Play, { "aria-hidden": "true", className: "h-4 w-4", fill: "currentColor" })
3418
3455
  }
3419
3456
  ),
3420
- /* @__PURE__ */ (0, import_jsx_runtime41.jsx)(
3457
+ /* @__PURE__ */ (0, import_jsx_runtime42.jsx)(
3421
3458
  "button",
3422
3459
  {
3423
3460
  type: "button",
@@ -3426,14 +3463,14 @@ function PlayerControls() {
3426
3463
  disabled: !isReady || !hasNextTrack,
3427
3464
  onClick: playNextTrack,
3428
3465
  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",
3429
- children: /* @__PURE__ */ (0, import_jsx_runtime41.jsx)(import_lucide_react15.SkipForward, { "aria-hidden": "true", className: "h-4 w-4" })
3466
+ children: /* @__PURE__ */ (0, import_jsx_runtime42.jsx)(import_lucide_react15.SkipForward, { "aria-hidden": "true", className: "h-4 w-4" })
3430
3467
  }
3431
3468
  )
3432
3469
  ] }),
3433
- /* @__PURE__ */ (0, import_jsx_runtime41.jsxs)("label", { className: "col-start-3 flex w-[4.2rem] shrink-0 items-center gap-[0.35rem] justify-self-end text-zinc-400 sm:w-[4.9rem]", children: [
3434
- volume === 0 ? /* @__PURE__ */ (0, import_jsx_runtime41.jsx)(import_lucide_react15.VolumeX, { "aria-hidden": "true", className: "h-[0.7rem] w-[0.7rem] shrink-0" }) : /* @__PURE__ */ (0, import_jsx_runtime41.jsx)(import_lucide_react15.Volume2, { "aria-hidden": "true", className: "h-[0.7rem] w-[0.7rem] shrink-0" }),
3435
- /* @__PURE__ */ (0, import_jsx_runtime41.jsx)("span", { className: "sr-only", children: "Volume" }),
3436
- /* @__PURE__ */ (0, import_jsx_runtime41.jsx)(
3470
+ /* @__PURE__ */ (0, import_jsx_runtime42.jsxs)("label", { className: "col-start-3 flex w-[4.2rem] shrink-0 items-center gap-[0.35rem] justify-self-end text-zinc-400 sm:w-[4.9rem]", children: [
3471
+ volume === 0 ? /* @__PURE__ */ (0, import_jsx_runtime42.jsx)(import_lucide_react15.VolumeX, { "aria-hidden": "true", className: "h-[0.7rem] w-[0.7rem] shrink-0" }) : /* @__PURE__ */ (0, import_jsx_runtime42.jsx)(import_lucide_react15.Volume2, { "aria-hidden": "true", className: "h-[0.7rem] w-[0.7rem] shrink-0" }),
3472
+ /* @__PURE__ */ (0, import_jsx_runtime42.jsx)("span", { className: "sr-only", children: "Volume" }),
3473
+ /* @__PURE__ */ (0, import_jsx_runtime42.jsx)(
3437
3474
  "input",
3438
3475
  {
3439
3476
  "aria-label": "Volume",
@@ -3455,7 +3492,7 @@ function PlayerControls() {
3455
3492
  }
3456
3493
 
3457
3494
  // src/genre-tree/PlayerPanel.tsx
3458
- var import_jsx_runtime42 = require("react/jsx-runtime");
3495
+ var import_jsx_runtime43 = require("react/jsx-runtime");
3459
3496
  var PANEL_ID = "player-panel";
3460
3497
  var DEFAULT_EXTENDED_WIDTH = "calc(100vw - var(--layout-center-left) - var(--layout-gutter))";
3461
3498
  function PlayerPanel({
@@ -3469,12 +3506,12 @@ function PlayerPanel({
3469
3506
  }) {
3470
3507
  const { playerTrackObject, retryPlayback } = (0, import_player13.usePlayer)();
3471
3508
  const { toggleTrackListSidebar, isTrackListSidebarVisible } = (0, import_genre_tree32.useTrackListSidebarVisibility)();
3472
- const [lastLoadedTrackTitle, setLastLoadedTrackTitle] = (0, import_react19.useState)("");
3509
+ const [lastLoadedTrackTitle, setLastLoadedTrackTitle] = (0, import_react20.useState)("");
3473
3510
  const loadedTrackTitle = playerTrackObject?.track.title;
3474
- (0, import_react19.useEffect)(() => {
3511
+ (0, import_react20.useEffect)(() => {
3475
3512
  if (loadedTrackTitle) setLastLoadedTrackTitle(loadedTrackTitle);
3476
3513
  }, [loadedTrackTitle]);
3477
- const [uncontrolledExtended, setUncontrolledExtended] = (0, import_react19.useState)(false);
3514
+ const [uncontrolledExtended, setUncontrolledExtended] = (0, import_react20.useState)(false);
3478
3515
  const isExtendRequested = expanded ?? uncontrolledExtended;
3479
3516
  const setIsExtendRequested = (next) => {
3480
3517
  setUncontrolledExtended(next);
@@ -3495,7 +3532,7 @@ function PlayerPanel({
3495
3532
  onClick: () => setIsExtendRequested(true),
3496
3533
  expanded: false
3497
3534
  } : null;
3498
- return /* @__PURE__ */ (0, import_jsx_runtime42.jsxs)(
3535
+ return /* @__PURE__ */ (0, import_jsx_runtime43.jsxs)(
3499
3536
  "div",
3500
3537
  {
3501
3538
  id: PANEL_ID,
@@ -3508,7 +3545,7 @@ function PlayerPanel({
3508
3545
  ),
3509
3546
  style: isExtended ? { width: extendedWidth } : void 0,
3510
3547
  children: [
3511
- /* @__PURE__ */ (0, import_jsx_runtime42.jsx)(
3548
+ /* @__PURE__ */ (0, import_jsx_runtime43.jsx)(
3512
3549
  import_ui18.PanelEdgeTab,
3513
3550
  {
3514
3551
  side: "right",
@@ -3518,7 +3555,7 @@ function PlayerPanel({
3518
3555
  className: "absolute right-full top-1/2 -translate-y-1/2"
3519
3556
  }
3520
3557
  ),
3521
- /* @__PURE__ */ (0, import_jsx_runtime42.jsxs)(
3558
+ /* @__PURE__ */ (0, import_jsx_runtime43.jsxs)(
3522
3559
  "div",
3523
3560
  {
3524
3561
  className: (0, import_tailwind_merge.twMerge)(
@@ -3526,20 +3563,20 @@ function PlayerPanel({
3526
3563
  isExtended ? "flex-row" : "flex-col"
3527
3564
  ),
3528
3565
  children: [
3529
- /* @__PURE__ */ (0, import_jsx_runtime42.jsxs)("div", { className: "flex w-[var(--layout-right-w)] shrink-0 flex-col", children: [
3530
- /* @__PURE__ */ (0, import_jsx_runtime42.jsx)("div", { className: "flex shrink-0 items-center justify-between gap-2 border-b border-zinc-800 p-2", children: /* @__PURE__ */ (0, import_jsx_runtime42.jsx)("span", { className: "min-w-0 truncate text-sm font-semibold", children: loadedTrackTitle || lastLoadedTrackTitle }) }),
3531
- /* @__PURE__ */ (0, import_jsx_runtime42.jsxs)("div", { className: "relative w-full shrink-0", children: [
3532
- /* @__PURE__ */ (0, import_jsx_runtime42.jsx)(import_player13.PlayerVideoSurface, { className: "aspect-video w-full bg-black" }),
3533
- playerTrackObject?.loadError && /* @__PURE__ */ (0, import_jsx_runtime42.jsxs)("span", { className: "absolute bottom-2 left-2 right-2 text-xs text-red-400", children: [
3566
+ /* @__PURE__ */ (0, import_jsx_runtime43.jsxs)("div", { className: "flex w-[var(--layout-right-w)] shrink-0 flex-col", children: [
3567
+ /* @__PURE__ */ (0, import_jsx_runtime43.jsx)("div", { className: "flex shrink-0 items-center justify-between gap-2 border-b border-zinc-800 p-2", children: /* @__PURE__ */ (0, import_jsx_runtime43.jsx)("span", { className: "min-w-0 truncate text-sm font-semibold", children: loadedTrackTitle || lastLoadedTrackTitle }) }),
3568
+ /* @__PURE__ */ (0, import_jsx_runtime43.jsxs)("div", { className: "relative w-full shrink-0", children: [
3569
+ /* @__PURE__ */ (0, import_jsx_runtime43.jsx)(import_player13.PlayerVideoSurface, { className: "aspect-video w-full bg-black" }),
3570
+ playerTrackObject?.loadError && /* @__PURE__ */ (0, import_jsx_runtime43.jsxs)("span", { className: "absolute bottom-2 left-2 right-2 text-xs text-red-400", children: [
3534
3571
  playerTrackObject.loadError,
3535
- /* @__PURE__ */ (0, import_jsx_runtime42.jsx)("button", { type: "button", className: "ml-2 underline", onClick: () => {
3572
+ /* @__PURE__ */ (0, import_jsx_runtime43.jsx)("button", { type: "button", className: "ml-2 underline", onClick: () => {
3536
3573
  void retryPlayback();
3537
3574
  }, children: "Retry" })
3538
3575
  ] })
3539
3576
  ] }),
3540
- /* @__PURE__ */ (0, import_jsx_runtime42.jsx)(PlayerControls, {})
3577
+ /* @__PURE__ */ (0, import_jsx_runtime43.jsx)(PlayerControls, {})
3541
3578
  ] }),
3542
- /* @__PURE__ */ (0, import_jsx_runtime42.jsx)(
3579
+ /* @__PURE__ */ (0, import_jsx_runtime43.jsx)(
3543
3580
  TrackListSidebar,
3544
3581
  {
3545
3582
  layout: "inline",
@@ -3566,11 +3603,11 @@ function PlayerPanel({
3566
3603
  var import_uuid = require("uuid");
3567
3604
  var import_ai = require("react-icons/ai");
3568
3605
  var import_genre_tree33 = require("@behindthemusictree/app-kit/genre-tree");
3569
- var import_jsx_runtime43 = require("react/jsx-runtime");
3606
+ var import_jsx_runtime44 = require("react/jsx-runtime");
3570
3607
  function Rating({ rating, handleChange }) {
3571
3608
  const instanceId = (0, import_uuid.v4)();
3572
- return /* @__PURE__ */ (0, import_jsx_runtime43.jsx)("div", { className: "flex items-center", children: [...Array(6)].map((_, i) => /* @__PURE__ */ (0, import_jsx_runtime43.jsxs)("div", { className: `relative ${i === 0 ? "mr-4" : "mr-1"}`, children: [
3573
- /* @__PURE__ */ (0, import_jsx_runtime43.jsx)(
3609
+ return /* @__PURE__ */ (0, import_jsx_runtime44.jsx)("div", { className: "flex items-center", children: [...Array(6)].map((_, i) => /* @__PURE__ */ (0, import_jsx_runtime44.jsxs)("div", { className: `relative ${i === 0 ? "mr-4" : "mr-1"}`, children: [
3610
+ /* @__PURE__ */ (0, import_jsx_runtime44.jsx)(
3574
3611
  "input",
3575
3612
  {
3576
3613
  id: `star-${instanceId}-${i}`,
@@ -3582,16 +3619,16 @@ function Rating({ rating, handleChange }) {
3582
3619
  onChange: handleChange
3583
3620
  }
3584
3621
  ),
3585
- /* @__PURE__ */ (0, import_jsx_runtime43.jsx)("label", { htmlFor: `star-${instanceId}-${i}`, className: "cursor-pointer", children: i === 0 ? /* @__PURE__ */ (0, import_jsx_runtime43.jsxs)("div", { className: "flex", children: [
3586
- /* @__PURE__ */ (0, import_jsx_runtime43.jsx)(
3622
+ /* @__PURE__ */ (0, import_jsx_runtime44.jsx)("label", { htmlFor: `star-${instanceId}-${i}`, className: "cursor-pointer", children: i === 0 ? /* @__PURE__ */ (0, import_jsx_runtime44.jsxs)("div", { className: "flex", children: [
3623
+ /* @__PURE__ */ (0, import_jsx_runtime44.jsx)(
3587
3624
  import_ai.AiOutlineStar,
3588
3625
  {
3589
3626
  className: `w-7 h-7 absolute top-0 left-0 ${rating === import_genre_tree33.FORM_RATING_NULL_VALUE ? "text-[var(--color-accent-rating)]" : "text-[var(--color-text-muted)]"}`
3590
3627
  }
3591
3628
  ),
3592
- /* @__PURE__ */ (0, import_jsx_runtime43.jsx)("span", { className: "flex-grow w-8" }),
3629
+ /* @__PURE__ */ (0, import_jsx_runtime44.jsx)("span", { className: "flex-grow w-8" }),
3593
3630
  " "
3594
- ] }) : /* @__PURE__ */ (0, import_jsx_runtime43.jsx)(
3631
+ ] }) : /* @__PURE__ */ (0, import_jsx_runtime44.jsx)(
3595
3632
  import_ai.AiFillStar,
3596
3633
  {
3597
3634
  className: `w-7 h-7 absolute top-0 left-0 ${(rating ?? -1) >= i ? "text-[var(--color-accent-rating)]" : "text-[var(--color-text-muted)]"}`