@behindthemusictree/app-kit-ui 26.1.0 → 29.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.cjs CHANGED
@@ -24,13 +24,16 @@ __export(src_exports, {
24
24
  AuthErrorPopup: () => AuthErrorPopup,
25
25
  AuthPopup: () => AuthPopup,
26
26
  BasePopup: () => BasePopup,
27
+ GenreDetailPanel: () => GenreDetailPanel,
28
+ GenreInfoPanelBody: () => GenreInfoPanelBody,
29
+ GenreNodeActionBand: () => GenreNodeActionBand,
27
30
  GenrePlaylistTreePerRoot: () => GenrePlaylistTreePerRoot,
28
31
  GenreSearch: () => GenreSearch,
29
- GenreTreeOutlineSkeleton: () => import_genre_tree_view5.GenreTreeOutlineSkeleton,
30
- GenreTreeSkeleton: () => import_genre_tree_view5.GenreTreeSkeleton,
32
+ GenreTreeOutlineSkeleton: () => import_genre_tree_view6.GenreTreeOutlineSkeleton,
33
+ GenreTreeSkeleton: () => import_genre_tree_view6.GenreTreeSkeleton,
31
34
  GenreTreeView: () => GenreTreeView,
32
- GenreTreeViewSkeleton: () => import_genre_tree_view5.GenreTreeViewSkeleton,
33
- GenreTreeWheelSkeleton: () => import_genre_tree_view5.GenreTreeWheelSkeleton,
35
+ GenreTreeViewSkeleton: () => import_genre_tree_view6.GenreTreeViewSkeleton,
36
+ GenreTreeWheelSkeleton: () => import_genre_tree_view6.GenreTreeWheelSkeleton,
34
37
  InternalErrorPopup: () => InternalErrorPopup,
35
38
  NetworkErrorPopup: () => NetworkErrorPopup,
36
39
  PanelEdgeTab: () => PanelEdgeTab,
@@ -39,10 +42,13 @@ __export(src_exports, {
39
42
  PopupTitle: () => PopupTitle,
40
43
  Rating: () => Rating,
41
44
  SpotifyAuthErrorPopup: () => SpotifyAuthErrorPopup,
45
+ Toast: () => Toast,
42
46
  TrackItem: () => TrackItem,
43
47
  TrackListSidebar: () => TrackListSidebar,
44
48
  TrackPositionPlayPause: () => TrackPositionPlayPause,
45
- TrackUploadPopup: () => TrackUploadPopup
49
+ TrackUploadPopup: () => TrackUploadPopup,
50
+ useGenrePlaylistPlayback: () => useGenrePlaylistPlayback,
51
+ useLeftDetailPanelLayout: () => useLeftDetailPanelLayout
46
52
  });
47
53
  module.exports = __toCommonJS(src_exports);
48
54
 
@@ -50,29 +56,46 @@ module.exports = __toCommonJS(src_exports);
50
56
  var import_lucide_react = require("lucide-react");
51
57
  var import_ui = require("@behindthemusictree/ui");
52
58
  var import_jsx_runtime = require("react/jsx-runtime");
53
- function PopupTitle({ title, onClose, isDismissable, icon: Icon }) {
54
- return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "popup-title flex items-center justify-between bg-black p-4 rounded-t-lg", children: [
59
+ function PopupTitle({
60
+ title,
61
+ onClose,
62
+ isDismissable,
63
+ icon: Icon,
64
+ className: className2 = "",
65
+ titleClassName = "",
66
+ closeButtonClassName = ""
67
+ }) {
68
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: `popup-title flex items-center justify-between bg-black p-4 rounded-t-lg ${className2}`, children: [
55
69
  /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "flex items-center gap-2", children: [
56
70
  Icon && /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Icon, { className: "h-5 w-5 text-white" }),
57
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)("h3", { id: "popup-title", className: "text-white font-semibold", children: title })
71
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("h3", { id: "popup-title", className: `text-white font-semibold ${titleClassName}`, children: title })
58
72
  ] }),
59
- isDismissable && /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_ui.Button, { onClick: onClose, className: "h-8 w-8 text-white hover:text-white/80", "aria-label": "Close popup", children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_lucide_react.X, { className: "h-4 w-4" }) })
73
+ isDismissable && /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
74
+ import_ui.Button,
75
+ {
76
+ onClick: onClose,
77
+ className: `h-8 w-8 text-white hover:text-white/80 ${closeButtonClassName}`,
78
+ "aria-label": "Close popup",
79
+ children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_lucide_react.X, { className: "h-4 w-4" })
80
+ }
81
+ )
60
82
  ] });
61
83
  }
62
84
 
63
85
  // src/popup/PopupButtons.tsx
64
86
  var import_jsx_runtime2 = require("react/jsx-runtime");
65
- function PopupButtons({ children, alignment = "center", className = "" }) {
87
+ function PopupButtons({ children, alignment = "center", className: className2 = "" }) {
66
88
  const alignmentClasses = {
67
89
  left: "justify-start",
68
90
  center: "justify-center",
69
91
  right: "justify-end"
70
92
  };
71
- return /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("div", { className: `flex gap-3 ${alignmentClasses[alignment]} ${className}`, children });
93
+ return /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("div", { className: `flex gap-3 ${alignmentClasses[alignment]} ${className2}`, children });
72
94
  }
73
95
 
74
96
  // src/popup/BasePopup.tsx
75
97
  var import_react_dom = require("react-dom");
98
+ var import_react = require("react");
76
99
  var import_ui2 = require("@behindthemusictree/ui");
77
100
  var import_jsx_runtime3 = require("react/jsx-runtime");
78
101
  var typeClasses = {
@@ -96,8 +119,13 @@ function BasePopup(props) {
96
119
  type = "default",
97
120
  icon,
98
121
  isDismissable = true,
99
- className,
122
+ className: className2,
123
+ overlayClassName = "",
124
+ titleClassName = "",
125
+ titleTextClassName = "",
126
+ closeButtonClassName = "",
100
127
  fitContent = false,
128
+ contentClassName = "",
101
129
  showOkButton = false,
102
130
  showCancelButton = false,
103
131
  okButtonText = "OK",
@@ -106,8 +134,49 @@ function BasePopup(props) {
106
134
  okButtonDisabled = false,
107
135
  onCancel,
108
136
  buttonAlignment = "center",
109
- topOffset = 0
137
+ topOffset = 0,
138
+ trapFocus = false,
139
+ closeOnEscape = false,
140
+ closeOnBackdrop = false
110
141
  } = props;
142
+ const dialogRef = (0, import_react.useRef)(null);
143
+ (0, import_react.useEffect)(() => {
144
+ const previousFocus = document.activeElement instanceof HTMLElement ? document.activeElement : null;
145
+ const dialog = dialogRef.current;
146
+ const focusableSelector = 'a[href], button:not([disabled]), input:not([disabled]), select:not([disabled]), textarea:not([disabled]), [tabindex]:not([tabindex="-1"])';
147
+ const getFocusable = () => Array.from(dialog?.querySelectorAll(focusableSelector) ?? []).filter(
148
+ (element) => !element.hasAttribute("hidden") && element.getAttribute("aria-hidden") !== "true"
149
+ );
150
+ if (trapFocus) (getFocusable()[0] ?? dialog)?.focus();
151
+ const handleKeyDown = (event) => {
152
+ if (event.key === "Escape" && closeOnEscape && isDismissable) {
153
+ event.preventDefault();
154
+ onClose?.();
155
+ return;
156
+ }
157
+ if (!trapFocus || event.key !== "Tab") return;
158
+ const focusable = getFocusable();
159
+ if (focusable.length === 0) {
160
+ event.preventDefault();
161
+ dialog?.focus();
162
+ return;
163
+ }
164
+ const first = focusable[0];
165
+ const last = focusable[focusable.length - 1];
166
+ if (event.shiftKey && (document.activeElement === first || !dialog?.contains(document.activeElement))) {
167
+ event.preventDefault();
168
+ last.focus();
169
+ } else if (!event.shiftKey && (document.activeElement === last || !dialog?.contains(document.activeElement))) {
170
+ event.preventDefault();
171
+ first.focus();
172
+ }
173
+ };
174
+ if (trapFocus || closeOnEscape) document.addEventListener("keydown", handleKeyDown);
175
+ return () => {
176
+ if (trapFocus || closeOnEscape) document.removeEventListener("keydown", handleKeyDown);
177
+ if (trapFocus && previousFocus?.isConnected) previousFocus.focus();
178
+ };
179
+ }, [closeOnEscape, isDismissable, onClose, trapFocus]);
111
180
  const baseClasses = "bg-white rounded-lg";
112
181
  const renderButtons = () => {
113
182
  if (!showOkButton && !showCancelButton) return null;
@@ -121,7 +190,7 @@ function BasePopup(props) {
121
190
  const popupComponent = /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
122
191
  "div",
123
192
  {
124
- className: "fixed z-50 flex items-center justify-center bg-transparent",
193
+ className: `fixed z-50 flex items-center justify-center bg-transparent ${overlayClassName}`,
125
194
  style: {
126
195
  top: topOffset,
127
196
  left: 0,
@@ -131,15 +200,29 @@ function BasePopup(props) {
131
200
  role: "dialog",
132
201
  "aria-modal": "true",
133
202
  "aria-labelledby": "popup-title",
203
+ onClick: closeOnBackdrop && isDismissable ? onClose : void 0,
134
204
  children: /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(
135
205
  "div",
136
206
  {
137
- className: `${baseClasses} ${fitContent ? typeClasses[type].replace("w-full", "w-fit") : typeClasses[type]} ${className || ""}`,
207
+ ref: dialogRef,
208
+ tabIndex: trapFocus ? -1 : void 0,
209
+ className: `${baseClasses} ${fitContent ? typeClasses[type].replace("w-full", "w-fit") : typeClasses[type]} ${className2 || ""}`,
138
210
  onClick: (e) => e.stopPropagation(),
139
211
  children: [
140
- /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(PopupTitle, { title, onClose: onClose ?? (() => {
141
- }), isDismissable, icon }),
142
- /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { className: `popup-content rounded-b-lg p-4 ${contentBgClasses[type] ?? ""}`, children: [
212
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
213
+ PopupTitle,
214
+ {
215
+ title,
216
+ onClose: onClose ?? (() => {
217
+ }),
218
+ isDismissable,
219
+ icon,
220
+ className: titleClassName,
221
+ titleClassName: titleTextClassName,
222
+ closeButtonClassName
223
+ }
224
+ ),
225
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { className: `popup-content rounded-b-lg p-4 ${contentBgClasses[type] ?? ""} ${contentClassName}`, children: [
143
226
  children,
144
227
  renderButtons()
145
228
  ] })
@@ -152,21 +235,48 @@ function BasePopup(props) {
152
235
  }
153
236
  var BasePopup_default = BasePopup;
154
237
 
238
+ // src/popup/Toast.tsx
239
+ var import_react2 = require("react");
240
+ var import_react_dom2 = require("react-dom");
241
+ var import_jsx_runtime4 = require("react/jsx-runtime");
242
+ function Toast({ message, onClose, durationMs = 2200 }) {
243
+ const [mounted, setMounted] = (0, import_react2.useState)(false);
244
+ (0, import_react2.useEffect)(() => {
245
+ setMounted(true);
246
+ const timeout = window.setTimeout(onClose, durationMs);
247
+ return () => window.clearTimeout(timeout);
248
+ }, [durationMs, onClose]);
249
+ if (!mounted) return null;
250
+ return (0, import_react_dom2.createPortal)(
251
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("div", { className: "pointer-events-none fixed inset-x-0 bottom-8 z-[60] flex justify-center px-4", children: /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
252
+ "div",
253
+ {
254
+ role: "status",
255
+ "aria-live": "polite",
256
+ "aria-atomic": "true",
257
+ className: "pointer-events-auto w-full max-w-md rounded-md bg-[var(--color-surface)] px-4 py-4 text-sm text-[var(--color-text)] shadow-lg ring-1 ring-black/10",
258
+ children: message
259
+ }
260
+ ) }),
261
+ document.body
262
+ );
263
+ }
264
+
155
265
  // src/popup/TrackUploadPopup.tsx
156
266
  var import_md = require("react-icons/md");
157
267
  var import_popup = require("@behindthemusictree/app-kit/popup");
158
268
  var import_track_noun = require("@behindthemusictree/app-kit/track-noun");
159
- var import_jsx_runtime4 = require("react/jsx-runtime");
269
+ var import_jsx_runtime5 = require("react/jsx-runtime");
160
270
  function getStatusIcon(status) {
161
271
  switch (status) {
162
272
  case "success":
163
- return /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(import_md.MdCheckCircle, { className: "text-[var(--color-green-500)]", size: 20 });
273
+ return /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(import_md.MdCheckCircle, { className: "text-[var(--color-green-500)]", size: 20 });
164
274
  case "error":
165
- return /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(import_md.MdError, { className: "text-[var(--color-red-500)]", size: 20 });
275
+ return /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(import_md.MdError, { className: "text-[var(--color-red-500)]", size: 20 });
166
276
  case "uploading":
167
- return /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(import_md.MdUpload, { className: "text-[var(--color-blue-500)] animate-pulse", size: 20 });
277
+ return /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(import_md.MdUpload, { className: "text-[var(--color-blue-500)] animate-pulse", size: 20 });
168
278
  default:
169
- return /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("div", { className: "w-5 h-5 rounded-full border-2 border-[var(--color-neutral-300)]" });
279
+ return /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("div", { className: "w-5 h-5 rounded-full border-2 border-[var(--color-neutral-300)]" });
170
280
  }
171
281
  }
172
282
  function getStatusText(status) {
@@ -199,7 +309,7 @@ function TrackUploadPopup({
199
309
  });
200
310
  const displayTotal = items.length > 0 ? items.length : files.length;
201
311
  const trackNoun = (0, import_track_noun.useTrackNoun)();
202
- return /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
312
+ return /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(
203
313
  BasePopup,
204
314
  {
205
315
  ...rest,
@@ -210,31 +320,31 @@ function TrackUploadPopup({
210
320
  okButtonText: "OK",
211
321
  okButtonDisabled: isUploading || !allComplete,
212
322
  onOk: onClose,
213
- children: /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("div", { className: "space-y-4", children: [
214
- items.map((item) => /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("div", { className: "border rounded-lg p-4", children: [
215
- /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("div", { className: "flex items-center justify-between mb-2", children: [
216
- /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("div", { className: "flex items-center space-x-3", children: [
323
+ children: /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)("div", { className: "space-y-4", children: [
324
+ items.map((item) => /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)("div", { className: "border rounded-lg p-4", children: [
325
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)("div", { className: "flex items-center justify-between mb-2", children: [
326
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)("div", { className: "flex items-center space-x-3", children: [
217
327
  getStatusIcon(item.status),
218
- /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("div", { children: [
219
- /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("div", { className: "font-medium text-sm truncate max-w-xs", title: item.file.name, children: item.file.name }),
220
- /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("div", { className: "text-xs text-[var(--color-neutral-500)]", children: [
328
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)("div", { children: [
329
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("div", { className: "font-medium text-sm truncate max-w-xs", title: item.file.name, children: item.file.name }),
330
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)("div", { className: "text-xs text-[var(--color-neutral-500)]", children: [
221
331
  (item.file.size / 1024 / 1024).toFixed(1),
222
332
  " MB"
223
333
  ] })
224
334
  ] })
225
335
  ] }),
226
- /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("div", { className: "text-sm text-[var(--color-neutral-600)]", children: getStatusText(item.status) })
336
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("div", { className: "text-sm text-[var(--color-neutral-600)]", children: getStatusText(item.status) })
227
337
  ] }),
228
- item.status === "uploading" && /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("div", { className: "w-full bg-[var(--color-neutral-200)] rounded-full h-2", children: /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
338
+ item.status === "uploading" && /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("div", { className: "w-full bg-[var(--color-neutral-200)] rounded-full h-2", children: /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(
229
339
  "div",
230
340
  {
231
341
  className: "bg-[var(--color-blue-500)] h-2 rounded-full transition-all duration-300",
232
342
  style: { width: `${item.progress}%` }
233
343
  }
234
344
  ) }),
235
- item.status === "error" && item.error && /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("div", { className: "text-[var(--color-red-500)] text-sm mt-2", children: item.error.name === "InvalidInputError" ? "Upload failed due to invalid file data. Please check your file and try again." : item.error.name === "ZodError" ? "Upload failed due to a server error. Please try again later." : item.error.message })
345
+ item.status === "error" && item.error && /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("div", { className: "text-[var(--color-red-500)] text-sm mt-2", children: item.error.name === "InvalidInputError" ? "Upload failed due to invalid file data. Please check your file and try again." : item.error.name === "ZodError" ? "Upload failed due to a server error. Please try again later." : item.error.message })
236
346
  ] }, item.id)),
237
- allComplete && /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("div", { className: "flex justify-between items-center", children: /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("div", { className: "text-sm text-[var(--color-neutral-600)]", children: [
347
+ allComplete && /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("div", { className: "flex justify-between items-center", children: /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)("div", { className: "text-sm text-[var(--color-neutral-600)]", children: [
238
348
  successfulCount,
239
349
  " successful, ",
240
350
  errorCount,
@@ -248,18 +358,18 @@ function TrackUploadPopup({
248
358
  // src/popup/AuthErrorPopup.tsx
249
359
  var import_lucide_react2 = require("lucide-react");
250
360
  var import_ui3 = require("@behindthemusictree/ui");
251
- var import_jsx_runtime5 = require("react/jsx-runtime");
361
+ var import_jsx_runtime6 = require("react/jsx-runtime");
252
362
  function AuthErrorPopup({ message, onClose, ...rest }) {
253
- return /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(
363
+ return /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(
254
364
  BasePopup,
255
365
  {
256
366
  ...rest,
257
367
  title: "Sign-in error",
258
368
  isDismissable: true,
259
369
  icon: import_lucide_react2.AlertCircle,
260
- children: /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)("div", { className: "flex flex-col items-center space-y-6 py-4", children: [
261
- /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("p", { className: "text-center text-lg font-medium text-[var(--color-text)]", children: message }),
262
- /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(import_ui3.Button, { onClick: onClose, variant: "secondary", className: "w-full max-w-xs", children: "Close" })
370
+ children: /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)("div", { className: "flex flex-col items-center space-y-6 py-4", children: [
371
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("p", { className: "text-center text-lg font-medium text-[var(--color-text)]", children: message }),
372
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(import_ui3.Button, { onClick: onClose, variant: "secondary", className: "w-full max-w-xs", children: "Close" })
263
373
  ] })
264
374
  }
265
375
  );
@@ -267,23 +377,23 @@ function AuthErrorPopup({ message, onClose, ...rest }) {
267
377
 
268
378
  // src/popup/InternalErrorPopup.tsx
269
379
  var import_lucide_react3 = require("lucide-react");
270
- var import_jsx_runtime6 = require("react/jsx-runtime");
380
+ var import_jsx_runtime7 = require("react/jsx-runtime");
271
381
  function InternalErrorPopup({ errorCode, contactEmail, ...rest }) {
272
- return /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(
382
+ return /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(
273
383
  BasePopup,
274
384
  {
275
385
  ...rest,
276
386
  title: "Internal Error",
277
387
  isDismissable: false,
278
388
  icon: import_lucide_react3.AlertTriangle,
279
- children: /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)("div", { className: "flex flex-col items-center space-y-6 py-4", children: [
280
- /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(import_lucide_react3.AlertCircle, { className: "h-16 w-16 text-[var(--color-red-500)]", strokeWidth: 1.5 }),
281
- /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("div", { children: /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)("p", { className: "text-center text-[var(--color-neutral-600)]", children: [
389
+ children: /* @__PURE__ */ (0, import_jsx_runtime7.jsxs)("div", { className: "flex flex-col items-center space-y-6 py-4", children: [
390
+ /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(import_lucide_react3.AlertCircle, { className: "h-16 w-16 text-[var(--color-red-500)]", strokeWidth: 1.5 }),
391
+ /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("div", { children: /* @__PURE__ */ (0, import_jsx_runtime7.jsxs)("p", { className: "text-center text-[var(--color-neutral-600)]", children: [
282
392
  "Please try again. If the problem persists, contact us at",
283
393
  " ",
284
- /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("a", { href: `mailto:${contactEmail}`, className: "text-[var(--color-primary)] hover:text-[var(--color-primary-hover)]", children: contactEmail })
394
+ /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("a", { href: `mailto:${contactEmail}`, className: "text-[var(--color-primary)] hover:text-[var(--color-primary-hover)]", children: contactEmail })
285
395
  ] }) }),
286
- errorCode && /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)("div", { className: "text-sm text-[var(--color-neutral-500)] bg-[var(--color-neutral-100)] p-3 rounded-lg border border-[var(--color-neutral-100)]", children: [
396
+ errorCode && /* @__PURE__ */ (0, import_jsx_runtime7.jsxs)("div", { className: "text-sm text-[var(--color-neutral-500)] bg-[var(--color-neutral-100)] p-3 rounded-lg border border-[var(--color-neutral-100)]", children: [
287
397
  "Error Code: ",
288
398
  errorCode
289
399
  ] })
@@ -294,18 +404,18 @@ function InternalErrorPopup({ errorCode, contactEmail, ...rest }) {
294
404
 
295
405
  // src/popup/NetworkErrorPopup.tsx
296
406
  var import_lucide_react4 = require("lucide-react");
297
- var import_jsx_runtime7 = require("react/jsx-runtime");
407
+ var import_jsx_runtime8 = require("react/jsx-runtime");
298
408
  function NetworkErrorPopup(props) {
299
- return /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(
409
+ return /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(
300
410
  BasePopup,
301
411
  {
302
412
  ...props,
303
413
  title: "Network Error",
304
414
  isDismissable: false,
305
415
  icon: import_lucide_react4.AlertTriangle,
306
- children: /* @__PURE__ */ (0, import_jsx_runtime7.jsxs)("div", { className: "flex flex-col items-center space-y-6 py-4", children: [
307
- /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(import_lucide_react4.AlertTriangle, { className: "h-16 w-16 text-[var(--color-neutral-600)]", strokeWidth: 1.5 }),
308
- /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("p", { className: "text-center text-[var(--color-neutral-700)]", children: "It seems that you are not connected to the internet. Please check your connection and try again." })
416
+ children: /* @__PURE__ */ (0, import_jsx_runtime8.jsxs)("div", { className: "flex flex-col items-center space-y-6 py-4", children: [
417
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(import_lucide_react4.AlertTriangle, { className: "h-16 w-16 text-[var(--color-neutral-600)]", strokeWidth: 1.5 }),
418
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("p", { className: "text-center text-[var(--color-neutral-700)]", children: "It seems that you are not connected to the internet. Please check your connection and try again." })
309
419
  ] })
310
420
  }
311
421
  );
@@ -316,7 +426,7 @@ var import_lucide_react5 = require("lucide-react");
316
426
  var import_ui4 = require("@behindthemusictree/ui");
317
427
  var import_fa = require("react-icons/fa");
318
428
  var import_fc = require("react-icons/fc");
319
- var import_jsx_runtime8 = require("react/jsx-runtime");
429
+ var import_jsx_runtime9 = require("react/jsx-runtime");
320
430
  function AuthPopup({
321
431
  handleSpotifyOAuth,
322
432
  handleGoogleOAuth,
@@ -328,7 +438,7 @@ function AuthPopup({
328
438
  ...rest
329
439
  }) {
330
440
  const showGoogle = !spotifyOnly && handleGoogleOAuth;
331
- return /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(
441
+ return /* @__PURE__ */ (0, import_jsx_runtime9.jsx)(
332
442
  BasePopup,
333
443
  {
334
444
  ...rest,
@@ -336,32 +446,32 @@ function AuthPopup({
336
446
  isDismissable: false,
337
447
  icon: import_lucide_react5.User,
338
448
  type: "auth",
339
- children: /* @__PURE__ */ (0, import_jsx_runtime8.jsxs)("div", { className: "flex flex-col items-center space-y-7", children: [
340
- /* @__PURE__ */ (0, import_jsx_runtime8.jsxs)("div", { className: "px-2 text-center", children: [
341
- message && /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("p", { className: "mb-3 text-base font-medium leading-relaxed text-amber-200", children: message }),
342
- /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("p", { className: "text-lg font-medium leading-relaxed text-white", children: spotifyOnly ? spotifyOnlyDescription : defaultDescription })
449
+ children: /* @__PURE__ */ (0, import_jsx_runtime9.jsxs)("div", { className: "flex flex-col items-center space-y-7", children: [
450
+ /* @__PURE__ */ (0, import_jsx_runtime9.jsxs)("div", { className: "px-2 text-center", children: [
451
+ message && /* @__PURE__ */ (0, import_jsx_runtime9.jsx)("p", { className: "mb-3 text-base font-medium leading-relaxed text-amber-200", children: message }),
452
+ /* @__PURE__ */ (0, import_jsx_runtime9.jsx)("p", { className: "text-lg font-medium leading-relaxed text-white", children: spotifyOnly ? spotifyOnlyDescription : defaultDescription })
343
453
  ] }),
344
- /* @__PURE__ */ (0, import_jsx_runtime8.jsxs)("div", { className: "flex w-full flex-col items-center gap-3", children: [
345
- /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(
454
+ /* @__PURE__ */ (0, import_jsx_runtime9.jsxs)("div", { className: "flex w-full flex-col items-center gap-3", children: [
455
+ /* @__PURE__ */ (0, import_jsx_runtime9.jsx)(
346
456
  import_ui4.Button,
347
457
  {
348
458
  onClick: () => handleSpotifyOAuth(redirectAfterAuthPath),
349
459
  className: "relative mb-2 w-fit transform bg-green-600 px-8 py-3 text-white shadow-lg transition-all duration-200 hover:scale-[1.02] hover:bg-green-700 hover:shadow-xl",
350
- children: /* @__PURE__ */ (0, import_jsx_runtime8.jsxs)("span", { className: "flex items-center justify-center", children: [
351
- /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(import_fa.FaSpotify, { className: "absolute left-6 text-3xl" }),
352
- /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("span", { className: "w-full pl-14 pr-8 text-center text-lg font-medium", children: "Sign in with Spotify" })
460
+ children: /* @__PURE__ */ (0, import_jsx_runtime9.jsxs)("span", { className: "flex items-center justify-center", children: [
461
+ /* @__PURE__ */ (0, import_jsx_runtime9.jsx)(import_fa.FaSpotify, { className: "absolute left-6 text-3xl" }),
462
+ /* @__PURE__ */ (0, import_jsx_runtime9.jsx)("span", { className: "w-full pl-14 pr-8 text-center text-lg font-medium", children: "Sign in with Spotify" })
353
463
  ] })
354
464
  }
355
465
  ),
356
- showGoogle && /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(
466
+ showGoogle && /* @__PURE__ */ (0, import_jsx_runtime9.jsx)(
357
467
  import_ui4.Button,
358
468
  {
359
469
  variant: "outline",
360
470
  onClick: () => handleGoogleOAuth(redirectAfterAuthPath),
361
471
  className: "relative w-fit transform border-white bg-white px-8 py-3 text-black shadow-lg transition-all duration-200 hover:scale-[1.02] hover:bg-[var(--color-neutral-100)] hover:shadow-xl",
362
- children: /* @__PURE__ */ (0, import_jsx_runtime8.jsxs)("span", { className: "flex items-center justify-center", children: [
363
- /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(import_fc.FcGoogle, { className: "absolute left-6 text-2xl" }),
364
- /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("span", { className: "w-full pl-14 pr-8 text-center text-lg font-medium text-black", children: "Sign in with Google" })
472
+ children: /* @__PURE__ */ (0, import_jsx_runtime9.jsxs)("span", { className: "flex items-center justify-center", children: [
473
+ /* @__PURE__ */ (0, import_jsx_runtime9.jsx)(import_fc.FcGoogle, { className: "absolute left-6 text-2xl" }),
474
+ /* @__PURE__ */ (0, import_jsx_runtime9.jsx)("span", { className: "w-full pl-14 pr-8 text-center text-lg font-medium text-black", children: "Sign in with Google" })
365
475
  ] })
366
476
  }
367
477
  )
@@ -376,7 +486,7 @@ var import_fa2 = require("react-icons/fa");
376
486
  var import_ui5 = require("@behindthemusictree/ui");
377
487
  var import_lucide_react6 = require("lucide-react");
378
488
  var import_transport = require("@behindthemusictree/app-kit/transport");
379
- var import_jsx_runtime9 = require("react/jsx-runtime");
489
+ var import_jsx_runtime10 = require("react/jsx-runtime");
380
490
  function SpotifyAuthErrorPopup({
381
491
  message,
382
492
  details,
@@ -385,24 +495,24 @@ function SpotifyAuthErrorPopup({
385
495
  ...rest
386
496
  }) {
387
497
  const requestAccessHref = (0, import_transport.getSpotifyAllowlistMailtoHref)(contactEmail);
388
- return /* @__PURE__ */ (0, import_jsx_runtime9.jsx)(
498
+ return /* @__PURE__ */ (0, import_jsx_runtime10.jsx)(
389
499
  BasePopup,
390
500
  {
391
501
  ...rest,
392
502
  title: "Authentication Failed",
393
503
  isDismissable: true,
394
504
  icon: import_lucide_react6.User,
395
- children: /* @__PURE__ */ (0, import_jsx_runtime9.jsxs)("div", { className: "flex flex-col items-center space-y-8 py-4", children: [
396
- /* @__PURE__ */ (0, import_jsx_runtime9.jsx)(import_fa2.FaSpotify, { className: "text-[#1DB954] text-7xl" }),
397
- /* @__PURE__ */ (0, import_jsx_runtime9.jsxs)("div", { className: "space-y-3 text-center", children: [
398
- /* @__PURE__ */ (0, import_jsx_runtime9.jsx)("p", { className: "text-xl font-medium text-[var(--color-text)]", children: message }),
399
- details && /* @__PURE__ */ (0, import_jsx_runtime9.jsx)("p", { className: "text-sm text-[var(--color-neutral-500)] bg-[var(--color-neutral-100)] p-3 rounded-lg border border-[var(--color-neutral-100)]", children: details }),
400
- /* @__PURE__ */ (0, import_jsx_runtime9.jsxs)("div", { className: "space-y-2 text-center", children: [
401
- /* @__PURE__ */ (0, import_jsx_runtime9.jsx)("p", { className: "text-base text-[var(--color-neutral-700)]", children: "The Spotify app is in development mode, so only allowlisted accounts can connect." }),
402
- requestAccessHref && contactEmail ? /* @__PURE__ */ (0, import_jsx_runtime9.jsxs)("p", { className: "text-base text-[var(--color-neutral-700)]", children: [
505
+ children: /* @__PURE__ */ (0, import_jsx_runtime10.jsxs)("div", { className: "flex flex-col items-center space-y-8 py-4", children: [
506
+ /* @__PURE__ */ (0, import_jsx_runtime10.jsx)(import_fa2.FaSpotify, { className: "text-[#1DB954] text-7xl" }),
507
+ /* @__PURE__ */ (0, import_jsx_runtime10.jsxs)("div", { className: "space-y-3 text-center", children: [
508
+ /* @__PURE__ */ (0, import_jsx_runtime10.jsx)("p", { className: "text-xl font-medium text-[var(--color-text)]", children: message }),
509
+ details && /* @__PURE__ */ (0, import_jsx_runtime10.jsx)("p", { className: "text-sm text-[var(--color-neutral-500)] bg-[var(--color-neutral-100)] p-3 rounded-lg border border-[var(--color-neutral-100)]", children: details }),
510
+ /* @__PURE__ */ (0, import_jsx_runtime10.jsxs)("div", { className: "space-y-2 text-center", children: [
511
+ /* @__PURE__ */ (0, import_jsx_runtime10.jsx)("p", { className: "text-base text-[var(--color-neutral-700)]", children: "The Spotify app is in development mode, so only allowlisted accounts can connect." }),
512
+ requestAccessHref && contactEmail ? /* @__PURE__ */ (0, import_jsx_runtime10.jsxs)("p", { className: "text-base text-[var(--color-neutral-700)]", children: [
403
513
  "Request access by emailing your",
404
514
  " ",
405
- /* @__PURE__ */ (0, import_jsx_runtime9.jsx)(
515
+ /* @__PURE__ */ (0, import_jsx_runtime10.jsx)(
406
516
  "a",
407
517
  {
408
518
  href: requestAccessHref,
@@ -413,7 +523,7 @@ function SpotifyAuthErrorPopup({
413
523
  " ",
414
524
  "to",
415
525
  " ",
416
- /* @__PURE__ */ (0, import_jsx_runtime9.jsx)(
526
+ /* @__PURE__ */ (0, import_jsx_runtime10.jsx)(
417
527
  "a",
418
528
  {
419
529
  href: requestAccessHref,
@@ -422,16 +532,16 @@ function SpotifyAuthErrorPopup({
422
532
  }
423
533
  ),
424
534
  "."
425
- ] }) : /* @__PURE__ */ (0, import_jsx_runtime9.jsx)("p", { className: "text-base text-[var(--color-neutral-700)]", children: "To request access, contact the app owner." })
535
+ ] }) : /* @__PURE__ */ (0, import_jsx_runtime10.jsx)("p", { className: "text-base text-[var(--color-neutral-700)]", children: "To request access, contact the app owner." })
426
536
  ] })
427
537
  ] }),
428
- /* @__PURE__ */ (0, import_jsx_runtime9.jsxs)(
538
+ /* @__PURE__ */ (0, import_jsx_runtime10.jsxs)(
429
539
  import_ui5.Button,
430
540
  {
431
541
  onClick: onClose,
432
542
  className: "bg-[#1DB954] hover:bg-[#1DB954]/90 text-white w-full max-w-xs transform transition-all duration-200 hover:scale-[1.02] shadow-lg hover:shadow-xl",
433
543
  children: [
434
- /* @__PURE__ */ (0, import_jsx_runtime9.jsx)(import_fa2.FaSpotify, { className: "mr-2 text-lg" }),
544
+ /* @__PURE__ */ (0, import_jsx_runtime10.jsx)(import_fa2.FaSpotify, { className: "mr-2 text-lg" }),
435
545
  "Try Again"
436
546
  ]
437
547
  }
@@ -443,16 +553,16 @@ function SpotifyAuthErrorPopup({
443
553
 
444
554
  // src/auth/AuthCallbackHandler.tsx
445
555
  var import_auth = require("@behindthemusictree/app-kit/auth");
446
- var import_jsx_runtime10 = require("react/jsx-runtime");
556
+ var import_jsx_runtime11 = require("react/jsx-runtime");
447
557
  function AuthCallbackHandler({ renderStatus, ...options }) {
448
558
  const state = (0, import_auth.useAuthCallback)(options);
449
559
  if (state.status === null) return null;
450
- if (renderStatus) return /* @__PURE__ */ (0, import_jsx_runtime10.jsx)(import_jsx_runtime10.Fragment, { children: renderStatus(state) });
560
+ if (renderStatus) return /* @__PURE__ */ (0, import_jsx_runtime11.jsx)(import_jsx_runtime11.Fragment, { children: renderStatus(state) });
451
561
  if (state.status === "error") {
452
- return /* @__PURE__ */ (0, import_jsx_runtime10.jsx)("div", { className: "fixed inset-0 z-[100] flex flex-col items-center justify-center bg-[var(--color-neutral-100)] p-4", children: /* @__PURE__ */ (0, import_jsx_runtime10.jsxs)("div", { className: "w-full max-w-md rounded-lg border border-[color-mix(in_srgb,var(--color-red-500)_20%,transparent)] bg-[color-mix(in_srgb,var(--color-red-500)_10%,transparent)] p-4", children: [
453
- /* @__PURE__ */ (0, import_jsx_runtime10.jsx)("h2", { className: "mb-2 font-semibold text-[var(--color-red-500)]", children: "Authentication Error" }),
454
- /* @__PURE__ */ (0, import_jsx_runtime10.jsx)("p", { className: "text-[color-mix(in_srgb,var(--color-red-500)_80%,transparent)]", children: state.message }),
455
- /* @__PURE__ */ (0, import_jsx_runtime10.jsx)(
562
+ return /* @__PURE__ */ (0, import_jsx_runtime11.jsx)("div", { className: "fixed inset-0 z-[100] flex flex-col items-center justify-center bg-[var(--color-neutral-100)] p-4", children: /* @__PURE__ */ (0, import_jsx_runtime11.jsxs)("div", { className: "w-full max-w-md rounded-lg border border-[color-mix(in_srgb,var(--color-red-500)_20%,transparent)] bg-[color-mix(in_srgb,var(--color-red-500)_10%,transparent)] p-4", children: [
563
+ /* @__PURE__ */ (0, import_jsx_runtime11.jsx)("h2", { className: "mb-2 font-semibold text-[var(--color-red-500)]", children: "Authentication Error" }),
564
+ /* @__PURE__ */ (0, import_jsx_runtime11.jsx)("p", { className: "text-[color-mix(in_srgb,var(--color-red-500)_80%,transparent)]", children: state.message }),
565
+ /* @__PURE__ */ (0, import_jsx_runtime11.jsx)(
456
566
  "button",
457
567
  {
458
568
  type: "button",
@@ -463,439 +573,116 @@ function AuthCallbackHandler({ renderStatus, ...options }) {
463
573
  )
464
574
  ] }) });
465
575
  }
466
- return /* @__PURE__ */ (0, import_jsx_runtime10.jsx)("div", { className: "fixed inset-0 z-[100] flex flex-col items-center justify-center bg-[var(--color-neutral-100)]", "data-page": "auth-callback-handler", children: /* @__PURE__ */ (0, import_jsx_runtime10.jsxs)("div", { className: "text-center", children: [
467
- /* @__PURE__ */ (0, import_jsx_runtime10.jsx)("h1", { className: "mb-4 text-2xl font-bold", children: "Connecting\u2026" }),
468
- /* @__PURE__ */ (0, import_jsx_runtime10.jsx)("p", { className: "text-[var(--color-neutral-600)]", children: "Completing sign-in." })
576
+ return /* @__PURE__ */ (0, import_jsx_runtime11.jsx)("div", { className: "fixed inset-0 z-[100] flex flex-col items-center justify-center bg-[var(--color-neutral-100)]", "data-page": "auth-callback-handler", children: /* @__PURE__ */ (0, import_jsx_runtime11.jsxs)("div", { className: "text-center", children: [
577
+ /* @__PURE__ */ (0, import_jsx_runtime11.jsx)("h1", { className: "mb-4 text-2xl font-bold", children: "Connecting\u2026" }),
578
+ /* @__PURE__ */ (0, import_jsx_runtime11.jsx)("p", { className: "text-[var(--color-neutral-600)]", children: "Completing sign-in." })
469
579
  ] }) });
470
580
  }
471
581
 
472
582
  // src/genre-tree/GenreTreeView.tsx
473
- var import_react10 = require("react");
583
+ var import_react15 = require("react");
474
584
  var import_react_query3 = require("@tanstack/react-query");
475
- var import_lucide_react12 = require("lucide-react");
476
- var import_ui17 = require("@behindthemusictree/ui");
585
+ var import_lucide_react15 = require("lucide-react");
586
+ var import_ui16 = require("@behindthemusictree/ui");
477
587
  var import_genre_tree_view4 = require("@behindthemusictree/genre-tree-view");
588
+ var import_genre_tree16 = require("@behindthemusictree/app-kit/genre-tree");
478
589
  var import_genre_tree17 = require("@behindthemusictree/app-kit/genre-tree");
479
- var import_genre_tree18 = require("@behindthemusictree/app-kit/genre-tree");
480
- var import_player10 = require("@behindthemusictree/app-kit/player");
481
- var import_popup5 = require("@behindthemusictree/app-kit/popup");
482
-
483
- // src/genre-tree/GenreDetailExtras.tsx
484
- var import_ui7 = require("@behindthemusictree/ui");
485
- var import_genre_tree8 = require("@behindthemusictree/app-kit/genre-tree");
486
-
487
- // src/genre-tree/ClampedText.tsx
488
- var import_react = require("react");
489
- var import_jsx_runtime11 = require("react/jsx-runtime");
490
- function ClampedText({ children, className = "" }) {
491
- const ref = (0, import_react.useRef)(null);
492
- const [expanded, setExpanded] = (0, import_react.useState)(false);
493
- const [overflows, setOverflows] = (0, import_react.useState)(false);
494
- (0, import_react.useLayoutEffect)(() => {
495
- const element = ref.current;
496
- if (!element || expanded) return;
497
- setOverflows(element.scrollHeight > element.clientHeight);
498
- }, [children, expanded]);
499
- return /* @__PURE__ */ (0, import_jsx_runtime11.jsxs)(import_jsx_runtime11.Fragment, { children: [
500
- /* @__PURE__ */ (0, import_jsx_runtime11.jsx)("p", { ref, className: `${className} ${expanded ? "" : "line-clamp-3"}`, children }),
501
- (overflows || expanded) && /* @__PURE__ */ (0, import_jsx_runtime11.jsx)(
502
- "button",
503
- {
504
- type: "button",
505
- "aria-expanded": expanded,
506
- onClick: () => setExpanded((value) => !value),
507
- className: "mt-1 text-xs text-[var(--color-text-muted)] underline hover:text-[var(--color-text)]",
508
- children: expanded ? "Show less" : "Show more"
509
- }
510
- )
511
- ] });
512
- }
590
+ var import_popup6 = require("@behindthemusictree/app-kit/popup");
513
591
 
514
- // src/genre-tree/GenrePlaylistTracks.tsx
515
- var import_react2 = require("react");
516
- var import_react_query = require("@tanstack/react-query");
517
- var import_zod = require("zod");
518
- var import_fa5 = require("react-icons/fa");
519
- var import_ui6 = require("@behindthemusictree/ui");
520
- var import_auth2 = require("@behindthemusictree/app-kit/auth");
521
- var import_transport2 = require("@behindthemusictree/app-kit/transport");
522
- var import_transport3 = require("@behindthemusictree/app-kit/transport");
523
- var import_transport4 = require("@behindthemusictree/app-kit/transport");
592
+ // src/genre-tree/playlist-tree/TreePerRoot.tsx
593
+ var import_react4 = require("react");
594
+ var import_genre_tree_view = require("@behindthemusictree/genre-tree-view");
524
595
  var import_player = require("@behindthemusictree/app-kit/player");
525
- var import_player2 = require("@behindthemusictree/app-kit/player");
526
596
  var import_popup2 = require("@behindthemusictree/app-kit/popup");
597
+ var import_transport2 = require("@behindthemusictree/app-kit/transport");
527
598
 
528
- // src/genre-tree/UnplayableReason.tsx
529
- var import_fa3 = require("react-icons/fa");
599
+ // src/genre-tree/playlist-tree/useGenrePlaylistPlayback.ts
600
+ var import_react3 = require("react");
530
601
  var import_genre_tree = require("@behindthemusictree/app-kit/genre-tree");
531
- var import_jsx_runtime12 = require("react/jsx-runtime");
532
- function UnplayableReason({ reason }) {
533
- if (reason !== import_genre_tree.NO_VIDEO_LABEL) return /* @__PURE__ */ (0, import_jsx_runtime12.jsx)(import_jsx_runtime12.Fragment, { children: reason });
534
- return /* @__PURE__ */ (0, import_jsx_runtime12.jsxs)("span", { role: "img", "aria-label": reason, className: "relative inline-flex shrink-0 items-center", children: [
535
- /* @__PURE__ */ (0, import_jsx_runtime12.jsx)(import_fa3.FaYoutube, { size: 16, "aria-hidden": "true" }),
536
- /* @__PURE__ */ (0, import_jsx_runtime12.jsx)("span", { "aria-hidden": "true", className: "absolute left-1/2 top-1/2 h-0.5 w-[130%] -translate-x-1/2 -translate-y-1/2 -rotate-45 rounded bg-current" })
537
- ] });
538
- }
539
-
540
- // src/genre-tree/GenrePlaylistTracks.tsx
541
- var import_transport5 = require("@behindthemusictree/app-kit/transport");
542
- var import_genre_tree3 = require("@behindthemusictree/app-kit/genre-tree");
543
- var import_genre_tree4 = require("@behindthemusictree/app-kit/genre-tree");
544
- var import_genre_tree5 = require("@behindthemusictree/app-kit/genre-tree");
545
- var import_genre_tree6 = require("@behindthemusictree/app-kit/genre-tree");
546
-
547
- // src/genre-tree/MusicBrainzRecordingLink.tsx
548
- var import_fa4 = require("react-icons/fa");
549
- var import_genre_tree2 = require("@behindthemusictree/app-kit/genre-tree");
550
- var import_jsx_runtime13 = require("react/jsx-runtime");
551
- function MusicBrainzRecordingLink({ song }) {
552
- const url = (0, import_genre_tree2.musicBrainzRecordingUrl)(song);
553
- if (!url) return null;
554
- return /* @__PURE__ */ (0, import_jsx_runtime13.jsx)(
555
- "a",
556
- {
557
- href: url,
558
- target: "_blank",
559
- rel: "noopener noreferrer",
560
- "aria-label": `View ${song.title} on MusicBrainz`,
561
- title: "View on MusicBrainz",
562
- className: "inline-flex shrink-0 items-center opacity-50 hover:opacity-100 focus-visible:opacity-100",
563
- onClick: (event) => event.stopPropagation(),
564
- children: /* @__PURE__ */ (0, import_jsx_runtime13.jsx)(import_fa4.FaExternalLinkAlt, { size: 10 })
565
- }
602
+ function useGenrePlaylistPlayback({
603
+ scope,
604
+ genrePlaylists,
605
+ artist = null
606
+ }) {
607
+ const { trackList, playNewTrackListFromGenrePlaylist, playNewTrackListFromArtist } = (0, import_genre_tree.useTrackList)();
608
+ const origin = trackList?.origin;
609
+ const playingNodeId = (0, import_react3.useMemo)(() => {
610
+ if (!origin) return null;
611
+ if (!artist) return origin.type === import_genre_tree.TrackListOriginType.GENRE_PLAYLIST ? origin.uuid : null;
612
+ if (origin.type !== import_genre_tree.TrackListOriginType.ARTIST || origin.uuid !== artist.uuid) return null;
613
+ const { genreUuid } = origin;
614
+ return genreUuid ? genrePlaylists.find((gp) => gp.criteria?.uuid === genreUuid)?.uuid ?? null : null;
615
+ }, [origin, artist, genrePlaylists]);
616
+ const play = (0, import_react3.useCallback)(
617
+ async (genrePlaylist) => {
618
+ if (!artist) return playNewTrackListFromGenrePlaylist(genrePlaylist, scope);
619
+ if (!genrePlaylist.criteria) return;
620
+ return playNewTrackListFromArtist(artist, scope, { ordering: "title", genreUuid: genrePlaylist.criteria.uuid });
621
+ },
622
+ [artist, scope, playNewTrackListFromGenrePlaylist, playNewTrackListFromArtist]
566
623
  );
624
+ return (0, import_react3.useMemo)(() => ({ playingNodeId, play }), [playingNodeId, play]);
567
625
  }
568
626
 
569
- // src/genre-tree/GenrePlaylistTracks.tsx
570
- var import_genre_tree7 = require("@behindthemusictree/app-kit/genre-tree");
627
+ // src/genre-tree/playlist-tree/TreePerRoot.tsx
571
628
  var import_track_noun2 = require("@behindthemusictree/app-kit/track-noun");
572
- var import_jsx_runtime14 = require("react/jsx-runtime");
573
- var CATALOGUE_PAGE_SIZE = 100;
574
- var SongPageSchema = (0, import_transport3.PaginatedResponseSchema)(import_genre_tree5.SongDetailedSchema).extend({
575
- overallTotal: import_zod.z.number().nullable(),
576
- page: import_zod.z.number().nullable(),
577
- totalPages: import_zod.z.number().nullable()
578
- });
579
- var TEXT_CELL = "min-w-0 flex-1 truncate [@container(min-width:40rem)]:flex [@container(min-width:40rem)]:gap-2";
580
- var TITLE_CELL = "[@container(min-width:40rem)]:flex-[2] [@container(min-width:40rem)]:truncate";
581
- var COLUMN_CELL = "[@container(min-width:40rem)]:flex-1 [@container(min-width:40rem)]:truncate";
582
- var ALBUM_CELL = `hidden [@container(min-width:40rem)]:block ${COLUMN_CELL}`;
583
- var SEPARATOR = "[@container(min-width:40rem)]:hidden";
584
- var nextCursor = (next) => {
585
- const cursor = new URL(next).searchParams.get("cursor");
586
- if (!cursor) throw new Error(`Songs next link has no cursor: ${next}`);
587
- return cursor;
588
- };
589
- function GenrePlaylistTracks({
590
- genreUuid,
591
- genrePlaylist,
629
+ var import_jsx_runtime12 = require("react/jsx-runtime");
630
+ function GenrePlaylistTreePerRoot({
592
631
  scope,
593
- getBackendBaseUrl,
594
- renderArtistLink,
595
- renderSongLeading,
596
- renderSongActions,
597
- renderTracksToolbar,
598
- artist = null
632
+ className: className2,
633
+ rootUuid,
634
+ genrePlaylistTreePerRoot,
635
+ handleGenreCreationAction,
636
+ handleGenreRenameAction,
637
+ additionalActions,
638
+ onNodeClick,
639
+ onNodeHover,
640
+ renderInfoPanelBody,
641
+ selectedNodeId,
642
+ onSelectedNodeChange,
643
+ hideInfoPanelClose,
644
+ hideInfoPanelTitle,
645
+ readOnly = false,
646
+ showToolbar,
647
+ wheelZoom,
648
+ defaultViewport,
649
+ viewportRestore,
650
+ onViewportChange,
651
+ playback,
652
+ ...infoPanelCuration
599
653
  }) {
600
- const { fetchGenrePlaylistTracksPage, playNewTrackListFromGenrePlaylist, playNewTrackListFromArtist } = (0, import_genre_tree6.useTrackList)();
601
- const { session, sessionRestored } = (0, import_auth2.useSession)();
602
- const { playerTrackObject, playState, handlePlayPauseAction } = (0, import_player.usePlayer)();
603
- const { showPopup } = (0, import_popup2.usePopup)();
654
+ const { isPlaying, setIsPlaying } = (0, import_player.usePlayer)();
655
+ const defaultPlayback = useGenrePlaylistPlayback({ scope, genrePlaylists: genrePlaylistTreePerRoot });
656
+ const { playingNodeId, play } = playback ?? defaultPlayback;
604
657
  const trackNoun = (0, import_track_noun2.useTrackNoun)();
605
- const { uuid } = genrePlaylist;
606
- const { fetch } = (0, import_transport2.useFetchWrapper)(getBackendBaseUrl);
607
- const { data, isPending, isError, refetch, hasNextPage, isFetchingNextPage, fetchNextPage } = (0, import_react_query.useInfiniteQuery)({
608
- queryKey: scope === "me" ? import_genre_tree3.genrePlaylistQueryKeys.me.tracks(uuid) : artist ? [...import_genre_tree4.libraryQueryKeys.reference.songs.byGenre(getBackendBaseUrl(), genreUuid), "artist", artist.uuid] : import_genre_tree4.libraryQueryKeys.reference.songs.byGenre(getBackendBaseUrl(), genreUuid),
609
- queryFn: async ({ pageParam }) => {
610
- if (scope === "me") return fetchGenrePlaylistTracksPage(uuid, scope, Number(pageParam));
611
- const response = await fetch(
612
- import_genre_tree4.libraryEndpoints.reference.songs.list(),
613
- true,
614
- false,
615
- {},
616
- { genre: genreUuid, ...artist && { artist: artist.uuid }, pageSize: CATALOGUE_PAGE_SIZE, withTotal: false, ...pageParam && { cursor: pageParam } }
617
- );
618
- const parsed = (0, import_transport4.parseWithLog)(SongPageSchema, response, "GenrePlaylistTracks");
619
- return { tracks: parsed.results, total: parsed.overallTotal, nextPage: parsed.next ? nextCursor(parsed.next) : null };
620
- },
621
- initialPageParam: scope === "me" ? 1 : "",
622
- getNextPageParam: (lastPage) => lastPage.nextPage ?? void 0,
623
- enabled: scope !== "me" || sessionRestored && !!session?.accessToken
624
- });
625
- const sentinelRef = (0, import_react2.useRef)(null);
626
- (0, import_react2.useEffect)(() => {
627
- const sentinel = sentinelRef.current;
628
- if (!hasNextPage || isFetchingNextPage || !sentinel) return;
629
- const observer = new IntersectionObserver((entries) => {
630
- if (entries.some((entry) => entry.isIntersecting)) {
631
- void fetchNextPage();
632
- }
633
- });
634
- observer.observe(sentinel);
635
- return () => observer.disconnect();
636
- }, [hasNextPage, isFetchingNextPage, fetchNextPage]);
637
- const pages = data?.pages ?? [];
638
- const tracks = pages.flatMap((page) => page.tracks);
639
- const total = pages[0]?.total ?? void 0;
640
- const play = (startSongUuid) => {
641
- const started = artist ? playNewTrackListFromArtist(artist, scope, { startSongUuid, ordering: "title", genreUuid }) : playNewTrackListFromGenrePlaylist(genrePlaylist, scope, { startSongUuid });
642
- started.catch((error) => {
643
- console.error("Failed to play genre playlist:", error);
644
- showPopup(/* @__PURE__ */ (0, import_jsx_runtime14.jsx)(InternalErrorPopup, { errorCode: import_transport5.ErrorCode.CLIENT_UNKNOWN }));
645
- });
646
- };
647
- return /* @__PURE__ */ (0, import_jsx_runtime14.jsxs)("div", { className: "gtv-info-panel-children", children: [
648
- /* @__PURE__ */ (0, import_jsx_runtime14.jsxs)("span", { className: "gtv-info-panel-children-title", children: [
649
- (0, import_track_noun2.capitalize)(trackNoun.plural),
650
- total !== void 0 && ` (${total})`
651
- ] }),
652
- renderTracksToolbar?.(),
653
- isPending ? /* @__PURE__ */ (0, import_jsx_runtime14.jsx)(import_ui6.Skeleton, { className: "h-4 w-3/4", "aria-busy": "true" }) : isError && tracks.length === 0 ? /* @__PURE__ */ (0, import_jsx_runtime14.jsxs)("p", { role: "alert", children: [
654
- "Couldn't load ",
655
- trackNoun.plural,
656
- ".",
657
- " ",
658
- /* @__PURE__ */ (0, import_jsx_runtime14.jsx)("button", { type: "button", className: "underline", onClick: () => void refetch(), children: "Retry" })
659
- ] }) : tracks.length === 0 ? /* @__PURE__ */ (0, import_jsx_runtime14.jsx)("p", { children: "\u2014" }) : /* @__PURE__ */ (0, import_jsx_runtime14.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: [
660
- tracks.map((track, index) => {
661
- const label = track.artists?.length ? `${track.title} \u2014 ${track.artists.map((artist2) => artist2.name).join(", ")}` : track.title;
662
- const isCurrent = playerTrackObject?.track.id === track.uuid;
663
- const isPlaying = isCurrent && playState === import_player2.PlayStates.PLAYING;
664
- const text = /* @__PURE__ */ (0, import_jsx_runtime14.jsxs)("span", { className: TEXT_CELL, children: [
665
- /* @__PURE__ */ (0, import_jsx_runtime14.jsx)("span", { className: TITLE_CELL, children: track.title }),
666
- track.artists?.length ? /* @__PURE__ */ (0, import_jsx_runtime14.jsx)("span", { className: SEPARATOR, children: " \u2014 " }) : null,
667
- /* @__PURE__ */ (0, import_jsx_runtime14.jsx)("span", { className: COLUMN_CELL, children: track.artists?.length ? /* @__PURE__ */ (0, import_jsx_runtime14.jsx)(import_genre_tree7.ArtistNames, { artists: track.artists, renderArtistLink }) : null }),
668
- track.album && /* @__PURE__ */ (0, import_jsx_runtime14.jsx)("span", { className: ALBUM_CELL, children: track.album.name })
669
- ] });
670
- const leading = renderSongLeading?.(track, index);
671
- const actions = renderSongActions?.(track);
672
- const unplayableReason = (0, import_genre_tree5.isSong)(track) ? (0, import_genre_tree5.songUnplayableReason)(track) : null;
673
- if (unplayableReason) {
674
- return /* @__PURE__ */ (0, import_jsx_runtime14.jsxs)("li", { className: "flex min-h-8 items-center gap-1 px-1 opacity-50", title: unplayableReason, children: [
675
- leading,
676
- /* @__PURE__ */ (0, import_jsx_runtime14.jsx)(
677
- "button",
678
- {
679
- type: "button",
680
- disabled: true,
681
- "aria-disabled": "true",
682
- "aria-label": `${label} \u2014 ${unplayableReason}`,
683
- className: "flex h-6 w-6 shrink-0 items-center justify-center rounded-full opacity-0 cursor-not-allowed",
684
- children: /* @__PURE__ */ (0, import_jsx_runtime14.jsx)(import_fa5.FaPlay, { size: 10 })
685
- }
686
- ),
687
- text,
688
- (0, import_genre_tree5.isSong)(track) && /* @__PURE__ */ (0, import_jsx_runtime14.jsx)(MusicBrainzRecordingLink, { song: track }),
689
- /* @__PURE__ */ (0, import_jsx_runtime14.jsx)("span", { className: "shrink-0 text-xs", children: /* @__PURE__ */ (0, import_jsx_runtime14.jsx)(UnplayableReason, { reason: unplayableReason }) }),
690
- actions
691
- ] }, track.uuid);
692
- }
693
- return /* @__PURE__ */ (0, import_jsx_runtime14.jsxs)(
694
- "li",
695
- {
696
- 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)]" : ""}`,
697
- children: [
698
- leading,
699
- /* @__PURE__ */ (0, import_jsx_runtime14.jsx)(
700
- "button",
701
- {
702
- type: "button",
703
- "aria-label": `${isPlaying ? "Pause" : "Play"} ${label}`,
704
- 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_player2.PlayStates.STOPPED ? "opacity-100" : "opacity-0 [@media(pointer:coarse)]:opacity-100"}`,
705
- onClick: () => isCurrent ? handlePlayPauseAction() : play(track.uuid),
706
- children: isCurrent && playState === import_player2.PlayStates.LOADING ? /* @__PURE__ */ (0, import_jsx_runtime14.jsx)(import_ui6.RingLoader, { size: 10 }) : isPlaying ? /* @__PURE__ */ (0, import_jsx_runtime14.jsx)(import_fa5.FaPause, { size: 10 }) : /* @__PURE__ */ (0, import_jsx_runtime14.jsx)(import_fa5.FaPlay, { size: 10 })
707
- }
708
- ),
709
- text,
710
- (0, import_genre_tree5.isSong)(track) && /* @__PURE__ */ (0, import_jsx_runtime14.jsx)(MusicBrainzRecordingLink, { song: track }),
711
- actions
712
- ]
713
- },
714
- track.uuid
715
- );
716
- }),
717
- hasNextPage && /* @__PURE__ */ (0, import_jsx_runtime14.jsx)("li", { ref: sentinelRef, "aria-hidden": "true", className: "h-4" })
718
- ] })
719
- ] });
720
- }
721
-
722
- // src/genre-tree/GenreDetailExtras.tsx
723
- var import_track_noun3 = require("@behindthemusictree/app-kit/track-noun");
724
- var import_jsx_runtime15 = require("react/jsx-runtime");
725
- function GenreDetailExtras({
726
- genreUuid,
727
- genrePlaylist,
728
- scope,
729
- getBackendBaseUrl,
730
- criteriaOverviewSchema,
731
- renderGenreDetailExtras,
732
- renderArtistLink,
733
- renderSummary,
734
- renderEssentialTracks,
735
- family,
736
- ...trackSlots
737
- }) {
738
- const trackNoun = (0, import_track_noun3.useTrackNoun)();
739
- const { data: overview, isPending } = (0, import_genre_tree8.useFetchGenreOverview)(
740
- genreUuid,
741
- scope,
742
- getBackendBaseUrl,
743
- criteriaOverviewSchema
744
- );
745
- if (isPending) {
746
- return /* @__PURE__ */ (0, import_jsx_runtime15.jsxs)(import_jsx_runtime15.Fragment, { children: [
747
- /* @__PURE__ */ (0, import_jsx_runtime15.jsxs)("div", { className: "gtv-info-panel-children", "aria-busy": "true", "data-testid": "genre-detail-extras-skeleton", children: [
748
- /* @__PURE__ */ (0, import_jsx_runtime15.jsx)("span", { className: "gtv-info-panel-children-title", children: "Summary" }),
749
- /* @__PURE__ */ (0, import_jsx_runtime15.jsx)(import_ui7.Skeleton, { className: "h-4 w-3/4" })
750
- ] }),
751
- family
752
- ] });
753
- }
754
- if (!overview) return /* @__PURE__ */ (0, import_jsx_runtime15.jsx)(import_jsx_runtime15.Fragment, { children: family });
755
- const { summary, description, summarySourceUrl, essentialSongs } = overview;
756
- return /* @__PURE__ */ (0, import_jsx_runtime15.jsxs)(import_jsx_runtime15.Fragment, { children: [
757
- description && /* @__PURE__ */ (0, import_jsx_runtime15.jsx)("div", { className: "px-3.5 py-2.5", children: /* @__PURE__ */ (0, import_jsx_runtime15.jsx)(ClampedText, { className: "text-[0.8125rem] leading-[1.45] text-[var(--color-text-muted)]", children: description }, genreUuid) }),
758
- renderSummary ? renderSummary(overview) : /* @__PURE__ */ (0, import_jsx_runtime15.jsxs)("div", { className: "gtv-info-panel-children text-[0.8125rem] leading-[1.45] text-[var(--color-text)]", children: [
759
- /* @__PURE__ */ (0, import_jsx_runtime15.jsx)("span", { className: "gtv-info-panel-children-title", children: "Summary" }),
760
- /* @__PURE__ */ (0, import_jsx_runtime15.jsx)(ClampedText, { className: "mt-1", children: summary ?? "\u2014" }, genreUuid),
761
- summarySourceUrl && /* @__PURE__ */ (0, import_jsx_runtime15.jsx)(
762
- "a",
763
- {
764
- href: summarySourceUrl,
765
- target: "_blank",
766
- rel: "noopener noreferrer",
767
- className: "mt-1 inline-block text-xs text-[var(--color-text-muted)] underline hover:text-[var(--color-text)]",
768
- children: "Source: Wikipedia"
769
- }
770
- )
771
- ] }),
772
- renderSummary && summarySourceUrl && /* @__PURE__ */ (0, import_jsx_runtime15.jsx)(
773
- "a",
774
- {
775
- href: summarySourceUrl,
776
- target: "_blank",
777
- rel: "noopener noreferrer",
778
- className: "mx-3.5 mb-2.5 inline-block text-xs text-[var(--color-text-muted)] underline hover:text-[var(--color-text)]",
779
- children: "Source: Wikipedia"
780
- }
781
- ),
782
- family,
783
- renderGenreDetailExtras?.(overview),
784
- renderEssentialTracks ? renderEssentialTracks(overview) : essentialSongs && /* @__PURE__ */ (0, import_jsx_runtime15.jsxs)("div", { className: "gtv-info-panel-children text-[0.8125rem] leading-[1.45] text-[var(--color-text)]", children: [
785
- /* @__PURE__ */ (0, import_jsx_runtime15.jsxs)("span", { className: "gtv-info-panel-children-title", children: [
786
- "Essential ",
787
- trackNoun.plural
788
- ] }),
789
- essentialSongs.length > 0 ? /* @__PURE__ */ (0, import_jsx_runtime15.jsx)("ul", { className: "mt-1 divide-y divide-[var(--color-border)]", children: essentialSongs.map((track) => /* @__PURE__ */ (0, import_jsx_runtime15.jsx)("li", { className: "py-1", children: track.title }, track.uuid)) }) : /* @__PURE__ */ (0, import_jsx_runtime15.jsx)("p", { className: "mt-1", children: "\u2014" })
790
- ] }),
791
- /* @__PURE__ */ (0, import_jsx_runtime15.jsx)(
792
- GenrePlaylistTracks,
793
- {
794
- genreUuid,
795
- genrePlaylist,
796
- scope,
797
- getBackendBaseUrl,
798
- renderArtistLink,
799
- ...trackSlots
800
- }
801
- )
802
- ] });
803
- }
804
-
805
- // src/genre-tree/playlist-tree/TreePerRoot.tsx
806
- var import_react4 = require("react");
807
- var import_genre_tree_view = require("@behindthemusictree/genre-tree-view");
808
- var import_player3 = require("@behindthemusictree/app-kit/player");
809
- var import_popup3 = require("@behindthemusictree/app-kit/popup");
810
- var import_transport6 = require("@behindthemusictree/app-kit/transport");
811
-
812
- // src/genre-tree/playlist-tree/useGenrePlaylistPlayback.ts
813
- var import_react3 = require("react");
814
- var import_genre_tree9 = require("@behindthemusictree/app-kit/genre-tree");
815
- function useGenrePlaylistPlayback({
816
- scope,
817
- genrePlaylists,
818
- artist = null
819
- }) {
820
- const { trackList, playNewTrackListFromGenrePlaylist, playNewTrackListFromArtist } = (0, import_genre_tree9.useTrackList)();
821
- const origin = trackList?.origin;
822
- const playingNodeId = (0, import_react3.useMemo)(() => {
823
- if (!origin) return null;
824
- if (!artist) return origin.type === import_genre_tree9.TrackListOriginType.GENRE_PLAYLIST ? origin.uuid : null;
825
- if (origin.type !== import_genre_tree9.TrackListOriginType.ARTIST || origin.uuid !== artist.uuid) return null;
826
- const { genreUuid } = origin;
827
- return genreUuid ? genrePlaylists.find((gp) => gp.criteria?.uuid === genreUuid)?.uuid ?? null : null;
828
- }, [origin, artist, genrePlaylists]);
829
- const play = (0, import_react3.useCallback)(
830
- async (genrePlaylist) => {
831
- if (!artist) return playNewTrackListFromGenrePlaylist(genrePlaylist, scope);
832
- if (!genrePlaylist.criteria) return;
833
- return playNewTrackListFromArtist(artist, scope, { ordering: "title", genreUuid: genrePlaylist.criteria.uuid });
834
- },
835
- [artist, scope, playNewTrackListFromGenrePlaylist, playNewTrackListFromArtist]
836
- );
837
- return (0, import_react3.useMemo)(() => ({ playingNodeId, play }), [playingNodeId, play]);
838
- }
839
-
840
- // src/genre-tree/playlist-tree/TreePerRoot.tsx
841
- var import_track_noun4 = require("@behindthemusictree/app-kit/track-noun");
842
- var import_jsx_runtime16 = require("react/jsx-runtime");
843
- function GenrePlaylistTreePerRoot({
844
- scope,
845
- className,
846
- rootUuid,
847
- genrePlaylistTreePerRoot,
848
- handleGenreCreationAction,
849
- handleGenreRenameAction,
850
- additionalActions,
851
- onNodeClick,
852
- onNodeHover,
853
- renderInfoPanelBody,
854
- selectedNodeId,
855
- onSelectedNodeChange,
856
- hideInfoPanelClose,
857
- hideInfoPanelTitle,
858
- readOnly = false,
859
- showToolbar,
860
- wheelZoom,
861
- controlsOrientation,
862
- defaultViewport,
863
- onViewportChange,
864
- playback,
865
- ...infoPanelCuration
866
- }) {
867
- const { isPlaying, setIsPlaying } = (0, import_player3.usePlayer)();
868
- const defaultPlayback = useGenrePlaylistPlayback({ scope, genrePlaylists: genrePlaylistTreePerRoot });
869
- const { playingNodeId, play } = playback ?? defaultPlayback;
870
- const trackNoun = (0, import_track_noun4.useTrackNoun)();
871
- const { showPopup } = (0, import_popup3.usePopup)();
872
- const nodes = (0, import_react4.useMemo)(
873
- () => genrePlaylistTreePerRoot.map((genrePlaylist) => ({
874
- id: genrePlaylist.uuid,
875
- parentId: genrePlaylist.parent?.uuid ?? null,
876
- name: genrePlaylist.name,
877
- itemCount: genrePlaylist.tracksCount,
878
- actionable: Boolean(genrePlaylist.criteria),
879
- side: genrePlaylist.criteria?.side ?? void 0,
880
- wikidataId: genrePlaylist.criteria?.wikidataId ?? null,
881
- umbrella: genrePlaylist.isUmbrella
882
- })),
883
- [genrePlaylistTreePerRoot]
884
- );
885
- const handlePlayPause = (0, import_react4.useCallback)(
886
- (nodeId) => {
887
- const genrePlaylist = genrePlaylistTreePerRoot.find((g) => g.uuid === nodeId);
888
- if (!genrePlaylist) return;
889
- if (nodeId === playingNodeId) {
890
- setIsPlaying(!isPlaying);
891
- return;
658
+ const { showPopup } = (0, import_popup2.usePopup)();
659
+ const nodes = (0, import_react4.useMemo)(
660
+ () => genrePlaylistTreePerRoot.map((genrePlaylist) => ({
661
+ id: genrePlaylist.uuid,
662
+ parentId: genrePlaylist.parent?.uuid ?? null,
663
+ name: genrePlaylist.name,
664
+ itemCount: genrePlaylist.tracksCount,
665
+ actionable: Boolean(genrePlaylist.criteria),
666
+ side: genrePlaylist.criteria?.side ?? void 0,
667
+ wikidataId: genrePlaylist.criteria?.wikidataId ?? null,
668
+ umbrella: genrePlaylist.isUmbrella
669
+ })),
670
+ [genrePlaylistTreePerRoot]
671
+ );
672
+ const handlePlayPause = (0, import_react4.useCallback)(
673
+ (nodeId) => {
674
+ const genrePlaylist = genrePlaylistTreePerRoot.find((g) => g.uuid === nodeId);
675
+ if (!genrePlaylist) return;
676
+ if (nodeId === playingNodeId) {
677
+ setIsPlaying(!isPlaying);
678
+ return;
892
679
  }
893
680
  if (genrePlaylist.tracksCount === 0) {
894
681
  return;
895
682
  }
896
683
  play(genrePlaylist).catch((error) => {
897
684
  console.error("Failed to load genre playlist tracks:", error);
898
- showPopup(/* @__PURE__ */ (0, import_jsx_runtime16.jsx)(InternalErrorPopup, { errorCode: import_transport6.ErrorCode.CLIENT_UNKNOWN }));
685
+ showPopup(/* @__PURE__ */ (0, import_jsx_runtime12.jsx)(InternalErrorPopup, { errorCode: import_transport2.ErrorCode.CLIENT_UNKNOWN }));
899
686
  });
900
687
  },
901
688
  [
@@ -927,12 +714,12 @@ function GenrePlaylistTreePerRoot({
927
714
  if (confirm(`Are you sure you want to delete "${node.name}"?`)) {
928
715
  }
929
716
  }, []);
930
- return /* @__PURE__ */ (0, import_jsx_runtime16.jsx)(
717
+ return /* @__PURE__ */ (0, import_jsx_runtime12.jsx)(
931
718
  import_genre_tree_view.GenreTree,
932
719
  {
933
- className,
720
+ className: className2,
934
721
  nodes,
935
- itemCountLabel: `${(0, import_track_noun4.capitalize)(trackNoun.singular)} count`,
722
+ itemCountLabel: `${(0, import_track_noun2.capitalize)(trackNoun.singular)} count`,
936
723
  rootColor: (0, import_genre_tree_view.getGenreTreeColor)(rootUuid),
937
724
  playingNodeId,
938
725
  playState: isPlaying ? "playing" : "paused",
@@ -950,8 +737,8 @@ function GenrePlaylistTreePerRoot({
950
737
  hideInfoPanelTitle,
951
738
  showToolbar,
952
739
  wheelZoom,
953
- controlsOrientation,
954
740
  defaultViewport,
741
+ viewportRestore,
955
742
  onViewportChange,
956
743
  ...infoPanelCuration,
957
744
  readOnly
@@ -962,19 +749,26 @@ function GenrePlaylistTreePerRoot({
962
749
  // src/genre-tree/playlist-tree/TreeWheel.tsx
963
750
  var import_react5 = require("react");
964
751
  var import_genre_tree_view2 = require("@behindthemusictree/genre-tree-view");
965
- var import_player4 = require("@behindthemusictree/app-kit/player");
966
- var import_popup4 = require("@behindthemusictree/app-kit/popup");
967
- var import_transport7 = require("@behindthemusictree/app-kit/transport");
752
+ var import_player2 = require("@behindthemusictree/app-kit/player");
753
+ var import_popup3 = require("@behindthemusictree/app-kit/popup");
754
+ var import_transport3 = require("@behindthemusictree/app-kit/transport");
968
755
 
969
756
  // src/genre-tree/playlist-tree/size-metric.ts
970
- var genrePlaylistSize = (genrePlaylist, metric) => metric === "views" ? genrePlaylist.viewsCount : genrePlaylist.tracksCount;
757
+ var hasViewsCount = (genrePlaylists) => genrePlaylists.every((genrePlaylist) => genrePlaylist.viewsCount !== void 0);
758
+ var genrePlaylistSize = (genrePlaylist, metric) => {
759
+ if (metric === "songs") return genrePlaylist.tracksCount;
760
+ if (genrePlaylist.viewsCount === void 0) {
761
+ throw new Error(`Genre playlist ${genrePlaylist.uuid} has no viewsCount to size by views`);
762
+ }
763
+ return genrePlaylist.viewsCount;
764
+ };
971
765
 
972
766
  // src/genre-tree/playlist-tree/TreeWheel.tsx
973
- var import_track_noun5 = require("@behindthemusictree/app-kit/track-noun");
974
- var import_jsx_runtime17 = require("react/jsx-runtime");
767
+ var import_track_noun3 = require("@behindthemusictree/app-kit/track-noun");
768
+ var import_jsx_runtime13 = require("react/jsx-runtime");
975
769
  function GenrePlaylistTreeWheel({
976
770
  scope,
977
- className,
771
+ className: className2,
978
772
  genrePlaylists,
979
773
  handleGenreCreationAction,
980
774
  handleGenreRenameAction,
@@ -989,20 +783,20 @@ function GenrePlaylistTreeWheel({
989
783
  readOnly = false,
990
784
  allowWheelRotation,
991
785
  showToolbar,
992
- controlsOrientation,
993
786
  sizeMetric = "songs",
994
787
  floatingControlsLeading,
995
788
  busy,
996
789
  defaultViewport,
790
+ viewportRestore,
997
791
  onViewportChange,
998
792
  playback,
999
793
  ...infoPanelCuration
1000
794
  }) {
1001
- const { isPlaying, setIsPlaying } = (0, import_player4.usePlayer)();
795
+ const { isPlaying, setIsPlaying } = (0, import_player2.usePlayer)();
1002
796
  const defaultPlayback = useGenrePlaylistPlayback({ scope, genrePlaylists });
1003
797
  const { playingNodeId, play } = playback ?? defaultPlayback;
1004
- const trackNoun = (0, import_track_noun5.useTrackNoun)();
1005
- const { showPopup } = (0, import_popup4.usePopup)();
798
+ const trackNoun = (0, import_track_noun3.useTrackNoun)();
799
+ const { showPopup } = (0, import_popup3.usePopup)();
1006
800
  const nodes = (0, import_react5.useMemo)(
1007
801
  () => genrePlaylists.map((genrePlaylist) => ({
1008
802
  id: genrePlaylist.uuid,
@@ -1028,7 +822,7 @@ function GenrePlaylistTreeWheel({
1028
822
  }
1029
823
  play(genrePlaylist).catch((error) => {
1030
824
  console.error("Failed to load genre playlist tracks:", error);
1031
- showPopup(/* @__PURE__ */ (0, import_jsx_runtime17.jsx)(InternalErrorPopup, { errorCode: import_transport7.ErrorCode.CLIENT_UNKNOWN }));
825
+ showPopup(/* @__PURE__ */ (0, import_jsx_runtime13.jsx)(InternalErrorPopup, { errorCode: import_transport3.ErrorCode.CLIENT_UNKNOWN }));
1032
826
  });
1033
827
  },
1034
828
  [
@@ -1060,12 +854,12 @@ function GenrePlaylistTreeWheel({
1060
854
  if (confirm(`Are you sure you want to delete "${node.name}"?`)) {
1061
855
  }
1062
856
  }, []);
1063
- return /* @__PURE__ */ (0, import_jsx_runtime17.jsx)(
857
+ return /* @__PURE__ */ (0, import_jsx_runtime13.jsx)(
1064
858
  import_genre_tree_view2.GenreTreeWheel,
1065
859
  {
1066
- className,
860
+ className: className2,
1067
861
  nodes,
1068
- itemCountLabel: sizeMetric === "views" ? "Views" : `${(0, import_track_noun5.capitalize)(trackNoun.singular)} count`,
862
+ itemCountLabel: sizeMetric === "views" ? "Views" : `${(0, import_track_noun3.capitalize)(trackNoun.singular)} count`,
1069
863
  playingNodeId,
1070
864
  playState: isPlaying ? "playing" : "paused",
1071
865
  onPlayPause: handlePlayPause,
@@ -1082,10 +876,10 @@ function GenrePlaylistTreeWheel({
1082
876
  hideInfoPanelTitle,
1083
877
  allowWheelRotation,
1084
878
  showToolbar,
1085
- controlsOrientation,
1086
879
  floatingControlsLeading,
1087
880
  busy,
1088
881
  defaultViewport,
882
+ viewportRestore,
1089
883
  onViewportChange,
1090
884
  ...infoPanelCuration,
1091
885
  readOnly
@@ -1096,13 +890,13 @@ function GenrePlaylistTreeWheel({
1096
890
  // src/genre-tree/playlist-tree/TreeWheelRadialPopCore.tsx
1097
891
  var import_react6 = require("react");
1098
892
  var import_genre_tree_view3 = require("@behindthemusictree/genre-tree-view");
1099
- var import_player5 = require("@behindthemusictree/app-kit/player");
1100
- var import_track_noun6 = require("@behindthemusictree/app-kit/track-noun");
1101
- var import_jsx_runtime18 = require("react/jsx-runtime");
893
+ var import_player3 = require("@behindthemusictree/app-kit/player");
894
+ var import_track_noun4 = require("@behindthemusictree/app-kit/track-noun");
895
+ var import_jsx_runtime14 = require("react/jsx-runtime");
1102
896
  function GenrePlaylistTreeWheelRadialPopCore(props) {
1103
897
  const {
1104
898
  scope,
1105
- className,
899
+ className: className2,
1106
900
  genrePlaylists,
1107
901
  handleGenreCreationAction,
1108
902
  handleGenreRenameAction,
@@ -1117,12 +911,12 @@ function GenrePlaylistTreeWheelRadialPopCore(props) {
1117
911
  readOnly = false,
1118
912
  allowWheelRotation,
1119
913
  showToolbar,
1120
- controlsOrientation,
1121
914
  sizeMetric = "songs",
1122
915
  floatingControlsLeading,
1123
916
  busy,
1124
917
  outline = false,
1125
918
  defaultViewport,
919
+ viewportRestore,
1126
920
  onViewportChange,
1127
921
  playback
1128
922
  } = props;
@@ -1135,10 +929,10 @@ function GenrePlaylistTreeWheelRadialPopCore(props) {
1135
929
  hideExpandButton: props.hideExpandButton,
1136
930
  overlayPanel: props.overlayPanel
1137
931
  };
1138
- const { isPlaying, setIsPlaying } = (0, import_player5.usePlayer)();
932
+ const { isPlaying, setIsPlaying } = (0, import_player3.usePlayer)();
1139
933
  const defaultPlayback = useGenrePlaylistPlayback({ scope, genrePlaylists });
1140
934
  const { playingNodeId, play } = playback ?? defaultPlayback;
1141
- const trackNoun = (0, import_track_noun6.useTrackNoun)();
935
+ const trackNoun = (0, import_track_noun4.useTrackNoun)();
1142
936
  const nodes = (0, import_react6.useMemo)(
1143
937
  () => genrePlaylists.map((genrePlaylist) => ({
1144
938
  id: genrePlaylist.uuid,
@@ -1192,9 +986,9 @@ function GenrePlaylistTreeWheelRadialPopCore(props) {
1192
986
  }
1193
987
  }, []);
1194
988
  const treeProps = {
1195
- className,
989
+ className: className2,
1196
990
  nodes,
1197
- itemCountLabel: sizeMetric === "views" ? "Views" : `${(0, import_track_noun6.capitalize)(trackNoun.singular)} count`,
991
+ itemCountLabel: sizeMetric === "views" ? "Views" : `${(0, import_track_noun4.capitalize)(trackNoun.singular)} count`,
1198
992
  playingNodeId,
1199
993
  playState: isPlaying ? "playing" : "paused",
1200
994
  onPlayPause: handlePlayPause,
@@ -1213,15 +1007,15 @@ function GenrePlaylistTreeWheelRadialPopCore(props) {
1213
1007
  ...infoPanelCuration,
1214
1008
  readOnly
1215
1009
  };
1216
- return outline ? /* @__PURE__ */ (0, import_jsx_runtime18.jsx)(import_genre_tree_view3.GenreTreeOutline, { ...treeProps }) : /* @__PURE__ */ (0, import_jsx_runtime18.jsx)(
1010
+ return outline ? /* @__PURE__ */ (0, import_jsx_runtime14.jsx)(import_genre_tree_view3.GenreTreeOutline, { ...treeProps }) : /* @__PURE__ */ (0, import_jsx_runtime14.jsx)(
1217
1011
  import_genre_tree_view3.GenreTreeWheelRadialPopCore,
1218
1012
  {
1219
1013
  ...treeProps,
1220
1014
  allowWheelRotation,
1221
- controlsOrientation,
1222
1015
  floatingControlsLeading,
1223
1016
  busy,
1224
1017
  defaultViewport,
1018
+ viewportRestore,
1225
1019
  onViewportChange,
1226
1020
  ...focus.onFocusGenreChange ? { focusNodeId: focus.focusGenreUuid, onFocusChange: focus.onFocusGenreChange } : { focusNodeId: void 0, onFocusChange: void 0 }
1227
1021
  }
@@ -1230,7 +1024,7 @@ function GenrePlaylistTreeWheelRadialPopCore(props) {
1230
1024
 
1231
1025
  // src/genre-tree/GenreTreeWheelHandoff.tsx
1232
1026
  var import_react7 = require("react");
1233
- var import_jsx_runtime19 = require("react/jsx-runtime");
1027
+ var import_jsx_runtime15 = require("react/jsx-runtime");
1234
1028
  function GenreTreeWheelHandoff({ skeleton, children, onRevealedChange }) {
1235
1029
  const [revealed, setRevealed] = (0, import_react7.useState)(false);
1236
1030
  const onRevealedChangeRef = (0, import_react7.useRef)(onRevealedChange);
@@ -1249,8 +1043,8 @@ function GenreTreeWheelHandoff({ skeleton, children, onRevealedChange }) {
1249
1043
  cancelAnimationFrame(innerFrame);
1250
1044
  };
1251
1045
  }, []);
1252
- return /* @__PURE__ */ (0, import_jsx_runtime19.jsxs)("div", { className: "relative w-full h-full", children: [
1253
- /* @__PURE__ */ (0, import_jsx_runtime19.jsx)(
1046
+ return /* @__PURE__ */ (0, import_jsx_runtime15.jsxs)("div", { className: "relative w-full h-full", children: [
1047
+ /* @__PURE__ */ (0, import_jsx_runtime15.jsx)(
1254
1048
  "div",
1255
1049
  {
1256
1050
  className: "absolute inset-0 transition-opacity duration-150 ease-out",
@@ -1259,15 +1053,15 @@ function GenreTreeWheelHandoff({ skeleton, children, onRevealedChange }) {
1259
1053
  children
1260
1054
  }
1261
1055
  ),
1262
- !revealed && /* @__PURE__ */ (0, import_jsx_runtime19.jsx)("div", { className: "absolute inset-0", "aria-hidden": "true", children: skeleton })
1056
+ !revealed && /* @__PURE__ */ (0, import_jsx_runtime15.jsx)("div", { className: "absolute inset-0", "aria-hidden": "true", children: skeleton })
1263
1057
  ] });
1264
1058
  }
1265
1059
 
1266
1060
  // src/genre-tree/GenreSearch.tsx
1267
1061
  var import_lucide_react7 = require("lucide-react");
1268
- var import_ui8 = require("@behindthemusictree/ui");
1269
- var import_genre_tree10 = require("@behindthemusictree/app-kit/genre-tree");
1270
- var import_jsx_runtime20 = require("react/jsx-runtime");
1062
+ var import_ui6 = require("@behindthemusictree/ui");
1063
+ var import_genre_tree2 = require("@behindthemusictree/app-kit/genre-tree");
1064
+ var import_jsx_runtime16 = require("react/jsx-runtime");
1271
1065
  function GenreSearch({
1272
1066
  genrePlaylists,
1273
1067
  onSelect,
@@ -1278,7 +1072,7 @@ function GenreSearch({
1278
1072
  onQueryChange,
1279
1073
  extraResults
1280
1074
  }) {
1281
- const { query, setQuery, results, canClear, clear, select } = (0, import_genre_tree10.useGenreSearch)({
1075
+ const { query, setQuery, results, canClear, clear, select } = (0, import_genre_tree2.useGenreSearch)({
1282
1076
  genrePlaylists,
1283
1077
  onSelect,
1284
1078
  selectedName,
@@ -1289,20 +1083,20 @@ function GenreSearch({
1289
1083
  const loading = query !== "" && query !== selectedName && (extraResults?.loading ?? false);
1290
1084
  const showList = results.length > 0 || extraItems.length > 0;
1291
1085
  const heading = "px-3 pt-2 pb-1 text-xs font-medium uppercase text-[var(--color-text-muted)]";
1292
- return /* @__PURE__ */ (0, import_jsx_runtime20.jsxs)("div", { className: "genre-search relative w-[calc(var(--layout-detail-panel-w,25.5rem)-2*var(--layout-panel-inset,1rem))] shrink-0", children: [
1293
- onPrevious && /* @__PURE__ */ (0, import_jsx_runtime20.jsx)(
1086
+ return /* @__PURE__ */ (0, import_jsx_runtime16.jsxs)("div", { className: "genre-search relative w-[calc(var(--layout-detail-panel-w,25.5rem)-2*var(--layout-panel-inset,1rem))] shrink-0 max-md:w-full", children: [
1087
+ onPrevious && /* @__PURE__ */ (0, import_jsx_runtime16.jsx)(
1294
1088
  "button",
1295
1089
  {
1296
1090
  type: "button",
1297
1091
  "aria-label": "Previous",
1298
1092
  title: "Previous",
1299
- className: "genre-search-previous absolute left-2 top-1/2 z-10 hidden p-1.5 text-[var(--color-text-muted)] rounded-full -translate-y-1/2 hover:bg-[var(--color-surface-muted)] hover:text-[var(--color-text)]",
1093
+ className: "genre-search-previous absolute left-2 top-1/2 z-10 hidden items-center justify-center p-1.5 [@media(pointer:coarse)]:min-h-[var(--layout-touch-target)] [@media(pointer:coarse)]:min-w-[var(--layout-touch-target)] text-[var(--color-text-muted)] rounded-full -translate-y-1/2 hover:bg-[var(--color-surface-muted)] hover:text-[var(--color-text)]",
1300
1094
  onClick: onPrevious,
1301
- children: /* @__PURE__ */ (0, import_jsx_runtime20.jsx)(import_lucide_react7.ArrowLeft, { "aria-hidden": "true", className: "w-4 h-4" })
1095
+ children: /* @__PURE__ */ (0, import_jsx_runtime16.jsx)(import_lucide_react7.ArrowLeft, { "aria-hidden": "true", className: "w-4 h-4" })
1302
1096
  }
1303
1097
  ),
1304
- /* @__PURE__ */ (0, import_jsx_runtime20.jsx)(
1305
- import_ui8.Input,
1098
+ /* @__PURE__ */ (0, import_jsx_runtime16.jsx)(
1099
+ import_ui6.Input,
1306
1100
  {
1307
1101
  type: "text",
1308
1102
  value: query,
@@ -1313,24 +1107,27 @@ function GenreSearch({
1313
1107
  placeholder: placeholder ?? `${label}\u2026`,
1314
1108
  "aria-label": label,
1315
1109
  "aria-busy": loading,
1110
+ inputMode: "search",
1111
+ enterKeyHint: "search",
1112
+ autoComplete: "off",
1316
1113
  className: "h-[var(--layout-search-h,3rem)] pl-6 pr-20 text-base transition-[padding] duration-200 ease-out motion-reduce:transition-none bg-[var(--color-surface)] border-[var(--color-border)] rounded-full shadow-md"
1317
1114
  }
1318
1115
  ),
1319
- /* @__PURE__ */ (0, import_jsx_runtime20.jsxs)("div", { className: "absolute right-3 top-1/2 flex items-center -translate-y-1/2", children: [
1320
- /* @__PURE__ */ (0, import_jsx_runtime20.jsxs)(
1116
+ /* @__PURE__ */ (0, import_jsx_runtime16.jsxs)("div", { className: "absolute right-3 top-1/2 flex items-center -translate-y-1/2", children: [
1117
+ /* @__PURE__ */ (0, import_jsx_runtime16.jsxs)(
1321
1118
  "span",
1322
1119
  {
1323
1120
  "data-loading": loading,
1324
1121
  className: "relative p-2 text-[var(--color-text-muted)] pointer-events-none",
1325
1122
  children: [
1326
- /* @__PURE__ */ (0, import_jsx_runtime20.jsx)(
1123
+ /* @__PURE__ */ (0, import_jsx_runtime16.jsx)(
1327
1124
  import_lucide_react7.Search,
1328
1125
  {
1329
1126
  "aria-hidden": "true",
1330
1127
  className: `w-6 h-6 transition-opacity duration-200 motion-reduce:transition-none ${loading ? "opacity-0" : ""}`
1331
1128
  }
1332
1129
  ),
1333
- /* @__PURE__ */ (0, import_jsx_runtime20.jsx)(
1130
+ /* @__PURE__ */ (0, import_jsx_runtime16.jsx)(
1334
1131
  import_lucide_react7.LoaderCircle,
1335
1132
  {
1336
1133
  "aria-hidden": "true",
@@ -1340,44 +1137,44 @@ function GenreSearch({
1340
1137
  ]
1341
1138
  }
1342
1139
  ),
1343
- canClear && /* @__PURE__ */ (0, import_jsx_runtime20.jsx)(
1140
+ canClear && /* @__PURE__ */ (0, import_jsx_runtime16.jsx)(
1344
1141
  "button",
1345
1142
  {
1346
1143
  type: "button",
1347
1144
  "aria-label": "Clear search",
1348
- className: "p-2 text-[var(--color-text-muted)] rounded-full hover:bg-[var(--color-surface-muted)] hover:text-[var(--color-text)]",
1145
+ className: "flex items-center justify-center p-2 [@media(pointer:coarse)]:min-h-[var(--layout-touch-target)] [@media(pointer:coarse)]:min-w-[var(--layout-touch-target)] text-[var(--color-text-muted)] rounded-full hover:bg-[var(--color-surface-muted)] hover:text-[var(--color-text)]",
1349
1146
  onClick: () => {
1350
1147
  clear();
1351
1148
  onQueryChange?.("");
1352
1149
  },
1353
- children: /* @__PURE__ */ (0, import_jsx_runtime20.jsx)(import_lucide_react7.X, { "aria-hidden": "true", className: "w-6 h-6" })
1150
+ children: /* @__PURE__ */ (0, import_jsx_runtime16.jsx)(import_lucide_react7.X, { "aria-hidden": "true", className: "w-6 h-6" })
1354
1151
  }
1355
1152
  )
1356
1153
  ] }),
1357
- extraResults?.status && /* @__PURE__ */ (0, import_jsx_runtime20.jsx)("p", { role: "status", "aria-live": "polite", className: "sr-only", children: extraResults.status }),
1358
- showList && /* @__PURE__ */ (0, import_jsx_runtime20.jsxs)("ul", { className: "genre-search-results absolute z-40 w-full max-h-64 mt-1 overflow-y-auto bg-[var(--color-surface)] border border-[var(--color-border)] rounded-xl shadow-md", children: [
1359
- extraResults && results.length > 0 && /* @__PURE__ */ (0, import_jsx_runtime20.jsx)("li", { className: heading, children: "Genres" }),
1360
- results.map((genrePlaylist) => /* @__PURE__ */ (0, import_jsx_runtime20.jsx)("li", { children: /* @__PURE__ */ (0, import_jsx_runtime20.jsxs)(
1154
+ extraResults?.status && /* @__PURE__ */ (0, import_jsx_runtime16.jsx)("p", { role: "status", "aria-live": "polite", className: "sr-only", children: extraResults.status }),
1155
+ showList && /* @__PURE__ */ (0, import_jsx_runtime16.jsxs)("ul", { className: "genre-search-results absolute z-40 w-full max-h-[min(16rem,50dvh)] mt-1 overflow-y-auto bg-[var(--color-surface)] border border-[var(--color-border)] rounded-xl shadow-md", children: [
1156
+ extraResults && results.length > 0 && /* @__PURE__ */ (0, import_jsx_runtime16.jsx)("li", { className: heading, children: "Genres" }),
1157
+ results.map((genrePlaylist) => /* @__PURE__ */ (0, import_jsx_runtime16.jsx)("li", { children: /* @__PURE__ */ (0, import_jsx_runtime16.jsxs)(
1361
1158
  "button",
1362
1159
  {
1363
1160
  type: "button",
1364
- className: "w-full px-3 py-1.5 text-left hover:bg-[var(--color-surface-muted)]",
1161
+ className: "w-full px-3 py-1.5 [@media(pointer:coarse)]:min-h-[var(--layout-touch-target)] text-left hover:bg-[var(--color-surface-muted)]",
1365
1162
  onClick: () => {
1366
1163
  select(genrePlaylist);
1367
1164
  onQueryChange?.("");
1368
1165
  },
1369
1166
  children: [
1370
1167
  genrePlaylist.name,
1371
- genrePlaylist.isUmbrella && /* @__PURE__ */ (0, import_jsx_runtime20.jsx)("span", { className: "ml-2 text-xs italic text-[var(--color-text-muted)]", children: "umbrella" })
1168
+ genrePlaylist.isUmbrella && /* @__PURE__ */ (0, import_jsx_runtime16.jsx)("span", { className: "ml-2 text-xs italic text-[var(--color-text-muted)]", children: "umbrella" })
1372
1169
  ]
1373
1170
  }
1374
1171
  ) }, genrePlaylist.uuid)),
1375
- extraResults && extraItems.length > 0 && /* @__PURE__ */ (0, import_jsx_runtime20.jsx)("li", { className: heading, children: extraResults.label }),
1376
- extraItems.map((item) => /* @__PURE__ */ (0, import_jsx_runtime20.jsx)("li", { children: /* @__PURE__ */ (0, import_jsx_runtime20.jsx)(
1172
+ extraResults && extraItems.length > 0 && /* @__PURE__ */ (0, import_jsx_runtime16.jsx)("li", { className: heading, children: extraResults.label }),
1173
+ extraItems.map((item) => /* @__PURE__ */ (0, import_jsx_runtime16.jsx)("li", { children: /* @__PURE__ */ (0, import_jsx_runtime16.jsx)(
1377
1174
  "button",
1378
1175
  {
1379
1176
  type: "button",
1380
- className: "w-full px-3 py-1.5 text-left hover:bg-[var(--color-surface-muted)]",
1177
+ className: "w-full px-3 py-1.5 [@media(pointer:coarse)]:min-h-[var(--layout-touch-target)] text-left hover:bg-[var(--color-surface-muted)]",
1381
1178
  onClick: () => {
1382
1179
  setQuery("");
1383
1180
  onQueryChange?.("");
@@ -1390,45 +1187,282 @@ function GenreSearch({
1390
1187
  ] });
1391
1188
  }
1392
1189
 
1190
+ // src/genre-tree/GenreInfoPanelBody.tsx
1191
+ var import_player6 = require("@behindthemusictree/app-kit/player");
1192
+
1393
1193
  // src/genre-tree/GenreActionBand.tsx
1394
- var import_lucide_react8 = require("lucide-react");
1395
- var import_jsx_runtime21 = require("react/jsx-runtime");
1396
- function GenreActionBand({
1397
- node,
1398
- onPlayPause,
1399
- isPlaying = false,
1400
- onZoomIn,
1401
- onAddChild,
1402
- onRename,
1403
- onChangeParent,
1404
- additionalActions
1194
+ var import_lucide_react11 = require("lucide-react");
1195
+
1196
+ // src/genre-tree/ShareActionButton.tsx
1197
+ var import_lucide_react10 = require("lucide-react");
1198
+ var import_popup4 = require("@behindthemusictree/app-kit/popup");
1199
+
1200
+ // src/genre-tree/SharePopup.tsx
1201
+ var import_react8 = require("react");
1202
+ var import_lucide_react9 = require("lucide-react");
1203
+ var import_fa3 = require("react-icons/fa");
1204
+ var import_si = require("react-icons/si");
1205
+
1206
+ // src/genre-tree/ShareDestinationButton.tsx
1207
+ var import_jsx_runtime17 = require("react/jsx-runtime");
1208
+ 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";
1209
+ function Contents({ icon, label }) {
1210
+ return /* @__PURE__ */ (0, import_jsx_runtime17.jsxs)(import_jsx_runtime17.Fragment, { children: [
1211
+ /* @__PURE__ */ (0, import_jsx_runtime17.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 }),
1212
+ /* @__PURE__ */ (0, import_jsx_runtime17.jsx)("span", { className: "max-w-full truncate", children: label })
1213
+ ] });
1214
+ }
1215
+ function ShareDestinationButton({
1216
+ label,
1217
+ icon,
1218
+ href,
1219
+ onClick,
1220
+ disabled = false,
1221
+ ariaLabel
1405
1222
  }) {
1406
- const items = [];
1407
- const adminItems = [];
1408
- if (onPlayPause) {
1409
- items.push({
1410
- key: "play-pause",
1411
- label: isPlaying ? "Pause" : "Play",
1412
- icon: isPlaying ? /* @__PURE__ */ (0, import_jsx_runtime21.jsx)(import_lucide_react8.Pause, { "aria-hidden": "true", className: "w-5 h-5" }) : /* @__PURE__ */ (0, import_jsx_runtime21.jsx)(import_lucide_react8.Play, { "aria-hidden": "true", className: "w-5 h-5" }),
1413
- onClick: onPlayPause
1414
- });
1223
+ if (href) {
1224
+ return /* @__PURE__ */ (0, import_jsx_runtime17.jsx)("a", { href, target: "_blank", rel: "noopener noreferrer", "aria-label": ariaLabel, className, children: /* @__PURE__ */ (0, import_jsx_runtime17.jsx)(Contents, { icon, label }) });
1225
+ }
1226
+ return /* @__PURE__ */ (0, import_jsx_runtime17.jsx)("button", { type: "button", onClick, disabled, "aria-label": ariaLabel, className, children: /* @__PURE__ */ (0, import_jsx_runtime17.jsx)(Contents, { icon, label }) });
1227
+ }
1228
+
1229
+ // src/genre-tree/SharePreviewCard.tsx
1230
+ var import_lucide_react8 = require("lucide-react");
1231
+ var import_jsx_runtime18 = require("react/jsx-runtime");
1232
+ function SharePreviewCard({ target, url }) {
1233
+ const Icon = target.kind === "genre" ? import_lucide_react8.Music2 : import_lucide_react8.UserRound;
1234
+ const typeLabel = target.kind === "genre" ? "Genre" : "Artist";
1235
+ return /* @__PURE__ */ (0, import_jsx_runtime18.jsx)("div", { className: "rounded-3xl bg-[var(--color-surface-muted)] p-4", children: /* @__PURE__ */ (0, import_jsx_runtime18.jsxs)("div", { className: "flex min-h-32 overflow-hidden rounded-2xl bg-[var(--color-surface)] shadow-lg", children: [
1236
+ /* @__PURE__ */ (0, import_jsx_runtime18.jsx)("div", { className: "flex w-28 shrink-0 items-center justify-center bg-[var(--color-primary)]/15 text-[var(--color-primary)]", children: /* @__PURE__ */ (0, import_jsx_runtime18.jsx)(Icon, { "aria-hidden": "true", className: "h-10 w-10" }) }),
1237
+ /* @__PURE__ */ (0, import_jsx_runtime18.jsxs)("div", { className: "flex min-w-0 flex-1 flex-col justify-center gap-1 p-4", children: [
1238
+ /* @__PURE__ */ (0, import_jsx_runtime18.jsx)("span", { className: "text-xs font-medium uppercase tracking-wide text-[var(--color-text-muted)]", children: typeLabel }),
1239
+ /* @__PURE__ */ (0, import_jsx_runtime18.jsx)("span", { className: "truncate text-base font-semibold text-[var(--color-text)]", title: target.name, children: target.name }),
1240
+ /* @__PURE__ */ (0, import_jsx_runtime18.jsx)("span", { className: "truncate text-sm text-[var(--color-primary)]", title: url, children: url })
1241
+ ] })
1242
+ ] }) });
1243
+ }
1244
+
1245
+ // src/genre-tree/SharePopup.tsx
1246
+ var import_jsx_runtime19 = require("react/jsx-runtime");
1247
+ function resolveAbsoluteUrl(url) {
1248
+ try {
1249
+ const parsed = new URL(url, window.location.href);
1250
+ return parsed.protocol === "http:" || parsed.protocol === "https:" ? parsed.href : void 0;
1251
+ } catch {
1252
+ return void 0;
1253
+ }
1254
+ }
1255
+ async function copyText(text) {
1256
+ try {
1257
+ if (navigator.clipboard?.writeText) {
1258
+ await navigator.clipboard.writeText(text);
1259
+ return true;
1260
+ }
1261
+ } catch {
1262
+ }
1263
+ const input = document.createElement("textarea");
1264
+ input.value = text;
1265
+ input.setAttribute("readonly", "");
1266
+ input.style.position = "fixed";
1267
+ input.style.opacity = "0";
1268
+ document.body.append(input);
1269
+ input.select();
1270
+ try {
1271
+ return document.execCommand("copy");
1272
+ } catch {
1273
+ return false;
1274
+ } finally {
1275
+ input.remove();
1276
+ }
1277
+ }
1278
+ function SharePopup({ target, url, onClose }) {
1279
+ const [absoluteUrl, setAbsoluteUrl] = (0, import_react8.useState)(url);
1280
+ const [copyStatus, setCopyStatus] = (0, import_react8.useState)("idle");
1281
+ const [canUseNativeShare, setCanUseNativeShare] = (0, import_react8.useState)(false);
1282
+ const [shareError, setShareError] = (0, import_react8.useState)(null);
1283
+ const [toastId, setToastId] = (0, import_react8.useState)(0);
1284
+ (0, import_react8.useEffect)(() => {
1285
+ setAbsoluteUrl(resolveAbsoluteUrl(url) ?? "");
1286
+ setCopyStatus("idle");
1287
+ setShareError(null);
1288
+ }, [url]);
1289
+ (0, import_react8.useEffect)(() => {
1290
+ setCanUseNativeShare(typeof navigator !== "undefined" && typeof navigator.share === "function");
1291
+ }, []);
1292
+ const destinationUrls = (0, import_react8.useMemo)(() => {
1293
+ if (!absoluteUrl) return null;
1294
+ const message = `${target.name} \u2014 ${absoluteUrl}`;
1295
+ return {
1296
+ whatsapp: `https://wa.me/?text=${encodeURIComponent(message)}`,
1297
+ gmail: `https://mail.google.com/mail/?view=cm&fs=1&su=${encodeURIComponent(`Share: ${target.name}`)}&body=${encodeURIComponent(message)}`
1298
+ };
1299
+ }, [absoluteUrl, target.name]);
1300
+ const handleCopy = async () => {
1301
+ if (!absoluteUrl) {
1302
+ setCopyStatus("failed");
1303
+ return;
1304
+ }
1305
+ setShareError(null);
1306
+ const copied = await copyText(absoluteUrl);
1307
+ setCopyStatus(copied ? "idle" : "failed");
1308
+ if (copied) setToastId((id) => id + 1);
1309
+ };
1310
+ const dismissToast = (0, import_react8.useCallback)(() => setToastId(0), []);
1311
+ const handleNativeShare = async () => {
1312
+ if (!absoluteUrl || !navigator.share) return;
1313
+ setShareError(null);
1314
+ try {
1315
+ await navigator.share({ title: target.name, url: absoluteUrl });
1316
+ } catch (error) {
1317
+ if (error instanceof Error && error.name === "AbortError") return;
1318
+ setShareError("Could not open the share options.");
1319
+ }
1320
+ };
1321
+ return /* @__PURE__ */ (0, import_jsx_runtime19.jsxs)(import_jsx_runtime19.Fragment, { children: [
1322
+ /* @__PURE__ */ (0, import_jsx_runtime19.jsx)(
1323
+ BasePopup_default,
1324
+ {
1325
+ title: "Share",
1326
+ onClose,
1327
+ isDismissable: true,
1328
+ trapFocus: true,
1329
+ closeOnEscape: true,
1330
+ closeOnBackdrop: true,
1331
+ className: "!w-full !max-w-[28rem] overflow-hidden !rounded-[2rem] !bg-[var(--color-surface)] shadow-2xl",
1332
+ overlayClassName: "!bg-black/50 p-4 backdrop-blur-[2px]",
1333
+ titleClassName: "!rounded-none !bg-transparent !px-6 !pb-2 !pt-6",
1334
+ titleTextClassName: "!text-xl !font-semibold !text-[var(--color-text)]",
1335
+ 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)]",
1336
+ contentClassName: "!rounded-none !bg-transparent !px-6 !pb-6 !pt-2",
1337
+ children: /* @__PURE__ */ (0, import_jsx_runtime19.jsxs)("div", { className: "flex flex-col gap-4", children: [
1338
+ /* @__PURE__ */ (0, import_jsx_runtime19.jsx)(SharePreviewCard, { target, url: absoluteUrl || url }),
1339
+ /* @__PURE__ */ (0, import_jsx_runtime19.jsxs)("div", { className: "grid grid-cols-3 gap-1 rounded-3xl bg-[var(--color-surface)] p-3 sm:grid-cols-4", children: [
1340
+ /* @__PURE__ */ (0, import_jsx_runtime19.jsx)(
1341
+ ShareDestinationButton,
1342
+ {
1343
+ label: "Copy Link",
1344
+ icon: /* @__PURE__ */ (0, import_jsx_runtime19.jsx)(import_lucide_react9.Link2, { "aria-hidden": "true", className: "h-6 w-6" }),
1345
+ onClick: () => void handleCopy(),
1346
+ ariaLabel: "Copy link",
1347
+ disabled: !absoluteUrl
1348
+ }
1349
+ ),
1350
+ /* @__PURE__ */ (0, import_jsx_runtime19.jsx)(
1351
+ ShareDestinationButton,
1352
+ {
1353
+ label: "WhatsApp",
1354
+ icon: /* @__PURE__ */ (0, import_jsx_runtime19.jsx)(import_fa3.FaWhatsapp, { "aria-hidden": "true", className: "h-9 w-9 text-[#25D366]" }),
1355
+ href: destinationUrls?.whatsapp,
1356
+ ariaLabel: `Share ${target.name} via WhatsApp`,
1357
+ disabled: !absoluteUrl
1358
+ }
1359
+ ),
1360
+ /* @__PURE__ */ (0, import_jsx_runtime19.jsx)(
1361
+ ShareDestinationButton,
1362
+ {
1363
+ label: "Gmail",
1364
+ icon: /* @__PURE__ */ (0, import_jsx_runtime19.jsx)(import_si.SiGmail, { "aria-hidden": "true", className: "h-8 w-8 text-[#EA4335]" }),
1365
+ href: destinationUrls?.gmail,
1366
+ ariaLabel: `Email ${target.name} with Gmail`,
1367
+ disabled: !absoluteUrl
1368
+ }
1369
+ ),
1370
+ canUseNativeShare && /* @__PURE__ */ (0, import_jsx_runtime19.jsx)(
1371
+ ShareDestinationButton,
1372
+ {
1373
+ label: "More",
1374
+ icon: /* @__PURE__ */ (0, import_jsx_runtime19.jsx)(import_lucide_react9.Share2, { "aria-hidden": "true", className: "h-6 w-6" }),
1375
+ onClick: () => void handleNativeShare(),
1376
+ ariaLabel: "More sharing options",
1377
+ disabled: !absoluteUrl
1378
+ }
1379
+ )
1380
+ ] }),
1381
+ copyStatus === "failed" && /* @__PURE__ */ (0, import_jsx_runtime19.jsx)("p", { role: "alert", className: "text-center text-sm text-[var(--color-red-500)]", children: "Could not copy the link." }),
1382
+ shareError && /* @__PURE__ */ (0, import_jsx_runtime19.jsx)("p", { role: "alert", className: "text-center text-sm text-[var(--color-red-500)]", children: shareError })
1383
+ ] })
1384
+ }
1385
+ ),
1386
+ toastId > 0 && /* @__PURE__ */ (0, import_jsx_runtime19.jsx)(Toast, { message: "Link copied", onClose: dismissToast }, toastId)
1387
+ ] });
1388
+ }
1389
+
1390
+ // src/genre-tree/ShareActionButton.tsx
1391
+ var import_jsx_runtime20 = require("react/jsx-runtime");
1392
+ function ShareActionButton({ target, url }) {
1393
+ const { showPopup, hidePopup } = (0, import_popup4.usePopup)();
1394
+ const openSharePopup = () => {
1395
+ showPopup(/* @__PURE__ */ (0, import_jsx_runtime20.jsx)(SharePopup, { target, url, onClose: hidePopup }));
1396
+ };
1397
+ return /* @__PURE__ */ (0, import_jsx_runtime20.jsxs)(
1398
+ "button",
1399
+ {
1400
+ type: "button",
1401
+ onClick: openSharePopup,
1402
+ className: "group flex flex-col items-center gap-1 w-16 text-xs text-center text-[var(--color-text)] focus-visible:outline-none",
1403
+ "aria-label": `Share ${target.name}`,
1404
+ title: `Share ${target.name}`,
1405
+ children: [
1406
+ /* @__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)]", children: /* @__PURE__ */ (0, import_jsx_runtime20.jsx)(import_lucide_react10.Share2, { "aria-hidden": "true", className: "w-5 h-5" }) }),
1407
+ "Share"
1408
+ ]
1409
+ }
1410
+ );
1411
+ }
1412
+
1413
+ // src/genre-tree/GenreActionBand.tsx
1414
+ var import_jsx_runtime21 = require("react/jsx-runtime");
1415
+ function GenreActionBand({
1416
+ node,
1417
+ onPlayPause,
1418
+ isPlaying = false,
1419
+ onZoomIn,
1420
+ onAddChild,
1421
+ onRename,
1422
+ onChangeParent,
1423
+ side,
1424
+ onToggleSide,
1425
+ toggleSideDisabledReason,
1426
+ additionalActions,
1427
+ shareTarget,
1428
+ shareUrl
1429
+ }) {
1430
+ const items = [];
1431
+ const adminItems = [];
1432
+ if (onPlayPause) {
1433
+ items.push({
1434
+ key: "play-pause",
1435
+ label: isPlaying ? "Pause" : "Play",
1436
+ icon: isPlaying ? /* @__PURE__ */ (0, import_jsx_runtime21.jsx)(import_lucide_react11.Pause, { "aria-hidden": "true", className: "w-5 h-5" }) : /* @__PURE__ */ (0, import_jsx_runtime21.jsx)(import_lucide_react11.Play, { "aria-hidden": "true", className: "w-5 h-5" }),
1437
+ onClick: onPlayPause
1438
+ });
1415
1439
  }
1416
1440
  if (onZoomIn) {
1417
1441
  items.push({
1418
1442
  key: "zoom-in",
1419
1443
  label: "Focus",
1420
- icon: /* @__PURE__ */ (0, import_jsx_runtime21.jsx)(import_lucide_react8.ZoomIn, { "aria-hidden": "true", className: "w-5 h-5" }),
1444
+ icon: /* @__PURE__ */ (0, import_jsx_runtime21.jsx)(import_lucide_react11.ZoomIn, { "aria-hidden": "true", className: "w-5 h-5" }),
1421
1445
  onClick: onZoomIn
1422
1446
  });
1423
1447
  }
1424
1448
  if (onAddChild) {
1425
- adminItems.push({ key: "add-child", label: "Add", icon: /* @__PURE__ */ (0, import_jsx_runtime21.jsx)(import_lucide_react8.Plus, { "aria-hidden": "true", className: "w-5 h-5" }), onClick: onAddChild });
1449
+ adminItems.push({ key: "add-child", label: "Add", icon: /* @__PURE__ */ (0, import_jsx_runtime21.jsx)(import_lucide_react11.Plus, { "aria-hidden": "true", className: "w-5 h-5" }), onClick: onAddChild });
1426
1450
  }
1427
1451
  if (onRename) {
1428
- adminItems.push({ key: "rename", label: "Rename", icon: /* @__PURE__ */ (0, import_jsx_runtime21.jsx)(import_lucide_react8.Pencil, { "aria-hidden": "true", className: "w-5 h-5" }), onClick: onRename });
1452
+ adminItems.push({ key: "rename", label: "Rename", icon: /* @__PURE__ */ (0, import_jsx_runtime21.jsx)(import_lucide_react11.Pencil, { "aria-hidden": "true", className: "w-5 h-5" }), onClick: onRename });
1429
1453
  }
1430
1454
  if (onChangeParent) {
1431
- adminItems.push({ key: "change-parent", label: "Parent", icon: /* @__PURE__ */ (0, import_jsx_runtime21.jsx)(import_lucide_react8.MoveUpRight, { "aria-hidden": "true", className: "w-5 h-5" }), onClick: onChangeParent });
1455
+ adminItems.push({ key: "change-parent", label: "Parent", icon: /* @__PURE__ */ (0, import_jsx_runtime21.jsx)(import_lucide_react11.MoveUpRight, { "aria-hidden": "true", className: "w-5 h-5" }), onClick: onChangeParent });
1456
+ }
1457
+ if (onToggleSide) {
1458
+ adminItems.push({
1459
+ key: "toggle-side",
1460
+ label: side === "pop" ? "Make core" : "Make pop",
1461
+ icon: /* @__PURE__ */ (0, import_jsx_runtime21.jsx)(import_lucide_react11.ArrowLeftRight, { "aria-hidden": "true", className: "w-5 h-5" }),
1462
+ onClick: onToggleSide,
1463
+ disabled: !!toggleSideDisabledReason,
1464
+ title: toggleSideDisabledReason
1465
+ });
1432
1466
  }
1433
1467
  for (const action of additionalActions?.(node) ?? []) {
1434
1468
  items.push({
@@ -1439,12 +1473,13 @@ function GenreActionBand({
1439
1473
  disabled: action.enabled ? !action.enabled(node) : false
1440
1474
  });
1441
1475
  }
1442
- if (items.length === 0 && adminItems.length === 0) return null;
1476
+ if (items.length === 0 && (!shareTarget || !shareUrl) && adminItems.length === 0) return null;
1443
1477
  const renderItems = (list) => list.map((item) => /* @__PURE__ */ (0, import_jsx_runtime21.jsxs)(
1444
1478
  "button",
1445
1479
  {
1446
1480
  type: "button",
1447
1481
  disabled: item.disabled,
1482
+ title: item.title,
1448
1483
  onClick: item.onClick,
1449
1484
  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",
1450
1485
  children: [
@@ -1455,7 +1490,10 @@ function GenreActionBand({
1455
1490
  item.key
1456
1491
  ));
1457
1492
  return /* @__PURE__ */ (0, import_jsx_runtime21.jsxs)("div", { className: "genre-action-band border-b border-[var(--color-border)]", children: [
1458
- items.length > 0 && /* @__PURE__ */ (0, import_jsx_runtime21.jsx)("div", { className: "flex flex-wrap gap-1 px-3.5 py-3", children: renderItems(items) }),
1493
+ (items.length > 0 || shareTarget && shareUrl) && /* @__PURE__ */ (0, import_jsx_runtime21.jsxs)("div", { className: "flex flex-wrap gap-1 px-3.5 py-3", children: [
1494
+ renderItems(items),
1495
+ shareTarget && shareUrl && /* @__PURE__ */ (0, import_jsx_runtime21.jsx)(ShareActionButton, { target: shareTarget, url: shareUrl })
1496
+ ] }),
1459
1497
  adminItems.length > 0 && /* @__PURE__ */ (0, import_jsx_runtime21.jsxs)(
1460
1498
  "section",
1461
1499
  {
@@ -1470,17 +1508,392 @@ function GenreActionBand({
1470
1508
  ] });
1471
1509
  }
1472
1510
 
1511
+ // src/genre-tree/GenreDetailExtras.tsx
1512
+ var import_ui8 = require("@behindthemusictree/ui");
1513
+ var import_genre_tree9 = require("@behindthemusictree/app-kit/genre-tree");
1514
+
1515
+ // src/genre-tree/ClampedText.tsx
1516
+ var import_react9 = require("react");
1517
+ var import_jsx_runtime22 = require("react/jsx-runtime");
1518
+ function ClampedText({ children, className: className2 = "" }) {
1519
+ const ref = (0, import_react9.useRef)(null);
1520
+ const [expanded, setExpanded] = (0, import_react9.useState)(false);
1521
+ const [overflows, setOverflows] = (0, import_react9.useState)(false);
1522
+ (0, import_react9.useLayoutEffect)(() => {
1523
+ const element = ref.current;
1524
+ if (!element || expanded) return;
1525
+ setOverflows(element.scrollHeight > element.clientHeight);
1526
+ }, [children, expanded]);
1527
+ return /* @__PURE__ */ (0, import_jsx_runtime22.jsxs)(import_jsx_runtime22.Fragment, { children: [
1528
+ /* @__PURE__ */ (0, import_jsx_runtime22.jsx)("p", { ref, className: `${className2} ${expanded ? "" : "line-clamp-3"}`, children }),
1529
+ (overflows || expanded) && /* @__PURE__ */ (0, import_jsx_runtime22.jsx)(
1530
+ "button",
1531
+ {
1532
+ type: "button",
1533
+ "aria-expanded": expanded,
1534
+ onClick: () => setExpanded((value) => !value),
1535
+ className: "mt-1 text-xs text-[var(--color-text-muted)] underline hover:text-[var(--color-text)]",
1536
+ children: expanded ? "Show less" : "Show more"
1537
+ }
1538
+ )
1539
+ ] });
1540
+ }
1541
+
1542
+ // src/genre-tree/GenrePlaylistTracks.tsx
1543
+ var import_react10 = require("react");
1544
+ var import_react_query = require("@tanstack/react-query");
1545
+ var import_zod = require("zod");
1546
+ var import_fa4 = require("react-icons/fa");
1547
+ var import_ui7 = require("@behindthemusictree/ui");
1548
+ var import_auth2 = require("@behindthemusictree/app-kit/auth");
1549
+ var import_transport4 = require("@behindthemusictree/app-kit/transport");
1550
+ var import_transport5 = require("@behindthemusictree/app-kit/transport");
1551
+ var import_transport6 = require("@behindthemusictree/app-kit/transport");
1552
+ var import_player4 = require("@behindthemusictree/app-kit/player");
1553
+ var import_player5 = require("@behindthemusictree/app-kit/player");
1554
+ var import_popup5 = require("@behindthemusictree/app-kit/popup");
1555
+ var import_transport7 = require("@behindthemusictree/app-kit/transport");
1556
+ var import_genre_tree4 = require("@behindthemusictree/app-kit/genre-tree");
1557
+ var import_genre_tree5 = require("@behindthemusictree/app-kit/genre-tree");
1558
+ var import_genre_tree6 = require("@behindthemusictree/app-kit/genre-tree");
1559
+ var import_genre_tree7 = require("@behindthemusictree/app-kit/genre-tree");
1560
+
1561
+ // src/genre-tree/MusicBrainzRecordingLink.tsx
1562
+ var import_genre_tree3 = require("@behindthemusictree/app-kit/genre-tree");
1563
+ var import_jsx_runtime23 = require("react/jsx-runtime");
1564
+ function MusicBrainzRecordingLink({
1565
+ song,
1566
+ children,
1567
+ className: className2 = ""
1568
+ }) {
1569
+ const url = (0, import_genre_tree3.musicBrainzRecordingUrl)(song);
1570
+ if (!url) return /* @__PURE__ */ (0, import_jsx_runtime23.jsx)(import_jsx_runtime23.Fragment, { children });
1571
+ return /* @__PURE__ */ (0, import_jsx_runtime23.jsx)(
1572
+ "a",
1573
+ {
1574
+ href: url,
1575
+ target: "_blank",
1576
+ rel: "noopener noreferrer",
1577
+ "aria-label": `View ${song.title} on MusicBrainz`,
1578
+ className: className2,
1579
+ onClick: (event) => event.stopPropagation(),
1580
+ children
1581
+ }
1582
+ );
1583
+ }
1584
+
1585
+ // src/genre-tree/GenrePlaylistTracks.tsx
1586
+ var import_genre_tree8 = require("@behindthemusictree/app-kit/genre-tree");
1587
+ var import_track_noun5 = require("@behindthemusictree/app-kit/track-noun");
1588
+ var import_jsx_runtime24 = require("react/jsx-runtime");
1589
+ var CATALOGUE_PAGE_SIZE = 100;
1590
+ var SongPageSchema = (0, import_transport5.PaginatedResponseSchema)(import_genre_tree6.SongDetailedSchema).extend({
1591
+ overallTotal: import_zod.z.number().nullable(),
1592
+ page: import_zod.z.number().nullable(),
1593
+ totalPages: import_zod.z.number().nullable()
1594
+ });
1595
+ var TEXT_CELL = "min-w-0 flex-1 truncate [@container(min-width:40rem)]:flex [@container(min-width:40rem)]:gap-2";
1596
+ var TITLE_CELL = "[@container(min-width:40rem)]:flex-[2] [@container(min-width:40rem)]:truncate";
1597
+ var COLUMN_CELL = "[@container(min-width:40rem)]:flex-1 [@container(min-width:40rem)]:truncate";
1598
+ var ALBUM_CELL = `hidden [@container(min-width:40rem)]:block ${COLUMN_CELL}`;
1599
+ var SEPARATOR = "[@container(min-width:40rem)]:hidden";
1600
+ var nextCursor = (next) => {
1601
+ const cursor = new URL(next).searchParams.get("cursor");
1602
+ if (!cursor) throw new Error(`Songs next link has no cursor: ${next}`);
1603
+ return cursor;
1604
+ };
1605
+ function GenrePlaylistTracks({
1606
+ genreUuid,
1607
+ genrePlaylist,
1608
+ scope,
1609
+ getBackendBaseUrl,
1610
+ renderArtistLink,
1611
+ renderSongLeading,
1612
+ renderSongActions,
1613
+ renderTracksToolbar,
1614
+ artist = null
1615
+ }) {
1616
+ const { fetchGenrePlaylistTracksPage, playNewTrackListFromGenrePlaylist, playNewTrackListFromArtist } = (0, import_genre_tree7.useTrackList)();
1617
+ const { session, sessionRestored } = (0, import_auth2.useSession)();
1618
+ const { playerTrackObject, playState, handlePlayPauseAction } = (0, import_player4.usePlayer)();
1619
+ const { showPopup } = (0, import_popup5.usePopup)();
1620
+ const trackNoun = (0, import_track_noun5.useTrackNoun)();
1621
+ const { uuid } = genrePlaylist;
1622
+ const { fetch } = (0, import_transport4.useFetchWrapper)(getBackendBaseUrl);
1623
+ const { data, isPending, isError, refetch, hasNextPage, isFetchingNextPage, fetchNextPage } = (0, import_react_query.useInfiniteQuery)({
1624
+ queryKey: scope === "me" ? import_genre_tree4.genrePlaylistQueryKeys.me.tracks(uuid) : artist ? [...import_genre_tree5.libraryQueryKeys.reference.songs.byGenre(getBackendBaseUrl(), genreUuid), "artist", artist.uuid] : import_genre_tree5.libraryQueryKeys.reference.songs.byGenre(getBackendBaseUrl(), genreUuid),
1625
+ queryFn: async ({ pageParam }) => {
1626
+ if (scope === "me") return fetchGenrePlaylistTracksPage(uuid, scope, Number(pageParam));
1627
+ const response = await fetch(
1628
+ import_genre_tree5.libraryEndpoints.reference.songs.list(),
1629
+ true,
1630
+ false,
1631
+ {},
1632
+ { genre: genreUuid, ...artist && { artist: artist.uuid }, pageSize: CATALOGUE_PAGE_SIZE, withTotal: false, ...pageParam && { cursor: pageParam } }
1633
+ );
1634
+ const parsed = (0, import_transport6.parseWithLog)(SongPageSchema, response, "GenrePlaylistTracks");
1635
+ return { tracks: parsed.results, total: parsed.overallTotal, nextPage: parsed.next ? nextCursor(parsed.next) : null };
1636
+ },
1637
+ initialPageParam: scope === "me" ? 1 : "",
1638
+ getNextPageParam: (lastPage) => lastPage.nextPage ?? void 0,
1639
+ enabled: scope !== "me" || sessionRestored && !!session?.accessToken
1640
+ });
1641
+ const sentinelRef = (0, import_react10.useRef)(null);
1642
+ (0, import_react10.useEffect)(() => {
1643
+ const sentinel = sentinelRef.current;
1644
+ if (!hasNextPage || isFetchingNextPage || !sentinel) return;
1645
+ const observer = new IntersectionObserver((entries) => {
1646
+ if (entries.some((entry) => entry.isIntersecting)) {
1647
+ void fetchNextPage();
1648
+ }
1649
+ });
1650
+ observer.observe(sentinel);
1651
+ return () => observer.disconnect();
1652
+ }, [hasNextPage, isFetchingNextPage, fetchNextPage]);
1653
+ const pages = data?.pages ?? [];
1654
+ const tracks = pages.flatMap((page) => page.tracks);
1655
+ const total = pages[0]?.total ?? void 0;
1656
+ const play = (startSongUuid) => {
1657
+ const started = artist ? playNewTrackListFromArtist(artist, scope, { startSongUuid, ordering: "title", genreUuid }) : playNewTrackListFromGenrePlaylist(genrePlaylist, scope, { startSongUuid });
1658
+ started.catch((error) => {
1659
+ console.error("Failed to play genre playlist:", error);
1660
+ showPopup(/* @__PURE__ */ (0, import_jsx_runtime24.jsx)(InternalErrorPopup, { errorCode: import_transport7.ErrorCode.CLIENT_UNKNOWN }));
1661
+ });
1662
+ };
1663
+ return /* @__PURE__ */ (0, import_jsx_runtime24.jsxs)("div", { className: "gtv-info-panel-children", children: [
1664
+ /* @__PURE__ */ (0, import_jsx_runtime24.jsxs)("span", { className: "gtv-info-panel-children-title", children: [
1665
+ (0, import_track_noun5.capitalize)(trackNoun.plural),
1666
+ total !== void 0 && ` (${total})`
1667
+ ] }),
1668
+ renderTracksToolbar?.(),
1669
+ isPending ? /* @__PURE__ */ (0, import_jsx_runtime24.jsx)(import_ui7.Skeleton, { className: "h-4 w-3/4", "aria-busy": "true" }) : isError && tracks.length === 0 ? /* @__PURE__ */ (0, import_jsx_runtime24.jsxs)("p", { role: "alert", children: [
1670
+ "Couldn't load ",
1671
+ trackNoun.plural,
1672
+ ".",
1673
+ " ",
1674
+ /* @__PURE__ */ (0, import_jsx_runtime24.jsx)("button", { type: "button", className: "underline", onClick: () => void refetch(), children: "Retry" })
1675
+ ] }) : tracks.length === 0 ? /* @__PURE__ */ (0, import_jsx_runtime24.jsx)("p", { children: "\u2014" }) : /* @__PURE__ */ (0, import_jsx_runtime24.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: [
1676
+ tracks.map((track, index) => {
1677
+ const label = track.artists?.length ? `${track.title} \u2014 ${track.artists.map((artist2) => artist2.name).join(", ")}` : track.title;
1678
+ const isCurrent = playerTrackObject?.track.id === track.uuid;
1679
+ const isPlaying = isCurrent && playState === import_player5.PlayStates.PLAYING;
1680
+ const text = /* @__PURE__ */ (0, import_jsx_runtime24.jsxs)("span", { className: TEXT_CELL, children: [
1681
+ /* @__PURE__ */ (0, import_jsx_runtime24.jsx)("span", { className: TITLE_CELL, children: (0, import_genre_tree6.isSong)(track) ? /* @__PURE__ */ (0, import_jsx_runtime24.jsx)(MusicBrainzRecordingLink, { song: track, className: "hover:underline focus-visible:underline", children: track.title }) : track.title }),
1682
+ track.artists?.length ? /* @__PURE__ */ (0, import_jsx_runtime24.jsx)("span", { className: SEPARATOR, children: " \u2014 " }) : null,
1683
+ /* @__PURE__ */ (0, import_jsx_runtime24.jsx)("span", { className: COLUMN_CELL, children: track.artists?.length ? /* @__PURE__ */ (0, import_jsx_runtime24.jsx)(import_genre_tree8.ArtistNames, { artists: track.artists, renderArtistLink }) : null }),
1684
+ track.album && /* @__PURE__ */ (0, import_jsx_runtime24.jsx)("span", { className: ALBUM_CELL, children: track.album.name })
1685
+ ] });
1686
+ const leading = renderSongLeading?.(track, index);
1687
+ const actions = renderSongActions?.(track);
1688
+ const isUnplayable = (0, import_genre_tree6.isSong)(track) && (0, import_genre_tree6.songUnplayableReason)(track) !== null;
1689
+ if (isUnplayable) {
1690
+ return /* @__PURE__ */ (0, import_jsx_runtime24.jsxs)("li", { className: "flex min-h-8 items-center gap-1 px-1 opacity-50", children: [
1691
+ leading,
1692
+ /* @__PURE__ */ (0, import_jsx_runtime24.jsx)(
1693
+ "button",
1694
+ {
1695
+ type: "button",
1696
+ disabled: true,
1697
+ "aria-disabled": "true",
1698
+ "aria-label": label,
1699
+ className: "flex h-6 w-6 shrink-0 items-center justify-center rounded-full opacity-0 cursor-not-allowed",
1700
+ children: /* @__PURE__ */ (0, import_jsx_runtime24.jsx)(import_fa4.FaPlay, { size: 10 })
1701
+ }
1702
+ ),
1703
+ text,
1704
+ actions
1705
+ ] }, track.uuid);
1706
+ }
1707
+ return /* @__PURE__ */ (0, import_jsx_runtime24.jsxs)(
1708
+ "li",
1709
+ {
1710
+ 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)]" : ""}`,
1711
+ children: [
1712
+ leading,
1713
+ /* @__PURE__ */ (0, import_jsx_runtime24.jsx)(
1714
+ "button",
1715
+ {
1716
+ type: "button",
1717
+ "aria-label": `${isPlaying ? "Pause" : "Play"} ${label}`,
1718
+ 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"}`,
1719
+ onClick: () => isCurrent ? handlePlayPauseAction() : play(track.uuid),
1720
+ children: isCurrent && playState === import_player5.PlayStates.LOADING ? /* @__PURE__ */ (0, import_jsx_runtime24.jsx)(import_ui7.RingLoader, { size: 10 }) : isPlaying ? /* @__PURE__ */ (0, import_jsx_runtime24.jsx)(import_fa4.FaPause, { size: 10 }) : /* @__PURE__ */ (0, import_jsx_runtime24.jsx)(import_fa4.FaPlay, { size: 10 })
1721
+ }
1722
+ ),
1723
+ text,
1724
+ actions
1725
+ ]
1726
+ },
1727
+ track.uuid
1728
+ );
1729
+ }),
1730
+ hasNextPage && /* @__PURE__ */ (0, import_jsx_runtime24.jsx)("li", { ref: sentinelRef, "aria-hidden": "true", className: "h-4" })
1731
+ ] })
1732
+ ] });
1733
+ }
1734
+
1735
+ // src/genre-tree/GenreDetailExtras.tsx
1736
+ var import_track_noun6 = require("@behindthemusictree/app-kit/track-noun");
1737
+ var import_si2 = require("react-icons/si");
1738
+ var import_jsx_runtime25 = require("react/jsx-runtime");
1739
+ function GenreDetailExtras({
1740
+ genreUuid,
1741
+ genrePlaylist,
1742
+ wikidataId,
1743
+ scope,
1744
+ getBackendBaseUrl,
1745
+ criteriaOverviewSchema,
1746
+ renderGenreDetailExtras,
1747
+ renderArtistLink,
1748
+ renderSummary,
1749
+ renderEssentialTracks,
1750
+ family,
1751
+ ...trackSlots
1752
+ }) {
1753
+ const trackNoun = (0, import_track_noun6.useTrackNoun)();
1754
+ const { data: overview, isPending } = (0, import_genre_tree9.useFetchGenreOverview)(
1755
+ genreUuid,
1756
+ scope,
1757
+ getBackendBaseUrl,
1758
+ criteriaOverviewSchema
1759
+ );
1760
+ if (isPending) {
1761
+ return /* @__PURE__ */ (0, import_jsx_runtime25.jsxs)(import_jsx_runtime25.Fragment, { children: [
1762
+ /* @__PURE__ */ (0, import_jsx_runtime25.jsxs)("div", { className: "gtv-info-panel-children", "aria-busy": "true", "data-testid": "genre-detail-extras-skeleton", children: [
1763
+ /* @__PURE__ */ (0, import_jsx_runtime25.jsx)("span", { className: "gtv-info-panel-children-title", children: "Summary" }),
1764
+ /* @__PURE__ */ (0, import_jsx_runtime25.jsx)(import_ui8.Skeleton, { className: "h-4 w-3/4" })
1765
+ ] }),
1766
+ family
1767
+ ] });
1768
+ }
1769
+ if (!overview) return /* @__PURE__ */ (0, import_jsx_runtime25.jsx)(import_jsx_runtime25.Fragment, { children: family });
1770
+ const { summary, summarySourceUrl, essentialSongs } = overview;
1771
+ const wikidataUrl = wikidataId ? `https://www.wikidata.org/wiki/${encodeURIComponent(wikidataId)}` : null;
1772
+ return /* @__PURE__ */ (0, import_jsx_runtime25.jsxs)(import_jsx_runtime25.Fragment, { children: [
1773
+ (summarySourceUrl || wikidataUrl) && /* @__PURE__ */ (0, import_jsx_runtime25.jsxs)("div", { className: "gtv-info-panel-children text-[0.8125rem] leading-[1.45] text-[var(--color-text)]", children: [
1774
+ /* @__PURE__ */ (0, import_jsx_runtime25.jsx)("span", { className: "gtv-info-panel-children-title", children: "Links" }),
1775
+ /* @__PURE__ */ (0, import_jsx_runtime25.jsxs)("div", { className: "mt-1 flex flex-wrap gap-x-3 gap-y-1 text-xs", children: [
1776
+ summarySourceUrl && /* @__PURE__ */ (0, import_jsx_runtime25.jsxs)(
1777
+ "a",
1778
+ {
1779
+ href: summarySourceUrl,
1780
+ target: "_blank",
1781
+ rel: "noopener noreferrer",
1782
+ className: "inline-flex items-center gap-1.5 text-[var(--color-text-muted)] hover:text-[var(--color-text)]",
1783
+ children: [
1784
+ /* @__PURE__ */ (0, import_jsx_runtime25.jsx)(import_si2.SiWikipedia, { "aria-hidden": "true", className: "h-3.5 w-3.5" }),
1785
+ "Wikipedia"
1786
+ ]
1787
+ }
1788
+ ),
1789
+ wikidataUrl && /* @__PURE__ */ (0, import_jsx_runtime25.jsxs)(
1790
+ "a",
1791
+ {
1792
+ href: wikidataUrl,
1793
+ target: "_blank",
1794
+ rel: "noopener noreferrer",
1795
+ className: "inline-flex items-center gap-1.5 text-[var(--color-text-muted)] hover:text-[var(--color-text)]",
1796
+ children: [
1797
+ /* @__PURE__ */ (0, import_jsx_runtime25.jsx)(import_si2.SiWikidata, { "aria-hidden": "true", className: "h-3.5 w-3.5" }),
1798
+ "Wikidata"
1799
+ ]
1800
+ }
1801
+ )
1802
+ ] })
1803
+ ] }),
1804
+ renderSummary ? renderSummary(overview) : /* @__PURE__ */ (0, import_jsx_runtime25.jsxs)("div", { className: "gtv-info-panel-children text-[0.8125rem] leading-[1.45] text-[var(--color-text)]", children: [
1805
+ /* @__PURE__ */ (0, import_jsx_runtime25.jsx)("span", { className: "gtv-info-panel-children-title", children: "Summary" }),
1806
+ /* @__PURE__ */ (0, import_jsx_runtime25.jsx)(ClampedText, { className: "mt-1", children: summary ?? "\u2014" }, genreUuid)
1807
+ ] }),
1808
+ family,
1809
+ renderGenreDetailExtras?.(overview),
1810
+ renderEssentialTracks ? renderEssentialTracks(overview) : essentialSongs && /* @__PURE__ */ (0, import_jsx_runtime25.jsxs)("div", { className: "gtv-info-panel-children text-[0.8125rem] leading-[1.45] text-[var(--color-text)]", children: [
1811
+ /* @__PURE__ */ (0, import_jsx_runtime25.jsxs)("span", { className: "gtv-info-panel-children-title", children: [
1812
+ "Essential ",
1813
+ trackNoun.plural
1814
+ ] }),
1815
+ essentialSongs.length > 0 ? /* @__PURE__ */ (0, import_jsx_runtime25.jsx)("ul", { className: "mt-1 divide-y divide-[var(--color-border)]", children: essentialSongs.map((track) => /* @__PURE__ */ (0, import_jsx_runtime25.jsx)("li", { className: "py-1", children: track.title }, track.uuid)) }) : /* @__PURE__ */ (0, import_jsx_runtime25.jsx)("p", { className: "mt-1", children: "\u2014" })
1816
+ ] }),
1817
+ /* @__PURE__ */ (0, import_jsx_runtime25.jsx)(
1818
+ GenrePlaylistTracks,
1819
+ {
1820
+ genreUuid,
1821
+ genrePlaylist,
1822
+ scope,
1823
+ getBackendBaseUrl,
1824
+ renderArtistLink,
1825
+ ...trackSlots
1826
+ }
1827
+ )
1828
+ ] });
1829
+ }
1830
+
1831
+ // src/genre-tree/GenreInfoPanelBody.tsx
1832
+ var import_jsx_runtime26 = require("react/jsx-runtime");
1833
+ var import_react11 = require("react");
1834
+ function GenreNodeActionBand({ genrePlaylist, playback, ...bandProps }) {
1835
+ const { isPlaying, setIsPlaying } = (0, import_player6.usePlayer)();
1836
+ const isCurrentTrackList = playback.playingNodeId === genrePlaylist.uuid;
1837
+ const handlePlayPause = () => {
1838
+ if (isCurrentTrackList) {
1839
+ setIsPlaying(!isPlaying);
1840
+ return;
1841
+ }
1842
+ playback.play(genrePlaylist).catch((error) => {
1843
+ console.error("Failed to load genre playlist tracks:", error);
1844
+ });
1845
+ };
1846
+ return /* @__PURE__ */ (0, import_jsx_runtime26.jsx)(
1847
+ GenreActionBand,
1848
+ {
1849
+ ...bandProps,
1850
+ onPlayPause: genrePlaylist.tracksCount > 0 ? handlePlayPause : void 0,
1851
+ isPlaying: isCurrentTrackList && isPlaying
1852
+ }
1853
+ );
1854
+ }
1855
+ function GenreInfoPanelBody({
1856
+ toolbar,
1857
+ family,
1858
+ genreUuid,
1859
+ genrePlaylist,
1860
+ wikidataId,
1861
+ scope,
1862
+ getBackendBaseUrl,
1863
+ playback,
1864
+ band,
1865
+ extras
1866
+ }) {
1867
+ return /* @__PURE__ */ (0, import_jsx_runtime26.jsxs)(import_jsx_runtime26.Fragment, { children: [
1868
+ toolbar,
1869
+ /* @__PURE__ */ (0, import_jsx_runtime26.jsx)(GenreNodeActionBand, { ...band, genrePlaylist, playback }),
1870
+ /* @__PURE__ */ (0, import_react11.createElement)(
1871
+ GenreDetailExtras,
1872
+ {
1873
+ ...extras,
1874
+ key: genreUuid,
1875
+ family,
1876
+ genreUuid,
1877
+ genrePlaylist,
1878
+ wikidataId,
1879
+ scope,
1880
+ getBackendBaseUrl
1881
+ }
1882
+ )
1883
+ ] });
1884
+ }
1885
+
1473
1886
  // src/genre-tree/PanelEdgeTab.tsx
1474
- var import_lucide_react9 = require("lucide-react");
1887
+ var import_lucide_react12 = require("lucide-react");
1475
1888
  var import_tailwind_merge = require("tailwind-merge");
1476
- var import_jsx_runtime22 = require("react/jsx-runtime");
1889
+ var import_jsx_runtime27 = require("react/jsx-runtime");
1477
1890
  var buttonClassName = "flex h-12 w-full items-center justify-center p-0 text-[var(--color-text-muted)] hover:bg-[var(--color-surface-muted)] hover:text-[var(--color-text)] focus-visible:outline focus-visible:outline-2 focus-visible:-outline-offset-2 focus-visible:outline-[var(--color-primary)]";
1478
- function PanelEdgeTab({ side, edge, center, controls, className }) {
1891
+ function PanelEdgeTab({ side, edge, center, controls, className: className2 }) {
1479
1892
  if (!edge && !center) return null;
1480
1893
  const isLeft = side === "left";
1481
- const EdgeIcon = isLeft ? import_lucide_react9.ChevronLeft : import_lucide_react9.ChevronRight;
1482
- const CenterIcon = isLeft ? import_lucide_react9.ChevronRight : import_lucide_react9.ChevronLeft;
1483
- const renderButton = (action, icon) => /* @__PURE__ */ (0, import_jsx_runtime22.jsx)(
1894
+ const EdgeIcon = isLeft ? import_lucide_react12.ChevronLeft : import_lucide_react12.ChevronRight;
1895
+ const CenterIcon = isLeft ? import_lucide_react12.ChevronRight : import_lucide_react12.ChevronLeft;
1896
+ const renderButton = (action, icon) => /* @__PURE__ */ (0, import_jsx_runtime27.jsx)(
1484
1897
  "button",
1485
1898
  {
1486
1899
  type: "button",
@@ -1493,30 +1906,43 @@ function PanelEdgeTab({ side, edge, center, controls, className }) {
1493
1906
  children: icon
1494
1907
  }
1495
1908
  );
1496
- const edgeButton = edge && renderButton(edge, /* @__PURE__ */ (0, import_jsx_runtime22.jsx)(EdgeIcon, { "aria-hidden": "true", className: "h-5 w-5" }));
1497
- const centerButton = center && renderButton(center, /* @__PURE__ */ (0, import_jsx_runtime22.jsx)(CenterIcon, { "aria-hidden": "true", className: "h-5 w-5" }));
1909
+ const edgeButton = edge && renderButton(edge, /* @__PURE__ */ (0, import_jsx_runtime27.jsx)(EdgeIcon, { "aria-hidden": "true", className: "h-5 w-5" }));
1910
+ const centerButton = center && renderButton(center, /* @__PURE__ */ (0, import_jsx_runtime27.jsx)(CenterIcon, { "aria-hidden": "true", className: "h-5 w-5" }));
1498
1911
  const buttons = isLeft ? [edgeButton, centerButton] : [centerButton, edgeButton];
1499
- return /* @__PURE__ */ (0, import_jsx_runtime22.jsxs)(
1912
+ return /* @__PURE__ */ (0, import_jsx_runtime27.jsxs)(
1500
1913
  "div",
1501
1914
  {
1502
1915
  className: (0, import_tailwind_merge.twMerge)(
1503
1916
  "panel-edge-tab flex w-[var(--layout-edge-tab-w)] flex-col overflow-hidden bg-[var(--color-surface)] shadow-[0_1px_2px_rgba(60,64,67,0.3),0_2px_6px_2px_rgba(60,64,67,0.15)] [clip-path:inset(-0.5rem_-0.5rem_-0.5rem_0)]",
1504
1917
  isLeft ? "rounded-r-lg" : "rounded-l-lg [clip-path:inset(-0.5rem_0_-0.5rem_-0.5rem)]",
1505
- className
1918
+ className2
1506
1919
  ),
1507
1920
  children: [
1508
1921
  buttons[0],
1509
- buttons[0] && buttons[1] && /* @__PURE__ */ (0, import_jsx_runtime22.jsx)("div", { role: "presentation", className: "h-px shrink-0 bg-[var(--color-border)]" }),
1922
+ buttons[0] && buttons[1] && /* @__PURE__ */ (0, import_jsx_runtime27.jsx)("div", { role: "presentation", className: "h-px shrink-0 bg-[var(--color-border)]" }),
1510
1923
  buttons[1]
1511
1924
  ]
1512
1925
  }
1513
1926
  );
1514
1927
  }
1515
1928
 
1929
+ // src/genre-tree/useLeftDetailPanelLayout.ts
1930
+ var import_react12 = require("react");
1931
+ function useLeftDetailPanelLayout(isPanelShown, enabled = true) {
1932
+ (0, import_react12.useEffect)(() => {
1933
+ if (!enabled || isPanelShown) return void 0;
1934
+ const root = document.documentElement;
1935
+ root.style.setProperty("--layout-left-w", "0px");
1936
+ return () => {
1937
+ root.style.removeProperty("--layout-left-w");
1938
+ };
1939
+ }, [enabled, isPanelShown]);
1940
+ }
1941
+
1516
1942
  // src/genre-tree/ChangeParentPopup.tsx
1517
- var import_react8 = require("react");
1518
- var import_genre_tree11 = require("@behindthemusictree/app-kit/genre-tree");
1519
- var import_jsx_runtime23 = require("react/jsx-runtime");
1943
+ var import_react13 = require("react");
1944
+ var import_genre_tree10 = require("@behindthemusictree/app-kit/genre-tree");
1945
+ var import_jsx_runtime28 = require("react/jsx-runtime");
1520
1946
  function ChangeParentPopup({
1521
1947
  genrePlaylist,
1522
1948
  genrePlaylists,
@@ -1525,9 +1951,9 @@ function ChangeParentPopup({
1525
1951
  onClose,
1526
1952
  onParentChanged
1527
1953
  }) {
1528
- const { mutate, isPending, formErrors } = (0, import_genre_tree11.useUpdateGenre)(scope, getBackendBaseUrl);
1529
- const candidates = (0, import_react8.useMemo)(
1530
- () => (0, import_genre_tree11.getReparentCandidates)(genrePlaylists, genrePlaylist.uuid),
1954
+ const { mutate, isPending, formErrors } = (0, import_genre_tree10.useUpdateGenre)(scope, getBackendBaseUrl);
1955
+ const candidates = (0, import_react13.useMemo)(
1956
+ () => (0, import_genre_tree10.getReparentCandidates)(genrePlaylists, genrePlaylist.uuid),
1531
1957
  [genrePlaylists, genrePlaylist.uuid]
1532
1958
  );
1533
1959
  const handleSelect = (newParent) => {
@@ -1544,19 +1970,19 @@ function ChangeParentPopup({
1544
1970
  }
1545
1971
  );
1546
1972
  };
1547
- (0, import_react8.useEffect)(() => {
1973
+ (0, import_react13.useEffect)(() => {
1548
1974
  const handleKeyDown = (event) => {
1549
1975
  if (event.key === "Escape") onClose();
1550
1976
  };
1551
1977
  window.addEventListener("keydown", handleKeyDown);
1552
1978
  return () => window.removeEventListener("keydown", handleKeyDown);
1553
1979
  }, [onClose]);
1554
- return /* @__PURE__ */ (0, import_jsx_runtime23.jsxs)(BasePopup_default, { title: `Change parent of ${genrePlaylist.name}`, onClose, showCancelButton: true, fitContent: true, children: [
1555
- /* @__PURE__ */ (0, import_jsx_runtime23.jsxs)("p", { className: "mb-3 text-sm text-[var(--color-text-muted)]", children: [
1980
+ return /* @__PURE__ */ (0, import_jsx_runtime28.jsxs)(BasePopup_default, { title: `Change parent of ${genrePlaylist.name}`, onClose, showCancelButton: true, fitContent: true, children: [
1981
+ /* @__PURE__ */ (0, import_jsx_runtime28.jsxs)("p", { className: "mb-3 text-sm text-[var(--color-text-muted)]", children: [
1556
1982
  "Current parent: ",
1557
1983
  genrePlaylist.parent?.name ?? "none"
1558
1984
  ] }),
1559
- /* @__PURE__ */ (0, import_jsx_runtime23.jsx)("div", { "aria-busy": isPending, className: isPending ? "pointer-events-none opacity-60" : void 0, children: /* @__PURE__ */ (0, import_jsx_runtime23.jsx)(
1985
+ /* @__PURE__ */ (0, import_jsx_runtime28.jsx)("div", { "aria-busy": isPending, className: isPending ? "pointer-events-none opacity-60" : void 0, children: /* @__PURE__ */ (0, import_jsx_runtime28.jsx)(
1560
1986
  GenreSearch,
1561
1987
  {
1562
1988
  genrePlaylists: candidates,
@@ -1564,32 +1990,32 @@ function ChangeParentPopup({
1564
1990
  placeholder: "Search a new parent\u2026"
1565
1991
  }
1566
1992
  ) }),
1567
- formErrors.length > 0 && /* @__PURE__ */ (0, import_jsx_runtime23.jsx)("p", { role: "alert", className: "mt-3 text-sm text-[var(--color-red-500)]", children: formErrors.map(({ message }) => message).join(" ") })
1993
+ formErrors.length > 0 && /* @__PURE__ */ (0, import_jsx_runtime28.jsx)("p", { role: "alert", className: "mt-3 text-sm text-[var(--color-red-500)]", children: formErrors.map(({ message }) => message).join(" ") })
1568
1994
  ] });
1569
1995
  }
1570
1996
 
1571
1997
  // src/genre-tree/artist/useArtistPanel.tsx
1572
- var import_ui15 = require("@behindthemusictree/ui");
1573
- var import_genre_tree16 = require("@behindthemusictree/app-kit/genre-tree");
1998
+ var import_ui14 = require("@behindthemusictree/ui");
1999
+ var import_genre_tree15 = require("@behindthemusictree/app-kit/genre-tree");
1574
2000
 
1575
2001
  // src/genre-tree/artist/ArtistHeader.tsx
1576
2002
  var import_ui10 = require("@behindthemusictree/ui");
1577
2003
 
1578
2004
  // src/genre-tree/artist/ArtistLinks.tsx
1579
- var import_lucide_react10 = require("lucide-react");
1580
- var import_si = require("react-icons/si");
2005
+ var import_lucide_react13 = require("lucide-react");
2006
+ var import_si3 = require("react-icons/si");
1581
2007
  var import_ui9 = require("@behindthemusictree/ui");
1582
- var import_jsx_runtime24 = require("react/jsx-runtime");
2008
+ var import_jsx_runtime29 = require("react/jsx-runtime");
1583
2009
  var LINK_KINDS = {
1584
- official: { label: "Official website", Icon: import_lucide_react10.Globe },
1585
- wikipedia: { label: "Wikipedia", Icon: import_si.SiWikipedia },
1586
- wikidata: { label: "Wikidata", Icon: import_si.SiWikidata },
1587
- spotify: { label: "Spotify", Icon: import_si.SiSpotify },
1588
- youtube: { label: "YouTube", Icon: import_si.SiYoutube },
1589
- bandcamp: { label: "Bandcamp", Icon: import_si.SiBandcamp },
1590
- soundcloud: { label: "SoundCloud", Icon: import_si.SiSoundcloud },
1591
- discogs: { label: "Discogs", Icon: import_si.SiDiscogs },
1592
- social: { label: "Social", Icon: import_lucide_react10.Share2 }
2010
+ official: { label: "Official website", Icon: import_lucide_react13.Globe },
2011
+ wikipedia: { label: "Wikipedia", Icon: import_si3.SiWikipedia },
2012
+ wikidata: { label: "Wikidata", Icon: import_si3.SiWikidata },
2013
+ spotify: { label: "Spotify", Icon: import_si3.SiSpotify },
2014
+ youtube: { label: "YouTube", Icon: import_si3.SiYoutube },
2015
+ bandcamp: { label: "Bandcamp", Icon: import_si3.SiBandcamp },
2016
+ soundcloud: { label: "SoundCloud", Icon: import_si3.SiSoundcloud },
2017
+ discogs: { label: "Discogs", Icon: import_si3.SiDiscogs },
2018
+ social: { label: "Social", Icon: import_lucide_react13.Share2 }
1593
2019
  };
1594
2020
  function socialLabel(url) {
1595
2021
  try {
@@ -1609,14 +2035,14 @@ function artistLinks(artist) {
1609
2035
  links.push({
1610
2036
  key: "musicbrainz",
1611
2037
  url: `https://musicbrainz.org/artist/${artist.musicbrainzArtistId}`,
1612
- Icon: import_si.SiMusicbrainz,
2038
+ Icon: import_si3.SiMusicbrainz,
1613
2039
  label: "MusicBrainz"
1614
2040
  });
1615
2041
  }
1616
2042
  return links;
1617
2043
  }
1618
2044
  function ArtistLinks({ links }) {
1619
- return /* @__PURE__ */ (0, import_jsx_runtime24.jsx)("ul", { className: "flex flex-wrap gap-1.5", children: links.map(({ key, url, Icon, label }) => /* @__PURE__ */ (0, import_jsx_runtime24.jsx)("li", { children: /* @__PURE__ */ (0, import_jsx_runtime24.jsx)(import_ui9.Chip, { href: url, icon: /* @__PURE__ */ (0, import_jsx_runtime24.jsx)(Icon, { className: "h-3.5 w-3.5 shrink-0", "aria-hidden": "true" }), children: label }) }, key)) });
2045
+ return /* @__PURE__ */ (0, import_jsx_runtime29.jsx)("ul", { className: "flex flex-wrap gap-1.5", children: links.map(({ key, url, Icon, label }) => /* @__PURE__ */ (0, import_jsx_runtime29.jsx)("li", { children: /* @__PURE__ */ (0, import_jsx_runtime29.jsx)(import_ui9.Chip, { href: url, icon: /* @__PURE__ */ (0, import_jsx_runtime29.jsx)(Icon, { className: "h-3.5 w-3.5 shrink-0", "aria-hidden": "true" }), children: label }) }, key)) });
1620
2046
  }
1621
2047
 
1622
2048
  // src/genre-tree/artist/formatYears.ts
@@ -1627,7 +2053,7 @@ function formatYears(beginYear, endYear, ended) {
1627
2053
  }
1628
2054
 
1629
2055
  // src/genre-tree/artist/ArtistHeader.tsx
1630
- var import_jsx_runtime25 = require("react/jsx-runtime");
2056
+ var import_jsx_runtime30 = require("react/jsx-runtime");
1631
2057
  var countryNames = new Intl.DisplayNames(["en"], { type: "region" });
1632
2058
  function ArtistHeader({ artist }) {
1633
2059
  const fields = [];
@@ -1637,35 +2063,35 @@ function ArtistHeader({ artist }) {
1637
2063
  const years = formatYears(artist.beginYear, artist.endYear, artist.ended);
1638
2064
  if (years) fields.push({ label: "Active", value: years });
1639
2065
  const links = artistLinks(artist);
1640
- if (links.length > 0) fields.push({ label: "Links", value: /* @__PURE__ */ (0, import_jsx_runtime25.jsx)(ArtistLinks, { links }) });
2066
+ if (links.length > 0) fields.push({ label: "Links", value: /* @__PURE__ */ (0, import_jsx_runtime30.jsx)(ArtistLinks, { links }) });
1641
2067
  if (fields.length === 0) return null;
1642
- return /* @__PURE__ */ (0, import_jsx_runtime25.jsx)(import_ui10.FieldList, { items: fields });
2068
+ return /* @__PURE__ */ (0, import_jsx_runtime30.jsx)(import_ui10.FieldList, { items: fields });
1643
2069
  }
1644
2070
 
1645
2071
  // src/genre-tree/artist/ArtistPhoto.tsx
1646
- var import_jsx_runtime26 = require("react/jsx-runtime");
2072
+ var import_jsx_runtime31 = require("react/jsx-runtime");
1647
2073
  function ExternalLink({ href, children }) {
1648
- return /* @__PURE__ */ (0, import_jsx_runtime26.jsx)("a", { href, target: "_blank", rel: "noopener noreferrer", className: "underline-offset-2 hover:underline", children });
2074
+ return /* @__PURE__ */ (0, import_jsx_runtime31.jsx)("a", { href, target: "_blank", rel: "noopener noreferrer", className: "underline-offset-2 hover:underline", children });
1649
2075
  }
1650
2076
  function ArtistPhoto({ image, name }) {
1651
- return /* @__PURE__ */ (0, import_jsx_runtime26.jsxs)("figure", { className: "flex flex-col gap-1.5 px-6 py-2.5", children: [
1652
- /* @__PURE__ */ (0, import_jsx_runtime26.jsx)("img", { src: image.url, alt: name, className: "h-64 w-full object-contain bg-[var(--color-surface-muted)] rounded" }),
1653
- /* @__PURE__ */ (0, import_jsx_runtime26.jsxs)("figcaption", { className: "text-xs text-[var(--color-text-muted)]", children: [
2077
+ return /* @__PURE__ */ (0, import_jsx_runtime31.jsxs)("figure", { className: "flex flex-col gap-1.5 px-6 py-2.5", children: [
2078
+ /* @__PURE__ */ (0, import_jsx_runtime31.jsx)("img", { src: image.url, alt: name, className: "h-64 w-full object-contain bg-[var(--color-surface-muted)] rounded" }),
2079
+ /* @__PURE__ */ (0, import_jsx_runtime31.jsxs)("figcaption", { className: "text-xs text-[var(--color-text-muted)]", children: [
1654
2080
  "Photo: ",
1655
- /* @__PURE__ */ (0, import_jsx_runtime26.jsx)(ExternalLink, { href: image.pageUrl, children: image.author ?? "Wikimedia Commons" }),
2081
+ /* @__PURE__ */ (0, import_jsx_runtime31.jsx)(ExternalLink, { href: image.pageUrl, children: image.author ?? "Wikimedia Commons" }),
1656
2082
  ",",
1657
2083
  " ",
1658
- image.licenseUrl ? /* @__PURE__ */ (0, import_jsx_runtime26.jsx)(ExternalLink, { href: image.licenseUrl, children: image.license }) : image.license
2084
+ image.licenseUrl ? /* @__PURE__ */ (0, import_jsx_runtime31.jsx)(ExternalLink, { href: image.licenseUrl, children: image.license }) : image.license
1659
2085
  ] })
1660
2086
  ] });
1661
2087
  }
1662
2088
 
1663
2089
  // src/genre-tree/artist/ArtistMembers.tsx
1664
2090
  var import_ui11 = require("@behindthemusictree/ui");
1665
- var import_jsx_runtime27 = require("react/jsx-runtime");
2091
+ var import_jsx_runtime32 = require("react/jsx-runtime");
1666
2092
  function MembershipList({ title, memberships, renderArtistLink }) {
1667
2093
  if (memberships.length === 0) return null;
1668
- return /* @__PURE__ */ (0, import_jsx_runtime27.jsx)(import_ui11.PanelGroup, { title, children: /* @__PURE__ */ (0, import_jsx_runtime27.jsx)("ul", { children: memberships.map((membership, index) => {
2094
+ return /* @__PURE__ */ (0, import_jsx_runtime32.jsx)(import_ui11.PanelGroup, { title, children: /* @__PURE__ */ (0, import_jsx_runtime32.jsx)("ul", { children: memberships.map((membership, index) => {
1669
2095
  const details = [
1670
2096
  formatYears(membership.beginYear, membership.endYear, membership.ended),
1671
2097
  ...membership.roles
@@ -1673,9 +2099,9 @@ function MembershipList({ title, memberships, renderArtistLink }) {
1673
2099
  const detailsText = details.filter(Boolean).join(" \xB7 ");
1674
2100
  return (
1675
2101
  // A member can have several periods in the same group.
1676
- /* @__PURE__ */ (0, import_jsx_runtime27.jsxs)("li", { className: "truncate py-0.5", title: detailsText || void 0, children: [
1677
- /* @__PURE__ */ (0, import_jsx_runtime27.jsx)("span", { className: "font-medium text-[var(--color-text)]", children: renderArtistLink(membership.artist) }),
1678
- detailsText && /* @__PURE__ */ (0, import_jsx_runtime27.jsxs)("span", { className: "text-[var(--color-text-muted)]", children: [
2102
+ /* @__PURE__ */ (0, import_jsx_runtime32.jsxs)("li", { className: "truncate py-0.5", title: detailsText || void 0, children: [
2103
+ /* @__PURE__ */ (0, import_jsx_runtime32.jsx)("span", { className: "font-medium text-[var(--color-text)]", children: renderArtistLink(membership.artist) }),
2104
+ detailsText && /* @__PURE__ */ (0, import_jsx_runtime32.jsxs)("span", { className: "text-[var(--color-text-muted)]", children: [
1679
2105
  " \u2014 ",
1680
2106
  detailsText
1681
2107
  ] })
@@ -1686,9 +2112,9 @@ function MembershipList({ title, memberships, renderArtistLink }) {
1686
2112
  function ArtistMembers({ title, memberships, renderArtistLink }) {
1687
2113
  if (memberships.length === 0) return null;
1688
2114
  const isCurrent = (membership) => !membership.ended && membership.endYear === null;
1689
- return /* @__PURE__ */ (0, import_jsx_runtime27.jsxs)(import_ui11.PanelSection, { title, children: [
1690
- /* @__PURE__ */ (0, import_jsx_runtime27.jsx)(MembershipList, { title: "Current", memberships: memberships.filter(isCurrent), renderArtistLink }),
1691
- /* @__PURE__ */ (0, import_jsx_runtime27.jsx)(
2115
+ return /* @__PURE__ */ (0, import_jsx_runtime32.jsxs)(import_ui11.PanelSection, { title, children: [
2116
+ /* @__PURE__ */ (0, import_jsx_runtime32.jsx)(MembershipList, { title: "Current", memberships: memberships.filter(isCurrent), renderArtistLink }),
2117
+ /* @__PURE__ */ (0, import_jsx_runtime32.jsx)(
1692
2118
  MembershipList,
1693
2119
  {
1694
2120
  title: "Past",
@@ -1701,7 +2127,7 @@ function ArtistMembers({ title, memberships, renderArtistLink }) {
1701
2127
 
1702
2128
  // src/genre-tree/artist/ArtistDiscography.tsx
1703
2129
  var import_ui12 = require("@behindthemusictree/ui");
1704
- var import_jsx_runtime28 = require("react/jsx-runtime");
2130
+ var import_jsx_runtime33 = require("react/jsx-runtime");
1705
2131
  var GROUPS = ["Albums", "EPs", "Singles", "Live", "Compilations", "Other"];
1706
2132
  var PRIMARY_GROUPS = { album: "Albums", ep: "EPs", single: "Singles" };
1707
2133
  function discographyGroup({ primaryType, secondaryTypes }) {
@@ -1721,77 +2147,85 @@ function byYear(a, b) {
1721
2147
  function ArtistDiscography({ albums }) {
1722
2148
  if (albums.length === 0) return null;
1723
2149
  const sorted = [...albums].sort(byYear);
1724
- return /* @__PURE__ */ (0, import_jsx_runtime28.jsx)(import_ui12.PanelSection, { title: "Discography", children: GROUPS.map((group) => {
2150
+ return /* @__PURE__ */ (0, import_jsx_runtime33.jsx)(import_ui12.PanelSection, { title: "Discography", children: GROUPS.map((group) => {
1725
2151
  const groupAlbums = sorted.filter((album) => discographyGroup(album) === group);
1726
2152
  if (groupAlbums.length === 0) return null;
1727
- return /* @__PURE__ */ (0, import_jsx_runtime28.jsx)(import_ui12.PanelGroup, { title: group, children: /* @__PURE__ */ (0, import_jsx_runtime28.jsx)("ul", { children: groupAlbums.map((album) => /* @__PURE__ */ (0, import_jsx_runtime28.jsxs)("li", { className: "grid grid-cols-[2.5rem_minmax(0,1fr)] gap-2 py-0.5", children: [
1728
- /* @__PURE__ */ (0, import_jsx_runtime28.jsx)("span", { className: "tabular-nums text-[var(--color-text-muted)]", children: album.year ?? "\u2014" }),
1729
- /* @__PURE__ */ (0, import_jsx_runtime28.jsx)("span", { className: "truncate text-[var(--color-text)]", title: album.name, children: album.name })
2153
+ return /* @__PURE__ */ (0, import_jsx_runtime33.jsx)(import_ui12.PanelGroup, { title: group, children: /* @__PURE__ */ (0, import_jsx_runtime33.jsx)("ul", { children: groupAlbums.map((album) => /* @__PURE__ */ (0, import_jsx_runtime33.jsxs)("li", { className: "grid grid-cols-[2.5rem_minmax(0,1fr)] gap-2 py-0.5", children: [
2154
+ /* @__PURE__ */ (0, import_jsx_runtime33.jsx)("span", { className: "tabular-nums text-[var(--color-text-muted)]", children: album.year ?? "\u2014" }),
2155
+ /* @__PURE__ */ (0, import_jsx_runtime33.jsx)("span", { className: "truncate text-[var(--color-text)]", title: album.name, children: album.name })
1730
2156
  ] }, album.uuid)) }) }, group);
1731
2157
  }) });
1732
2158
  }
1733
2159
 
1734
2160
  // src/genre-tree/artist/ArtistSongs.tsx
1735
- var import_react9 = require("react");
2161
+ var import_react14 = require("react");
1736
2162
  var import_react_query2 = require("@tanstack/react-query");
1737
- var import_lucide_react11 = require("lucide-react");
1738
- var import_ui14 = require("@behindthemusictree/ui");
1739
- var import_player9 = require("@behindthemusictree/app-kit/player");
1740
- var import_genre_tree15 = require("@behindthemusictree/app-kit/genre-tree");
2163
+ var import_lucide_react14 = require("lucide-react");
2164
+ var import_ui13 = require("@behindthemusictree/ui");
2165
+ var import_player10 = require("@behindthemusictree/app-kit/player");
2166
+ var import_genre_tree14 = require("@behindthemusictree/app-kit/genre-tree");
1741
2167
  var import_transport8 = require("@behindthemusictree/app-kit/transport");
1742
2168
 
1743
2169
  // src/genre-tree/track-list-sidebar/TrackItem.tsx
1744
- var import_player8 = require("@behindthemusictree/app-kit/player");
1745
- var import_genre_tree12 = require("@behindthemusictree/app-kit/genre-tree");
2170
+ var import_player9 = require("@behindthemusictree/app-kit/player");
2171
+ var import_genre_tree11 = require("@behindthemusictree/app-kit/genre-tree");
1746
2172
 
1747
2173
  // src/genre-tree/TrackPositionPlayPause.tsx
1748
- var import_fa6 = require("react-icons/fa");
1749
- var import_player6 = require("@behindthemusictree/app-kit/player");
2174
+ var import_fa5 = require("react-icons/fa");
1750
2175
  var import_player7 = require("@behindthemusictree/app-kit/player");
1751
- var import_ui13 = require("@behindthemusictree/ui");
1752
- var import_jsx_runtime29 = require("react/jsx-runtime");
1753
- function TrackPositionPlayPause({ position, uuid, handlePlayPauseClick }) {
1754
- const { playerTrackObject, playState } = (0, import_player6.usePlayer)();
2176
+ var import_player8 = require("@behindthemusictree/app-kit/player");
2177
+ var import_jsx_runtime34 = require("react/jsx-runtime");
2178
+ function TrackPositionPlayPause({
2179
+ position,
2180
+ uuid,
2181
+ handlePlayPauseClick,
2182
+ isSelected = false
2183
+ }) {
2184
+ const { playerTrackObject, playbackConfirmedTrackId, playState, isLoading: isPlayerLoading } = (0, import_player7.usePlayer)();
1755
2185
  const isThisTrack = playerTrackObject?.track.id === uuid;
1756
- return /* @__PURE__ */ (0, import_jsx_runtime29.jsxs)(
2186
+ const isConfirmedPlayable = playbackConfirmedTrackId === uuid;
2187
+ const isAwaitingPlayback = (isSelected || isThisTrack) && !isConfirmedPlayable && !(isThisTrack && playerTrackObject?.loadError) && (!isThisTrack || isPlayerLoading || playState === import_player8.PlayStates.LOADING || playState === import_player8.PlayStates.PLAYING);
2188
+ const isPlaying = isThisTrack && isConfirmedPlayable && playState === import_player8.PlayStates.PLAYING;
2189
+ const isPaused = isThisTrack && isConfirmedPlayable && playState === import_player8.PlayStates.PAUSED;
2190
+ const isCurrentTrackActive = isPlaying || isPaused;
2191
+ return /* @__PURE__ */ (0, import_jsx_runtime34.jsxs)(
1757
2192
  "div",
1758
2193
  {
1759
- className: "track-position-play-pause flex items-center justify-center text-lg w-16 cursor-pointer",
2194
+ className: "track-position-play-pause relative flex items-center justify-center gap-2 text-lg w-16 cursor-pointer",
1760
2195
  style: { minWidth: "4rem", maxWidth: "4rem" },
1761
2196
  onClick: handlePlayPauseClick,
1762
2197
  children: [
1763
- /* @__PURE__ */ (0, import_jsx_runtime29.jsx)("div", { className: "group-hover:hidden", children: /* @__PURE__ */ (0, import_jsx_runtime29.jsx)("div", { children: isThisTrack && playState !== import_player7.PlayStates.STOPPED ? /* @__PURE__ */ (0, import_jsx_runtime29.jsxs)("div", { className: "flex space-x-1 items-end", children: [
1764
- /* @__PURE__ */ (0, import_jsx_runtime29.jsx)(
1765
- "div",
1766
- {
1767
- className: `w-playingbar bg-[var(--color-green-500)] h-3 origin-bottom ${playState === import_player7.PlayStates.PLAYING ? "animate-scale-pulse" : ""}`
1768
- }
1769
- ),
1770
- /* @__PURE__ */ (0, import_jsx_runtime29.jsx)(
1771
- "div",
1772
- {
1773
- className: `w-playingbar bg-[var(--color-green-500)] h-4 origin-bottom ${playState === import_player7.PlayStates.PLAYING ? "animate-scale-pulse delay-200" : ""}`,
1774
- style: { animationDelay: "0.2s" }
1775
- }
1776
- ),
1777
- /* @__PURE__ */ (0, import_jsx_runtime29.jsx)(
1778
- "div",
1779
- {
1780
- className: `w-playingbar bg-[var(--color-green-500)] h-3 origin-bottom ${playState === import_player7.PlayStates.PLAYING ? "animate-scale-pulse delay-400" : ""}`,
1781
- style: { animationDelay: "0.3s" }
1782
- }
1783
- )
1784
- ] }) : position }) }),
1785
- /* @__PURE__ */ (0, import_jsx_runtime29.jsx)("div", { className: "hidden group-hover:flex items-center justify-center", children: isThisTrack && playState === import_player7.PlayStates.LOADING ? /* @__PURE__ */ (0, import_jsx_runtime29.jsx)(import_ui13.RingLoader, { size: 16 }) : isThisTrack && playState === import_player7.PlayStates.PLAYING ? /* @__PURE__ */ (0, import_jsx_runtime29.jsx)(import_fa6.FaPause, {}) : /* @__PURE__ */ (0, import_jsx_runtime29.jsx)(import_fa6.FaPlay, {}) })
2198
+ /* @__PURE__ */ (0, import_jsx_runtime34.jsx)("div", { className: "track-position-play-pause__status group-hover:hidden flex items-center justify-center", children: isAwaitingPlayback ? /* @__PURE__ */ (0, import_jsx_runtime34.jsx)("span", { className: "track-position-play-pause__spinner", role: "status", "aria-label": "Loading track" }) : isCurrentTrackActive ? /* @__PURE__ */ (0, import_jsx_runtime34.jsxs)(
2199
+ "span",
2200
+ {
2201
+ className: "track-position-play-pause__bars",
2202
+ "data-playing": isPlaying,
2203
+ role: "img",
2204
+ "aria-label": isPlaying ? "Playing" : "Paused",
2205
+ children: [
2206
+ /* @__PURE__ */ (0, import_jsx_runtime34.jsx)("span", { className: "track-position-play-pause__bar" }),
2207
+ /* @__PURE__ */ (0, import_jsx_runtime34.jsx)("span", { className: "track-position-play-pause__bar" }),
2208
+ /* @__PURE__ */ (0, import_jsx_runtime34.jsx)("span", { className: "track-position-play-pause__bar" })
2209
+ ]
2210
+ }
2211
+ ) : position }),
2212
+ /* @__PURE__ */ (0, import_jsx_runtime34.jsx)(
2213
+ "div",
2214
+ {
2215
+ className: `hidden group-hover:flex items-center justify-center ${isCurrentTrackActive ? "" : "absolute inset-0"}`,
2216
+ "aria-hidden": "true",
2217
+ children: isAwaitingPlayback ? /* @__PURE__ */ (0, import_jsx_runtime34.jsx)("span", { className: "track-position-play-pause__spinner" }) : isPlaying ? /* @__PURE__ */ (0, import_jsx_runtime34.jsx)(import_fa5.FaPause, {}) : /* @__PURE__ */ (0, import_jsx_runtime34.jsx)(import_fa5.FaPlay, {})
2218
+ }
2219
+ )
1786
2220
  ]
1787
2221
  }
1788
2222
  );
1789
2223
  }
1790
2224
 
1791
2225
  // src/genre-tree/track-list-sidebar/TrackItem.tsx
2226
+ var import_genre_tree12 = require("@behindthemusictree/app-kit/genre-tree");
1792
2227
  var import_genre_tree13 = require("@behindthemusictree/app-kit/genre-tree");
1793
- var import_genre_tree14 = require("@behindthemusictree/app-kit/genre-tree");
1794
- var import_jsx_runtime30 = require("react/jsx-runtime");
2228
+ var import_jsx_runtime35 = require("react/jsx-runtime");
1795
2229
  function TrackItem({
1796
2230
  track,
1797
2231
  position,
@@ -1799,26 +2233,27 @@ function TrackItem({
1799
2233
  renderActions,
1800
2234
  renderArtistLink
1801
2235
  }) {
1802
- const { handlePlayPauseAction, playerTrackObject, loadTrackForPlayer } = (0, import_player8.usePlayer)();
1803
- const { trackList, toTrackAtPosition } = (0, import_genre_tree12.useTrackList)();
2236
+ const { handlePlayPauseAction, playerTrackObject, loadTrackForPlayer } = (0, import_player9.usePlayer)();
2237
+ const { trackList, selectedTrack, toTrackAtPosition, runtimeUnplayableTrackIds } = (0, import_genre_tree11.useTrackList)();
1804
2238
  const scope = trackList?.origin?.scope ?? null;
1805
- const unplayableReason = (0, import_genre_tree13.isSong)(track) ? (0, import_genre_tree13.songUnplayableReason)(track) : null;
2239
+ const isCurrentTrack = playerTrackObject?.track.id === track.uuid;
2240
+ const hasPlaybackError = isCurrentTrack && !!playerTrackObject?.loadError;
2241
+ const isUnplayable = hasPlaybackError || runtimeUnplayableTrackIds?.has(track.uuid) === true || (0, import_genre_tree12.isSong)(track) && (0, import_genre_tree12.songUnplayableReason)(track) !== null;
1806
2242
  const handlePlayPauseClick = (event) => {
1807
2243
  event.stopPropagation();
1808
- if (playerTrackObject && playerTrackObject.track.id === track.uuid) {
2244
+ if (isCurrentTrack) {
1809
2245
  handlePlayPauseAction();
1810
2246
  } else if (scope != null) {
1811
2247
  toTrackAtPosition(position);
1812
2248
  loadTrackForPlayer(track.uuid);
1813
2249
  }
1814
2250
  };
1815
- return /* @__PURE__ */ (0, import_jsx_runtime30.jsxs)(
2251
+ return /* @__PURE__ */ (0, import_jsx_runtime35.jsxs)(
1816
2252
  "div",
1817
2253
  {
1818
- className: `track-item flex h-14 text-[var(--color-text)] ${unplayableReason ? "opacity-50" : "hover:bg-[var(--color-surface-muted)] group"}`,
1819
- title: unplayableReason ?? void 0,
2254
+ className: `track-item flex h-14 text-[var(--color-text)] ${isUnplayable ? "opacity-50" : "hover:bg-[var(--color-surface-muted)] group"}`,
1820
2255
  children: [
1821
- unplayableReason ? /* @__PURE__ */ (0, import_jsx_runtime30.jsx)(
2256
+ isUnplayable ? /* @__PURE__ */ (0, import_jsx_runtime35.jsx)(
1822
2257
  "div",
1823
2258
  {
1824
2259
  className: "flex items-center justify-center text-lg w-16",
@@ -1826,18 +2261,21 @@ function TrackItem({
1826
2261
  "aria-disabled": "true",
1827
2262
  children: position
1828
2263
  }
1829
- ) : /* @__PURE__ */ (0, import_jsx_runtime30.jsx)(TrackPositionPlayPause, { position, uuid: track.uuid, handlePlayPauseClick }),
1830
- /* @__PURE__ */ (0, import_jsx_runtime30.jsxs)("div", { className: "title-artist-container flex flex-col items-start justify-center min-w-0 flex-1", children: [
1831
- /* @__PURE__ */ (0, import_jsx_runtime30.jsxs)("div", { className: "flex max-w-full items-center gap-2", children: [
1832
- /* @__PURE__ */ (0, import_jsx_runtime30.jsx)("div", { className: "title text-lg font-bold text-[var(--color-text)] max-w-full truncate", children: track.title }),
1833
- (0, import_genre_tree13.isSong)(track) && /* @__PURE__ */ (0, import_jsx_runtime30.jsx)(MusicBrainzRecordingLink, { song: track })
1834
- ] }),
1835
- track.artists && track.artists.length > 0 ? /* @__PURE__ */ (0, import_jsx_runtime30.jsx)("div", { className: "artist text-base max-w-full truncate text-[var(--color-text-muted)]", children: /* @__PURE__ */ (0, import_jsx_runtime30.jsx)(import_genre_tree14.ArtistNames, { artists: track.artists, renderArtistLink }) }) : "",
1836
- unplayableReason && /* @__PURE__ */ (0, import_jsx_runtime30.jsx)("div", { className: "text-xs", children: /* @__PURE__ */ (0, import_jsx_runtime30.jsx)(UnplayableReason, { reason: unplayableReason }) })
2264
+ ) : /* @__PURE__ */ (0, import_jsx_runtime35.jsx)(
2265
+ TrackPositionPlayPause,
2266
+ {
2267
+ position,
2268
+ uuid: track.uuid,
2269
+ handlePlayPauseClick,
2270
+ isSelected: selectedTrack?.uuid === track.uuid
2271
+ }
2272
+ ),
2273
+ /* @__PURE__ */ (0, import_jsx_runtime35.jsxs)("div", { className: "title-artist-container flex flex-col items-start justify-center min-w-0 flex-1", children: [
2274
+ /* @__PURE__ */ (0, import_jsx_runtime35.jsx)("div", { className: "flex max-w-full items-center gap-2", children: /* @__PURE__ */ (0, import_jsx_runtime35.jsx)("div", { className: "title text-lg font-bold text-[var(--color-text)] max-w-full truncate", children: (0, import_genre_tree12.isSong)(track) ? /* @__PURE__ */ (0, import_jsx_runtime35.jsx)(MusicBrainzRecordingLink, { song: track, className: "hover:underline focus-visible:underline", children: track.title }) : track.title }) }),
2275
+ track.artists && track.artists.length > 0 ? /* @__PURE__ */ (0, import_jsx_runtime35.jsx)("div", { className: "artist text-base max-w-full truncate text-[var(--color-text-muted)]", children: /* @__PURE__ */ (0, import_jsx_runtime35.jsx)(import_genre_tree13.ArtistNames, { artists: track.artists, renderArtistLink }) }) : ""
1837
2276
  ] }),
1838
- /* @__PURE__ */ (0, import_jsx_runtime30.jsx)("div", { className: "album-name items-start justify-center w-1/3 min-w-0 ml-2 max-w-full truncate text-[var(--color-text-muted)]", children: track.album ? track.album.name : "" }),
1839
- /* @__PURE__ */ (0, import_jsx_runtime30.jsx)("div", { className: "genre-name-container flex items-center justify-end w-1/6 min-w-0 shrink-0", children: track.genre ? /* @__PURE__ */ (0, import_jsx_runtime30.jsx)("div", { className: "genre-name font-bold p-1 border border-[var(--color-text-muted)] text-xs max-w-full truncate", children: track.genre.name }) : "" }),
1840
- renderDuration && /* @__PURE__ */ (0, import_jsx_runtime30.jsx)(
2277
+ /* @__PURE__ */ (0, import_jsx_runtime35.jsx)("div", { className: "genre-name-container flex items-center justify-start w-[21.6%] min-w-0 shrink-0", children: track.genre ? /* @__PURE__ */ (0, import_jsx_runtime35.jsx)("div", { className: "genre-name max-w-full whitespace-normal break-words p-1 text-xs font-bold", children: track.genre.name }) : "" }),
2278
+ renderDuration && /* @__PURE__ */ (0, import_jsx_runtime35.jsx)(
1841
2279
  "div",
1842
2280
  {
1843
2281
  className: "duration flex text-base w-16 items-center justify-center",
@@ -1845,28 +2283,28 @@ function TrackItem({
1845
2283
  children: renderDuration(track)
1846
2284
  }
1847
2285
  ),
1848
- renderActions && /* @__PURE__ */ (0, import_jsx_runtime30.jsx)("div", { className: "edit flex text-base w-6 items-center justify-center mr-2 cursor-pointer", children: renderActions(track) })
2286
+ renderActions && /* @__PURE__ */ (0, import_jsx_runtime35.jsx)("div", { className: "edit flex text-base w-6 items-center justify-center mr-2 cursor-pointer", children: renderActions(track) })
1849
2287
  ]
1850
2288
  }
1851
2289
  );
1852
2290
  }
1853
2291
 
1854
2292
  // src/genre-tree/artist/ArtistSongs.tsx
1855
- var import_jsx_runtime31 = require("react/jsx-runtime");
1856
- var SongPageSchema2 = (0, import_transport8.PaginatedResponseSchema)(import_genre_tree15.SongDetailedSchema);
2293
+ var import_jsx_runtime36 = require("react/jsx-runtime");
2294
+ var SongPageSchema2 = (0, import_transport8.PaginatedResponseSchema)(import_genre_tree14.SongDetailedSchema);
1857
2295
  var SORTS = [
1858
2296
  { ordering: "title", label: "Title" },
1859
2297
  { ordering: "-albumYear", label: "Year" }
1860
2298
  ];
1861
2299
  function ArtistSongs({ artist, getBackendBaseUrl, renderArtistLink }) {
1862
- const [page, setPage] = (0, import_react9.useState)(1);
1863
- const [ordering, setOrdering] = (0, import_react9.useState)("title");
2300
+ const [page, setPage] = (0, import_react14.useState)(1);
2301
+ const [ordering, setOrdering] = (0, import_react14.useState)("title");
1864
2302
  const { fetch } = (0, import_transport8.useFetchWrapper)(getBackendBaseUrl);
1865
- const { playNewTrackListFromArtist } = (0, import_genre_tree15.useTrackList)();
1866
- const { playerTrackObject } = (0, import_player9.usePlayer)();
2303
+ const { playNewTrackListFromArtist } = (0, import_genre_tree14.useTrackList)();
2304
+ const { playerTrackObject } = (0, import_player10.usePlayer)();
1867
2305
  const { data, isPending, isError } = (0, import_transport8.useQueryWithParse)({
1868
2306
  queryKey: ["referenceSongs", "artist", artist.uuid, ordering, page],
1869
- queryFn: () => fetch(import_genre_tree15.libraryEndpoints.reference.songs.list(), true, false, void 0, {
2307
+ queryFn: () => fetch(import_genre_tree14.libraryEndpoints.reference.songs.list(), true, false, void 0, {
1870
2308
  artist: artist.uuid,
1871
2309
  ordering,
1872
2310
  page
@@ -1881,25 +2319,25 @@ function ArtistSongs({ artist, getBackendBaseUrl, renderArtistLink }) {
1881
2319
  });
1882
2320
  const handleRowClickCapture = (event, song) => {
1883
2321
  if (!(event.target instanceof Element) || !event.target.closest(".track-position-play-pause")) return;
1884
- if (playerTrackObject?.track.id === song.uuid || (0, import_genre_tree15.songUnplayableReason)(song) !== null) return;
2322
+ if (playerTrackObject?.track.id === song.uuid || (0, import_genre_tree14.songUnplayableReason)(song) !== null) return;
1885
2323
  event.stopPropagation();
1886
2324
  void play(song.uuid);
1887
2325
  };
1888
- return /* @__PURE__ */ (0, import_jsx_runtime31.jsx)(import_ui14.PanelSection, { title: "Songs", children: isPending ? /* @__PURE__ */ (0, import_jsx_runtime31.jsx)(import_ui14.Skeleton, { className: "h-14", "aria-busy": "true" }) : isError ? /* @__PURE__ */ (0, import_jsx_runtime31.jsx)("p", { className: "text-red-600", children: "Could not load songs." }) : data.results.length === 0 ? /* @__PURE__ */ (0, import_jsx_runtime31.jsx)("p", { className: "text-[var(--color-text-muted)]", children: "No songs yet." }) : /* @__PURE__ */ (0, import_jsx_runtime31.jsxs)(import_jsx_runtime31.Fragment, { children: [
1889
- /* @__PURE__ */ (0, import_jsx_runtime31.jsxs)("div", { className: "flex flex-wrap items-center justify-between gap-2 mb-2", children: [
1890
- /* @__PURE__ */ (0, import_jsx_runtime31.jsxs)(
2326
+ return /* @__PURE__ */ (0, import_jsx_runtime36.jsx)(import_ui13.PanelSection, { title: "Songs", children: isPending ? /* @__PURE__ */ (0, import_jsx_runtime36.jsx)(import_ui13.Skeleton, { className: "h-14", "aria-busy": "true" }) : isError ? /* @__PURE__ */ (0, import_jsx_runtime36.jsx)("p", { className: "text-red-600", children: "Could not load songs." }) : data.results.length === 0 ? /* @__PURE__ */ (0, import_jsx_runtime36.jsx)("p", { className: "text-[var(--color-text-muted)]", children: "No songs yet." }) : /* @__PURE__ */ (0, import_jsx_runtime36.jsxs)(import_jsx_runtime36.Fragment, { children: [
2327
+ /* @__PURE__ */ (0, import_jsx_runtime36.jsxs)("div", { className: "flex flex-wrap items-center justify-between gap-2 mb-2", children: [
2328
+ /* @__PURE__ */ (0, import_jsx_runtime36.jsxs)(
1891
2329
  "button",
1892
2330
  {
1893
2331
  type: "button",
1894
2332
  onClick: () => void play(),
1895
2333
  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",
1896
2334
  children: [
1897
- /* @__PURE__ */ (0, import_jsx_runtime31.jsx)(import_lucide_react11.Play, { className: "h-3.5 w-3.5", "aria-hidden": "true" }),
2335
+ /* @__PURE__ */ (0, import_jsx_runtime36.jsx)(import_lucide_react14.Play, { className: "h-3.5 w-3.5", "aria-hidden": "true" }),
1898
2336
  "Play all"
1899
2337
  ]
1900
2338
  }
1901
2339
  ),
1902
- /* @__PURE__ */ (0, import_jsx_runtime31.jsx)("div", { role: "group", "aria-label": "Sort songs by", className: "flex gap-1 text-xs", children: SORTS.map((sort) => /* @__PURE__ */ (0, import_jsx_runtime31.jsx)(
2340
+ /* @__PURE__ */ (0, import_jsx_runtime36.jsx)("div", { role: "group", "aria-label": "Sort songs by", className: "flex gap-1 text-xs", children: SORTS.map((sort) => /* @__PURE__ */ (0, import_jsx_runtime36.jsx)(
1903
2341
  "button",
1904
2342
  {
1905
2343
  type: "button",
@@ -1914,7 +2352,7 @@ function ArtistSongs({ artist, getBackendBaseUrl, renderArtistLink }) {
1914
2352
  sort.ordering
1915
2353
  )) })
1916
2354
  ] }),
1917
- /* @__PURE__ */ (0, import_jsx_runtime31.jsx)("ul", { className: "overflow-hidden mb-2 bg-zinc-950 rounded-lg", children: data.results.map((song, index) => /* @__PURE__ */ (0, import_jsx_runtime31.jsx)("li", { onClickCapture: (event) => handleRowClickCapture(event, song), children: /* @__PURE__ */ (0, import_jsx_runtime31.jsx)(
2355
+ /* @__PURE__ */ (0, import_jsx_runtime36.jsx)("ul", { className: "overflow-hidden mb-2 bg-zinc-950 rounded-lg", children: data.results.map((song, index) => /* @__PURE__ */ (0, import_jsx_runtime36.jsx)("li", { onClickCapture: (event) => handleRowClickCapture(event, song), children: /* @__PURE__ */ (0, import_jsx_runtime36.jsx)(
1918
2356
  TrackItem,
1919
2357
  {
1920
2358
  track: song,
@@ -1922,21 +2360,23 @@ function ArtistSongs({ artist, getBackendBaseUrl, renderArtistLink }) {
1922
2360
  renderArtistLink
1923
2361
  }
1924
2362
  ) }, song.uuid)) }),
1925
- /* @__PURE__ */ (0, import_jsx_runtime31.jsx)(import_ui14.Pagination, { currentPage: page, totalPages: data.totalPages, onPageChange: setPage })
2363
+ /* @__PURE__ */ (0, import_jsx_runtime36.jsx)(import_ui13.Pagination, { currentPage: page, totalPages: data.totalPages, onPageChange: setPage })
1926
2364
  ] }) });
1927
2365
  }
1928
2366
 
1929
2367
  // src/genre-tree/artist/useArtistPanel.tsx
1930
- var import_jsx_runtime32 = require("react/jsx-runtime");
2368
+ var import_jsx_runtime37 = require("react/jsx-runtime");
1931
2369
  function useArtistPanel({
1932
2370
  uuid,
2371
+ scope,
1933
2372
  getBackendBaseUrl,
2373
+ getShareUrl,
1934
2374
  renderArtistLink,
1935
2375
  expanded,
1936
2376
  onExpandedChange,
1937
2377
  onClose
1938
2378
  }) {
1939
- const result = (0, import_genre_tree16.useArtist)(uuid, getBackendBaseUrl);
2379
+ const result = (0, import_genre_tree15.useArtist)(uuid, getBackendBaseUrl);
1940
2380
  const panel = { expanded, onExpandedChange, onClose };
1941
2381
  switch (result.status) {
1942
2382
  case "idle":
@@ -1945,38 +2385,41 @@ function useArtistPanel({
1945
2385
  return {
1946
2386
  ...panel,
1947
2387
  title: "Artist",
1948
- content: /* @__PURE__ */ (0, import_jsx_runtime32.jsxs)("div", { "aria-busy": "true", className: "flex flex-col gap-2.5 px-6 py-2.5", children: [
1949
- /* @__PURE__ */ (0, import_jsx_runtime32.jsx)(import_ui15.Skeleton, { className: "h-3 w-16" }),
1950
- /* @__PURE__ */ (0, import_jsx_runtime32.jsx)(import_ui15.Skeleton, { className: "h-4 w-32" }),
1951
- /* @__PURE__ */ (0, import_jsx_runtime32.jsx)(import_ui15.Skeleton, { className: "h-3 w-16" }),
1952
- /* @__PURE__ */ (0, import_jsx_runtime32.jsx)(import_ui15.Skeleton, { className: "h-4 w-24" }),
1953
- /* @__PURE__ */ (0, import_jsx_runtime32.jsx)(import_ui15.Skeleton, { className: "h-16 mt-2.5" }),
1954
- /* @__PURE__ */ (0, import_jsx_runtime32.jsx)(import_ui15.Skeleton, { className: "h-16" })
2388
+ content: /* @__PURE__ */ (0, import_jsx_runtime37.jsxs)("div", { "aria-busy": "true", className: "flex flex-col gap-2.5 px-6 py-2.5", children: [
2389
+ /* @__PURE__ */ (0, import_jsx_runtime37.jsx)(import_ui14.Skeleton, { className: "h-3 w-16" }),
2390
+ /* @__PURE__ */ (0, import_jsx_runtime37.jsx)(import_ui14.Skeleton, { className: "h-4 w-32" }),
2391
+ /* @__PURE__ */ (0, import_jsx_runtime37.jsx)(import_ui14.Skeleton, { className: "h-3 w-16" }),
2392
+ /* @__PURE__ */ (0, import_jsx_runtime37.jsx)(import_ui14.Skeleton, { className: "h-4 w-24" }),
2393
+ /* @__PURE__ */ (0, import_jsx_runtime37.jsx)(import_ui14.Skeleton, { className: "h-16 mt-2.5" }),
2394
+ /* @__PURE__ */ (0, import_jsx_runtime37.jsx)(import_ui14.Skeleton, { className: "h-16" })
1955
2395
  ] })
1956
2396
  };
1957
2397
  case "not-found":
1958
2398
  return {
1959
2399
  ...panel,
1960
2400
  title: "Artist not found",
1961
- content: /* @__PURE__ */ (0, import_jsx_runtime32.jsx)("p", { className: "px-6 py-2.5 text-[var(--color-text-muted)]", children: "This artist does not exist." })
2401
+ content: /* @__PURE__ */ (0, import_jsx_runtime37.jsx)("p", { className: "px-6 py-2.5 text-[var(--color-text-muted)]", children: "This artist does not exist." })
1962
2402
  };
1963
2403
  case "error":
1964
2404
  return {
1965
2405
  ...panel,
1966
2406
  title: "Artist",
1967
- content: /* @__PURE__ */ (0, import_jsx_runtime32.jsx)("p", { className: "px-6 py-2.5 text-red-600", children: "Could not load this artist." })
2407
+ content: /* @__PURE__ */ (0, import_jsx_runtime37.jsx)("p", { className: "px-6 py-2.5 text-red-600", children: "Could not load this artist." })
1968
2408
  };
1969
2409
  case "success": {
1970
2410
  const { artist } = result;
2411
+ const shareTarget = scope ? { kind: "artist", uuid: artist.uuid, name: artist.name, scope } : void 0;
2412
+ const shareUrl = shareTarget ? getShareUrl?.(shareTarget) : void 0;
1971
2413
  return {
1972
2414
  ...panel,
1973
2415
  title: artist.name,
1974
- content: /* @__PURE__ */ (0, import_jsx_runtime32.jsxs)(import_jsx_runtime32.Fragment, { children: [
1975
- artist.image && /* @__PURE__ */ (0, import_jsx_runtime32.jsx)(ArtistPhoto, { image: artist.image, name: artist.name }),
1976
- /* @__PURE__ */ (0, import_jsx_runtime32.jsx)(ArtistHeader, { artist }),
1977
- /* @__PURE__ */ (0, import_jsx_runtime32.jsx)(ArtistMembers, { title: "Members", memberships: artist.members, renderArtistLink }),
1978
- /* @__PURE__ */ (0, import_jsx_runtime32.jsx)(ArtistMembers, { title: "Member of", memberships: artist.memberOf, renderArtistLink }),
1979
- artist.tracksCount > 0 && /* @__PURE__ */ (0, import_jsx_runtime32.jsx)(
2416
+ content: /* @__PURE__ */ (0, import_jsx_runtime37.jsxs)(import_jsx_runtime37.Fragment, { children: [
2417
+ shareUrl && /* @__PURE__ */ (0, import_jsx_runtime37.jsx)("div", { className: "genre-action-band border-b border-[var(--color-border)]", children: /* @__PURE__ */ (0, import_jsx_runtime37.jsx)("div", { className: "flex flex-wrap gap-1 px-3.5 py-3", children: shareTarget && /* @__PURE__ */ (0, import_jsx_runtime37.jsx)(ShareActionButton, { target: shareTarget, url: shareUrl }) }) }),
2418
+ artist.image && /* @__PURE__ */ (0, import_jsx_runtime37.jsx)(ArtistPhoto, { image: artist.image, name: artist.name }),
2419
+ /* @__PURE__ */ (0, import_jsx_runtime37.jsx)(ArtistHeader, { artist }),
2420
+ /* @__PURE__ */ (0, import_jsx_runtime37.jsx)(ArtistMembers, { title: "Members", memberships: artist.members, renderArtistLink }),
2421
+ /* @__PURE__ */ (0, import_jsx_runtime37.jsx)(ArtistMembers, { title: "Member of", memberships: artist.memberOf, renderArtistLink }),
2422
+ artist.tracksCount > 0 && /* @__PURE__ */ (0, import_jsx_runtime37.jsx)(
1980
2423
  ArtistSongs,
1981
2424
  {
1982
2425
  artist: { uuid: artist.uuid, name: artist.name },
@@ -1985,7 +2428,7 @@ function useArtistPanel({
1985
2428
  }
1986
2429
  )
1987
2430
  ] }),
1988
- extraContent: artist.albums.length > 0 ? /* @__PURE__ */ (0, import_jsx_runtime32.jsx)(ArtistDiscography, { albums: artist.albums }) : void 0
2431
+ extraContent: artist.albums.length > 0 ? /* @__PURE__ */ (0, import_jsx_runtime37.jsx)(ArtistDiscography, { albums: artist.albums }) : void 0
1989
2432
  };
1990
2433
  }
1991
2434
  }
@@ -1993,28 +2436,28 @@ function useArtistPanel({
1993
2436
 
1994
2437
  // src/genre-tree/SizeMetricToggle.tsx
1995
2438
  var import_md2 = require("react-icons/md");
1996
- var import_ui16 = require("@behindthemusictree/ui");
1997
- var import_jsx_runtime33 = require("react/jsx-runtime");
2439
+ var import_ui15 = require("@behindthemusictree/ui");
2440
+ var import_jsx_runtime38 = require("react/jsx-runtime");
1998
2441
  function SizeMetricToggle({
1999
2442
  value,
2000
2443
  onChange
2001
2444
  }) {
2002
- return /* @__PURE__ */ (0, import_jsx_runtime33.jsx)(
2003
- import_ui16.SegmentedControl,
2445
+ return /* @__PURE__ */ (0, import_jsx_runtime38.jsx)(
2446
+ import_ui15.SegmentedControl,
2004
2447
  {
2005
2448
  "aria-label": "Size nodes by",
2006
2449
  value,
2007
2450
  onChange,
2008
2451
  options: [
2009
- { value: "songs", label: /* @__PURE__ */ (0, import_jsx_runtime33.jsx)(import_md2.MdMusicNote, { size: 18, "aria-hidden": true }), "aria-label": "Size by songs", title: "Node size by song count" },
2010
- { value: "views", label: /* @__PURE__ */ (0, import_jsx_runtime33.jsx)(import_md2.MdVisibility, { size: 18, "aria-hidden": true }), "aria-label": "Size by views", title: "Node size by song views" }
2452
+ { value: "songs", label: /* @__PURE__ */ (0, import_jsx_runtime38.jsx)(import_md2.MdMusicNote, { size: 18, "aria-hidden": true }), "aria-label": "Size by songs", title: "Node size by song count" },
2453
+ { value: "views", label: /* @__PURE__ */ (0, import_jsx_runtime38.jsx)(import_md2.MdVisibility, { size: 18, "aria-hidden": true }), "aria-label": "Size by views", title: "Node size by song views" }
2011
2454
  ]
2012
2455
  }
2013
2456
  );
2014
2457
  }
2015
2458
 
2016
2459
  // src/genre-tree/GenreTreeView.tsx
2017
- var import_jsx_runtime34 = require("react/jsx-runtime");
2460
+ var import_jsx_runtime39 = require("react/jsx-runtime");
2018
2461
  var PREVIOUS_BUTTON_CSS = `
2019
2462
  .genre-tree-view .search-overlay .gtv-info-panel-previous { display: none; }
2020
2463
  .genre-tree-view:has(.search-overlay .gtv-info-panel-previous) .genre-search input { padding-left: 2.75rem; }
@@ -2024,27 +2467,6 @@ var PREVIOUS_BUTTON_CSS = `
2024
2467
  .genre-search-previous { animation: genre-tree-view-previous-in 200ms ease-out; }
2025
2468
  }
2026
2469
  `;
2027
- function GenreNodeActionBand({ genrePlaylist, scope, playback, ...bandProps }) {
2028
- const { isPlaying, setIsPlaying } = (0, import_player10.usePlayer)();
2029
- const isCurrentTrackList = playback.playingNodeId === genrePlaylist.uuid;
2030
- const handlePlayPause = () => {
2031
- if (isCurrentTrackList) {
2032
- setIsPlaying(!isPlaying);
2033
- return;
2034
- }
2035
- playback.play(genrePlaylist).catch((error) => {
2036
- console.error("Failed to load genre playlist tracks:", error);
2037
- });
2038
- };
2039
- return /* @__PURE__ */ (0, import_jsx_runtime34.jsx)(
2040
- GenreActionBand,
2041
- {
2042
- ...bandProps,
2043
- onPlayPause: genrePlaylist.tracksCount > 0 ? handlePlayPause : void 0,
2044
- isPlaying: isCurrentTrackList && isPlaying
2045
- }
2046
- );
2047
- }
2048
2470
  var NO_POP_ROOT_TITLE = "This genre tree has no 'Mainstream Pop' root yet";
2049
2471
  var REGIONS_CLASS = "[--gtv-center-left:var(--layout-center-left)] [--gtv-center-right:var(--layout-center-right)] [--gtv-center-top:var(--layout-band-top)] [&_.gtv-info-panel--overlay]:[--gtv-info-panel-header-inset:0px]";
2050
2472
  function GenreTreeView({
@@ -2058,15 +2480,16 @@ function GenreTreeView({
2058
2480
  selectedNodeId: controlledSelectedNodeId,
2059
2481
  onSelectedNodeIdChange,
2060
2482
  defaultViewport,
2483
+ viewportRestore,
2061
2484
  onViewportChange,
2062
2485
  readOnly = false,
2063
- className,
2486
+ className: className2,
2064
2487
  showDisplayOptions = true,
2065
2488
  showAddRoot = true,
2066
2489
  allowWheelRotation: controlledAllowWheelRotation,
2067
2490
  criteriaOverviewSchema,
2068
2491
  renderGenreDetailExtras,
2069
- controlsOrientation = "horizontal",
2492
+ floatingControlsLeading,
2070
2493
  sizeMetric: controlledSizeMetric,
2071
2494
  onSizeMetricChange,
2072
2495
  renderArtistLink,
@@ -2074,6 +2497,7 @@ function GenreTreeView({
2074
2497
  onArtistUuidChange,
2075
2498
  artistExpanded: controlledArtistExpanded,
2076
2499
  onArtistExpandedChange,
2500
+ getShareUrl,
2077
2501
  artistSearch = true,
2078
2502
  onArtistSelect,
2079
2503
  artistSearchBaseUrl,
@@ -2091,12 +2515,14 @@ function GenreTreeView({
2091
2515
  placement = "default",
2092
2516
  ...focus
2093
2517
  }) {
2094
- const [internalArtistUuid, setInternalArtistUuid] = (0, import_react10.useState)(null);
2518
+ const getShareUrlRef = (0, import_react15.useRef)(getShareUrl);
2519
+ getShareUrlRef.current = getShareUrl;
2520
+ const [internalArtistUuid, setInternalArtistUuid] = (0, import_react15.useState)(null);
2095
2521
  const isArtistControlled = controlledArtistUuid !== void 0;
2096
2522
  const artistUuid = isArtistControlled ? controlledArtistUuid : internalArtistUuid;
2097
- const [internalSizeMetric, setInternalSizeMetric] = (0, import_react10.useState)("songs");
2523
+ const [internalSizeMetric, setInternalSizeMetric] = (0, import_react15.useState)("songs");
2098
2524
  const sizeMetric = controlledSizeMetric ?? internalSizeMetric;
2099
- const deferredSizeMetric = (0, import_react10.useDeferredValue)(sizeMetric);
2525
+ const deferredSizeMetric = (0, import_react15.useDeferredValue)(sizeMetric);
2100
2526
  const changeSizeMetric = (metric) => {
2101
2527
  if (controlledSizeMetric === void 0) setInternalSizeMetric(metric);
2102
2528
  onSizeMetricChange?.(metric);
@@ -2123,7 +2549,7 @@ function GenreTreeView({
2123
2549
  allowWheelRotation: internalAllowWheelRotation,
2124
2550
  toggleWheelRotation,
2125
2551
  onNodeHover: handleNodeHover
2126
- } = (0, import_genre_tree18.useGenreTreeViewState)({
2552
+ } = (0, import_genre_tree17.useGenreTreeViewState)({
2127
2553
  scope,
2128
2554
  getBackendBaseUrl,
2129
2555
  viewMode: controlledViewMode,
@@ -2132,25 +2558,41 @@ function GenreTreeView({
2132
2558
  criteriaOverviewSchema,
2133
2559
  artistUuid
2134
2560
  });
2135
- const isGenreUpdating = (0, import_react_query3.useIsMutating)({ mutationKey: import_genre_tree18.genreUpdateMutationKey }) > 0;
2561
+ const restoreRevision = viewportRestore?.revision;
2562
+ const [stackedRestoreOwner, setStackedRestoreOwner] = (0, import_react15.useState)({ revision: restoreRevision, rootUuid: selectedGenrePlaylist?.root.uuid });
2563
+ let restoreRootUuid = stackedRestoreOwner.rootUuid;
2564
+ if (stackedRestoreOwner.revision !== restoreRevision) {
2565
+ restoreRootUuid = selectedGenrePlaylist?.root.uuid;
2566
+ setStackedRestoreOwner({ revision: restoreRevision, rootUuid: restoreRootUuid });
2567
+ } else if (restoreRootUuid === void 0 && selectedGenrePlaylist) {
2568
+ restoreRootUuid = selectedGenrePlaylist.root.uuid;
2569
+ setStackedRestoreOwner({ revision: restoreRevision, rootUuid: restoreRootUuid });
2570
+ }
2571
+ const isGenreUpdating = (0, import_react_query3.useIsMutating)({ mutationKey: import_genre_tree17.genreUpdateMutationKey }) > 0;
2572
+ const { mutate: updateGenre } = (0, import_genre_tree17.useUpdateGenre)(scope, getBackendBaseUrl);
2136
2573
  const busy = deferredSizeMetric !== sizeMetric || isTreeFetching || isGenreUpdating;
2137
- const artistResult = (0, import_genre_tree18.useArtist)(artistUuid, artistSearchBaseUrl ?? getBackendBaseUrl);
2574
+ const isArtistFiltered = artistUuid !== null;
2575
+ const canSizeByViews = !isArtistFiltered && hasViewsCount(visibleGenrePlaylists);
2576
+ const artistResult = (0, import_genre_tree17.useArtist)(artistUuid, artistSearchBaseUrl ?? getBackendBaseUrl);
2138
2577
  const filterArtistName = artistResult.status === "success" ? artistResult.artist.name : null;
2139
- const filterArtist = (0, import_react10.useMemo)(
2578
+ const filterArtist = (0, import_react15.useMemo)(
2140
2579
  () => artistUuid !== null && filterArtistName !== null ? { uuid: artistUuid, name: filterArtistName } : null,
2141
2580
  [artistUuid, filterArtistName]
2142
2581
  );
2582
+ const hasSelectedGenreNode = selectedGenrePlaylist !== void 0 && visibleGenrePlaylists.some(({ uuid }) => uuid === selectedGenrePlaylist.uuid);
2143
2583
  const playback = useGenrePlaylistPlayback({ scope, genrePlaylists: visibleGenrePlaylists, artist: filterArtist });
2144
2584
  const openArtist = (uuid) => {
2145
2585
  if (selectedNodeId !== null) clearSelection();
2146
2586
  changeArtist(uuid);
2147
2587
  };
2148
- const [expandedArtistUuid, setExpandedArtistUuid] = (0, import_react10.useState)(null);
2588
+ const [expandedArtistUuid, setExpandedArtistUuid] = (0, import_react15.useState)(null);
2149
2589
  const artistBaseUrl = artistSearchBaseUrl ?? getBackendBaseUrl;
2150
2590
  const artistPanel = useArtistPanel({
2151
2591
  uuid: artistUuid,
2592
+ scope,
2152
2593
  getBackendBaseUrl: artistBaseUrl,
2153
- renderArtistLink: renderArtistLink ?? ((artist) => artist.isSpecialPurpose ? artist.name : /* @__PURE__ */ (0, import_jsx_runtime34.jsx)("button", { type: "button", onClick: () => openArtist(artist.uuid), className: "hover:underline", children: artist.name })),
2594
+ getShareUrl: getShareUrlRef.current,
2595
+ renderArtistLink: renderArtistLink ?? ((artist) => artist.isSpecialPurpose ? artist.name : /* @__PURE__ */ (0, import_jsx_runtime39.jsx)("button", { type: "button", onClick: () => openArtist(artist.uuid), className: "hover:underline", children: artist.name })),
2154
2596
  expanded: controlledArtistExpanded ?? (artistUuid !== null && expandedArtistUuid === artistUuid),
2155
2597
  onExpandedChange: (on) => {
2156
2598
  if (controlledArtistExpanded === void 0) setExpandedArtistUuid(on ? artistUuid : null);
@@ -2158,10 +2600,10 @@ function GenreTreeView({
2158
2600
  },
2159
2601
  onClose: () => changeArtist(null)
2160
2602
  });
2161
- const builtInArtistPanel = overlayPanel || selectedNodeId !== null ? void 0 : artistPanel;
2603
+ const builtInArtistPanel = overlayPanel || hasSelectedGenreNode ? void 0 : artistPanel;
2162
2604
  const activeOverlayPanel = overlayPanel ?? (builtInArtistPanel && { ...builtInArtistPanel, hideTitle: true, hideClose: true });
2163
- const [uncontrolledCollapsed, setUncontrolledCollapsed] = (0, import_react10.useState)(false);
2164
- const [uncontrolledExpanded, setUncontrolledExpanded] = (0, import_react10.useState)(false);
2605
+ const [uncontrolledCollapsed, setUncontrolledCollapsed] = (0, import_react15.useState)(false);
2606
+ const [uncontrolledExpanded, setUncontrolledExpanded] = (0, import_react15.useState)(false);
2165
2607
  const isCollapsed = collapsed ?? uncontrolledCollapsed;
2166
2608
  const isExpanded = expanded ?? uncontrolledExpanded;
2167
2609
  const changeCollapsed = (next) => {
@@ -2172,18 +2614,11 @@ function GenreTreeView({
2172
2614
  setUncontrolledExpanded(next);
2173
2615
  onExpandedChange?.(next);
2174
2616
  };
2175
- (0, import_react10.useEffect)(() => {
2617
+ (0, import_react15.useEffect)(() => {
2176
2618
  setUncontrolledCollapsed(false);
2177
2619
  }, [selectedNodeId, artistUuid]);
2178
- const isDetailHidden = isCollapsed || !selectedNodeId && !activeOverlayPanel;
2179
- (0, import_react10.useEffect)(() => {
2180
- if (placement !== "regions" || !isDetailHidden) return void 0;
2181
- const root = document.documentElement;
2182
- root.style.setProperty("--layout-left-w", "0px");
2183
- return () => {
2184
- root.style.removeProperty("--layout-left-w");
2185
- };
2186
- }, [placement, isDetailHidden]);
2620
+ const isDetailHidden = isCollapsed || !hasSelectedGenreNode && !activeOverlayPanel;
2621
+ useLeftDetailPanelLayout(!isDetailHidden, placement === "regions");
2187
2622
  const detailOverlay = placement === "regions" && activeOverlayPanel?.onExpandedChange ? activeOverlayPanel : void 0;
2188
2623
  const detailExpanded = detailOverlay ? !!detailOverlay.expanded : isExpanded;
2189
2624
  const onDetailExpandedChange = detailOverlay ? detailOverlay.onExpandedChange : changeExpanded;
@@ -2195,29 +2630,38 @@ function GenreTreeView({
2195
2630
  hideExpandButton: placement === "regions" ? true : void 0,
2196
2631
  overlayPanel: detailOverlay ? { ...detailOverlay, collapsed: isCollapsed, hideExpandButton: true } : activeOverlayPanel
2197
2632
  };
2198
- const [isWheelRevealed, setIsWheelRevealed] = (0, import_react10.useState)(false);
2633
+ const [isWheelRevealed, setIsWheelRevealed] = (0, import_react15.useState)(false);
2199
2634
  const usesHandoff = viewMode === "wheel" || viewMode === "pop-core";
2200
2635
  const isTreeReady = !isLoading && (!usesHandoff || isWheelRevealed);
2201
- const rootRef = (0, import_react10.useRef)(null);
2636
+ const rootRef = (0, import_react15.useRef)(null);
2202
2637
  const handlePrevious = () => {
2203
2638
  rootRef.current?.querySelector(".search-overlay .gtv-info-panel-previous")?.click();
2204
2639
  };
2205
- const { showPopup, hidePopup } = (0, import_popup5.usePopup)();
2206
- const [searchText, setSearchText] = (0, import_react10.useState)("");
2207
- const artistSearchResult = (0, import_genre_tree17.useArtistSearch)(artistBaseUrl, artistSearch ? searchText : "");
2640
+ (0, import_react15.useEffect)(() => {
2641
+ if (!isTreeReady || viewMode !== "pop-core" || !hasSelectedGenreNode || !selectedGenrePlaylist) return;
2642
+ const root = rootRef.current;
2643
+ const centerChip = root?.querySelector(".gtv-wheel-center-node .gtv-wheel-chip");
2644
+ if (!centerChip || centerChip.getAttribute("aria-label") !== selectedGenrePlaylist.name) return;
2645
+ const panelTitle = root?.querySelector(".gtv-info-panel-title");
2646
+ if (panelTitle?.textContent?.trim().startsWith(selectedGenrePlaylist.name)) return;
2647
+ centerChip.click();
2648
+ }, [hasSelectedGenreNode, isTreeReady, selectedGenrePlaylist, viewMode]);
2649
+ const { showPopup, hidePopup } = (0, import_popup6.usePopup)();
2650
+ const [searchText, setSearchText] = (0, import_react15.useState)("");
2651
+ const artistSearchResult = (0, import_genre_tree16.useArtistSearch)(artistBaseUrl, artistSearch ? searchText : "");
2208
2652
  const artistExtraResults = artistSearch ? {
2209
2653
  label: "Artists",
2210
2654
  items: artistSearchResult.artists.map((artist) => ({
2211
2655
  key: artist.uuid,
2212
- content: /* @__PURE__ */ (0, import_jsx_runtime34.jsxs)(import_jsx_runtime34.Fragment, { children: [
2213
- /* @__PURE__ */ (0, import_jsx_runtime34.jsxs)("span", { className: "block truncate font-medium", children: [
2656
+ content: /* @__PURE__ */ (0, import_jsx_runtime39.jsxs)(import_jsx_runtime39.Fragment, { children: [
2657
+ /* @__PURE__ */ (0, import_jsx_runtime39.jsxs)("span", { className: "block truncate font-medium", children: [
2214
2658
  artist.name,
2215
- artist.disambiguation && /* @__PURE__ */ (0, import_jsx_runtime34.jsxs)("span", { className: "font-normal text-[var(--color-neutral-500)]", children: [
2659
+ artist.disambiguation && /* @__PURE__ */ (0, import_jsx_runtime39.jsxs)("span", { className: "font-normal text-[var(--color-neutral-500)]", children: [
2216
2660
  " \xB7 ",
2217
2661
  artist.disambiguation
2218
2662
  ] })
2219
2663
  ] }),
2220
- artist.matchedAlias && /* @__PURE__ */ (0, import_jsx_runtime34.jsxs)("span", { className: "block truncate text-xs text-[var(--color-neutral-500)]", children: [
2664
+ artist.matchedAlias && /* @__PURE__ */ (0, import_jsx_runtime39.jsxs)("span", { className: "block truncate text-xs text-[var(--color-neutral-500)]", children: [
2221
2665
  "also known as ",
2222
2666
  artist.matchedAlias
2223
2667
  ] })
@@ -2243,31 +2687,52 @@ function GenreTreeView({
2243
2687
  const handleGenreSearchZoomIn = () => {
2244
2688
  if (selectedUuid) focus.onFocusGenreChange?.(selectedUuid, "zoom-in");
2245
2689
  };
2246
- const zoomInRef = (0, import_react10.useRef)(void 0);
2690
+ const zoomInRef = (0, import_react15.useRef)(void 0);
2247
2691
  zoomInRef.current = canZoomIn ? handleGenreSearchZoomIn : void 0;
2248
- const renderInfoPanelBody = (0, import_react10.useCallback)(
2692
+ const renderInfoPanelBody = (0, import_react15.useCallback)(
2249
2693
  (node, { toolbar, ancestors, family }) => {
2250
2694
  const genrePlaylist = genrePlaylistByNodeId.get(node.id);
2251
2695
  const criteria = genrePlaylist?.criteria;
2252
- if (!genrePlaylist || !criteria) return { primary: /* @__PURE__ */ (0, import_jsx_runtime34.jsxs)(import_jsx_runtime34.Fragment, { children: [
2696
+ if (!genrePlaylist || !criteria) return { primary: /* @__PURE__ */ (0, import_jsx_runtime39.jsxs)(import_jsx_runtime39.Fragment, { children: [
2253
2697
  toolbar,
2254
2698
  family
2255
2699
  ] }), aside: ancestors };
2256
2700
  const genreUuid = criteria.uuid;
2701
+ const parentPlaylist = visibleGenrePlaylists.find(({ uuid }) => uuid === genrePlaylist.parent?.uuid);
2702
+ const canToggleSide = !readOnly && parentPlaylist !== void 0 && parentPlaylist.parent === null;
2703
+ const side = criteria.side ?? "core";
2704
+ const shareTarget = {
2705
+ kind: "genre",
2706
+ genrePlaylistUuid: genrePlaylist.uuid,
2707
+ name: criteria.name,
2708
+ scope,
2709
+ artistUuid
2710
+ };
2711
+ const shareUrl = getShareUrlRef.current?.(shareTarget) ?? void 0;
2712
+ const popSibling = canToggleSide && side === "core" ? visibleGenrePlaylists.find(
2713
+ (sibling) => sibling.parent?.uuid === parentPlaylist.uuid && sibling.uuid !== genrePlaylist.uuid && sibling.criteria?.side === "pop"
2714
+ ) : void 0;
2257
2715
  return {
2258
- primary: /* @__PURE__ */ (0, import_jsx_runtime34.jsxs)(import_jsx_runtime34.Fragment, { children: [
2259
- toolbar,
2260
- /* @__PURE__ */ (0, import_jsx_runtime34.jsx)(
2261
- GenreNodeActionBand,
2262
- {
2716
+ primary: /* @__PURE__ */ (0, import_jsx_runtime39.jsx)(
2717
+ GenreInfoPanelBody,
2718
+ {
2719
+ toolbar,
2720
+ family,
2721
+ genreUuid,
2722
+ genrePlaylist,
2723
+ wikidataId: criteria.wikidataId,
2724
+ scope,
2725
+ getBackendBaseUrl,
2726
+ playback,
2727
+ band: {
2263
2728
  node,
2264
- genrePlaylist,
2265
- scope,
2729
+ shareTarget: shareUrl ? shareTarget : void 0,
2730
+ shareUrl,
2266
2731
  onZoomIn: zoomInRef.current,
2267
2732
  onAddChild: readOnly ? void 0 : () => handleGenreCreationAction(criteria),
2268
2733
  onRename: readOnly ? void 0 : () => handleGenreRenameAction(criteria),
2269
2734
  onChangeParent: readOnly ? void 0 : () => showPopup(
2270
- /* @__PURE__ */ (0, import_jsx_runtime34.jsx)(
2735
+ /* @__PURE__ */ (0, import_jsx_runtime39.jsx)(
2271
2736
  ChangeParentPopup,
2272
2737
  {
2273
2738
  genrePlaylist: { ...genrePlaylist, criteria },
@@ -2279,18 +2744,12 @@ function GenreTreeView({
2279
2744
  }
2280
2745
  )
2281
2746
  ),
2282
- additionalActions,
2283
- playback
2284
- }
2285
- ),
2286
- /* @__PURE__ */ (0, import_jsx_runtime34.jsx)(
2287
- GenreDetailExtras,
2288
- {
2289
- family,
2290
- genreUuid,
2291
- genrePlaylist,
2292
- scope,
2293
- getBackendBaseUrl,
2747
+ side,
2748
+ onToggleSide: canToggleSide ? () => updateGenre({ uuid: genreUuid, data: { side: side === "pop" ? "core" : "pop" } }) : void 0,
2749
+ toggleSideDisabledReason: popSibling ? `${popSibling.name} is already the pop side of ${parentPlaylist?.name}` : void 0,
2750
+ additionalActions
2751
+ },
2752
+ extras: {
2294
2753
  criteriaOverviewSchema,
2295
2754
  renderGenreDetailExtras,
2296
2755
  renderArtistLink,
@@ -2300,10 +2759,9 @@ function GenreTreeView({
2300
2759
  renderSongLeading,
2301
2760
  renderSongActions,
2302
2761
  renderTracksToolbar
2303
- },
2304
- genreUuid
2305
- )
2306
- ] }),
2762
+ }
2763
+ }
2764
+ ),
2307
2765
  aside: ancestors
2308
2766
  };
2309
2767
  },
@@ -2317,6 +2775,7 @@ function GenreTreeView({
2317
2775
  hidePopup,
2318
2776
  onParentChanged,
2319
2777
  visibleGenrePlaylists,
2778
+ updateGenre,
2320
2779
  playback,
2321
2780
  filterArtist,
2322
2781
  scope,
@@ -2333,7 +2792,7 @@ function GenreTreeView({
2333
2792
  );
2334
2793
  const isRotationControlled = controlledAllowWheelRotation !== void 0;
2335
2794
  const allowWheelRotation = controlledAllowWheelRotation ?? internalAllowWheelRotation;
2336
- const detailTab = placement === "regions" && (selectedNodeId || detailOverlay) ? /* @__PURE__ */ (0, import_jsx_runtime34.jsx)(
2795
+ const detailTab = placement === "regions" && (hasSelectedGenreNode || detailOverlay) ? /* @__PURE__ */ (0, import_jsx_runtime39.jsx)(
2337
2796
  PanelEdgeTab,
2338
2797
  {
2339
2798
  side: "left",
@@ -2341,25 +2800,14 @@ function GenreTreeView({
2341
2800
  center: detailExpanded ? null : isCollapsed ? { label: "Show panel", onClick: () => changeCollapsed(false) } : onDetailExpandedChange ? { label: "Extend panel", onClick: () => onDetailExpandedChange(true) } : null
2342
2801
  }
2343
2802
  ) : null;
2344
- const actions = /* @__PURE__ */ (0, import_jsx_runtime34.jsxs)(import_jsx_runtime34.Fragment, { children: [
2345
- !isLoading && /* @__PURE__ */ (0, import_jsx_runtime34.jsx)(
2346
- GenreSearch,
2347
- {
2348
- genrePlaylists: visibleGenrePlaylists,
2349
- onSelect: handleGenreSearchSelect,
2350
- selectedName: selectedGenrePlaylist?.name ?? builtInArtistPanel?.title ?? null,
2351
- onClear: builtInArtistPanel ? () => changeArtist(null) : handleGenreSearchClear,
2352
- onPrevious: handlePrevious,
2353
- onQueryChange: artistSearch ? setSearchText : void 0,
2354
- extraResults: artistExtraResults
2355
- }
2356
- ),
2357
- !isLoading && filterArtist && /* @__PURE__ */ (0, import_jsx_runtime34.jsxs)("span", { className: "inline-flex items-center overflow-hidden rounded-full bg-[var(--color-neutral-100)] text-sm", children: [
2358
- /* @__PURE__ */ (0, import_jsx_runtime34.jsx)(
2803
+ const renderArtistFilter = () => {
2804
+ if (isLoading || !filterArtist) return null;
2805
+ return /* @__PURE__ */ (0, import_jsx_runtime39.jsxs)("span", { className: "inline-flex items-center overflow-hidden rounded-full bg-[var(--color-neutral-100)] text-sm", children: [
2806
+ /* @__PURE__ */ (0, import_jsx_runtime39.jsx)(
2359
2807
  "button",
2360
2808
  {
2361
2809
  type: "button",
2362
- className: "py-1 pl-3 pr-2 hover:underline",
2810
+ className: "py-1 pl-3 pr-2 [@media(pointer:coarse)]:min-h-[var(--layout-touch-target)] hover:underline",
2363
2811
  "aria-label": `Show artist ${filterArtist.name}`,
2364
2812
  onClick: () => {
2365
2813
  if (selectedNodeId !== null) clearSelection();
@@ -2367,86 +2815,174 @@ function GenreTreeView({
2367
2815
  children: filterArtist.name
2368
2816
  }
2369
2817
  ),
2370
- /* @__PURE__ */ (0, import_jsx_runtime34.jsx)(
2818
+ /* @__PURE__ */ (0, import_jsx_runtime39.jsx)(
2371
2819
  "button",
2372
2820
  {
2373
2821
  type: "button",
2374
- className: "py-1 pl-1 pr-3 hover:text-[var(--color-text)]",
2822
+ className: "py-1 pl-1 pr-3 [@media(pointer:coarse)]:min-h-[var(--layout-touch-target)] hover:text-[var(--color-text)]",
2375
2823
  "aria-label": `Clear artist filter ${filterArtist.name}`,
2376
2824
  onClick: () => changeArtist(null),
2377
2825
  children: "\xD7"
2378
2826
  }
2379
2827
  )
2380
- ] }),
2381
- !isLoading && !isControlled && /* @__PURE__ */ (0, import_jsx_runtime34.jsx)(
2382
- import_ui17.SegmentedControl,
2383
- {
2384
- "aria-label": "Tree view mode",
2385
- value: viewMode,
2386
- onChange: setViewMode,
2387
- options: [
2388
- { value: "pop-core", label: "Pop/Core", disabled: !canShowPopCore, title: canShowPopCore ? void 0 : NO_POP_ROOT_TITLE },
2389
- { value: "wheel", label: "Wheel" },
2390
- { value: "stacked", label: "Stacked" },
2391
- { value: "outline", label: "Outline", disabled: !canShowPopCore, title: canShowPopCore ? void 0 : NO_POP_ROOT_TITLE }
2392
- ]
2393
- }
2394
- ),
2395
- !isLoading && showDisplayOptions && /* @__PURE__ */ (0, import_jsx_runtime34.jsx)(
2396
- "div",
2828
+ ] });
2829
+ };
2830
+ const actions = /* @__PURE__ */ (0, import_jsx_runtime39.jsxs)(import_jsx_runtime39.Fragment, { children: [
2831
+ !isLoading && /* @__PURE__ */ (0, import_jsx_runtime39.jsx)(
2832
+ GenreSearch,
2397
2833
  {
2398
- className: "flex items-center gap-1",
2399
- role: "group",
2400
- "aria-label": "Tree display options",
2401
- children: !isRotationControlled && viewMode !== "stacked" && viewMode !== "outline" && /* @__PURE__ */ (0, import_jsx_runtime34.jsx)(
2402
- import_ui17.Button,
2403
- {
2404
- variant: allowWheelRotation ? "default" : "outline",
2405
- size: "sm",
2406
- onClick: toggleWheelRotation,
2407
- children: "Rotation"
2408
- }
2409
- )
2834
+ genrePlaylists: visibleGenrePlaylists,
2835
+ onSelect: handleGenreSearchSelect,
2836
+ selectedName: selectedGenrePlaylist?.name ?? builtInArtistPanel?.title ?? null,
2837
+ onClear: builtInArtistPanel ? () => changeArtist(null) : handleGenreSearchClear,
2838
+ onPrevious: handlePrevious,
2839
+ onQueryChange: artistSearch ? setSearchText : void 0,
2840
+ extraResults: artistExtraResults
2410
2841
  }
2411
2842
  ),
2412
- !isLoading && !readOnly && showAddRoot && /* @__PURE__ */ (0, import_jsx_runtime34.jsx)(
2413
- import_ui17.IconTextButton,
2414
- {
2415
- icon: import_lucide_react12.Plus,
2416
- text: "Add root",
2417
- onClick: () => handleGenreCreationAction(null)
2418
- }
2419
- )
2843
+ !isLoading && filterArtist && /* @__PURE__ */ (0, import_jsx_runtime39.jsx)("div", { className: "hidden w-full justify-center max-md:flex", children: renderArtistFilter() }),
2844
+ /* @__PURE__ */ (0, import_jsx_runtime39.jsxs)("div", { className: "contents max-md:flex max-md:w-full max-md:items-center max-md:gap-2 max-md:overflow-x-auto max-md:[scrollbar-width:none] max-md:[&>*]:shrink-0", children: [
2845
+ !isLoading && !isControlled && /* @__PURE__ */ (0, import_jsx_runtime39.jsx)(
2846
+ import_ui16.SegmentedControl,
2847
+ {
2848
+ "aria-label": "Tree view mode",
2849
+ value: viewMode,
2850
+ onChange: setViewMode,
2851
+ options: [
2852
+ { value: "pop-core", label: "Pop/Core", disabled: !canShowPopCore, title: canShowPopCore ? void 0 : NO_POP_ROOT_TITLE },
2853
+ { value: "wheel", label: "Wheel" },
2854
+ { value: "stacked", label: "Stacked" },
2855
+ { value: "outline", label: "Outline", disabled: !canShowPopCore, title: canShowPopCore ? void 0 : NO_POP_ROOT_TITLE }
2856
+ ]
2857
+ }
2858
+ ),
2859
+ !isLoading && showDisplayOptions && /* @__PURE__ */ (0, import_jsx_runtime39.jsx)(
2860
+ "div",
2861
+ {
2862
+ className: "flex items-center gap-1",
2863
+ role: "group",
2864
+ "aria-label": "Tree display options",
2865
+ children: !isRotationControlled && viewMode !== "stacked" && viewMode !== "outline" && /* @__PURE__ */ (0, import_jsx_runtime39.jsx)(
2866
+ import_ui16.Button,
2867
+ {
2868
+ variant: allowWheelRotation ? "default" : "outline",
2869
+ size: "sm",
2870
+ onClick: toggleWheelRotation,
2871
+ children: "Rotation"
2872
+ }
2873
+ )
2874
+ }
2875
+ ),
2876
+ !isLoading && !readOnly && showAddRoot && /* @__PURE__ */ (0, import_jsx_runtime39.jsx)(
2877
+ import_ui16.IconTextButton,
2878
+ {
2879
+ icon: import_lucide_react15.Plus,
2880
+ text: "Add root",
2881
+ onClick: () => handleGenreCreationAction(null)
2882
+ }
2883
+ )
2884
+ ] })
2420
2885
  ] });
2421
- return /* @__PURE__ */ (0, import_jsx_runtime34.jsxs)(
2886
+ return /* @__PURE__ */ (0, import_jsx_runtime39.jsxs)(
2422
2887
  "div",
2423
2888
  {
2424
2889
  ref: rootRef,
2425
- className: ["genre-tree-view relative flex flex-col h-full", placement === "regions" && REGIONS_CLASS, className].filter(Boolean).join(" "),
2890
+ className: ["genre-tree-view relative flex flex-col h-full", placement === "regions" && REGIONS_CLASS, className2].filter(Boolean).join(" "),
2426
2891
  children: [
2427
- /* @__PURE__ */ (0, import_jsx_runtime34.jsx)("style", { children: PREVIOUS_BUTTON_CSS }),
2428
- /* @__PURE__ */ (0, import_jsx_runtime34.jsx)(
2892
+ /* @__PURE__ */ (0, import_jsx_runtime39.jsx)("style", { children: PREVIOUS_BUTTON_CSS }),
2893
+ /* @__PURE__ */ (0, import_jsx_runtime39.jsx)(
2429
2894
  "div",
2430
2895
  {
2431
- 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))]" : "left-5 top-5"}`,
2896
+ 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"}`,
2432
2897
  children: actions
2433
2898
  }
2434
2899
  ),
2435
- detailTab && /* @__PURE__ */ (0, import_jsx_runtime34.jsx)(
2900
+ !isLoading && filterArtist && /* @__PURE__ */ (0, import_jsx_runtime39.jsx)(
2901
+ "div",
2902
+ {
2903
+ 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"}`,
2904
+ children: renderArtistFilter()
2905
+ }
2906
+ ),
2907
+ detailTab && /* @__PURE__ */ (0, import_jsx_runtime39.jsx)(
2436
2908
  "div",
2437
2909
  {
2438
- className: `absolute top-1/2 z-30 -translate-y-1/2 transition-[opacity,visibility] duration-150 ease-out ${isTreeReady ? "opacity-100" : "pointer-events-none invisible opacity-0"} ${detailExpanded ? "right-[calc(var(--layout-center-right)-var(--layout-edge-tab-w))]" : isCollapsed ? "left-0" : "left-[calc(var(--layout-gutter)+var(--layout-left-w))]"}`,
2910
+ className: `absolute top-1/2 z-30 -translate-y-1/2 max-md:hidden transition-[opacity,visibility] duration-150 ease-out ${isTreeReady ? "opacity-100" : "pointer-events-none invisible opacity-0"} ${detailExpanded ? "right-[calc(var(--layout-center-right)-var(--layout-edge-tab-w))]" : isCollapsed ? "left-0" : "left-[calc(var(--layout-gutter)+var(--layout-left-w))]"}`,
2439
2911
  children: detailTab
2440
2912
  }
2441
2913
  ),
2442
- /* @__PURE__ */ (0, import_jsx_runtime34.jsx)("div", { className: "content-container flex-1 min-h-0 flex flex-row gap-4", children: /* @__PURE__ */ (0, import_jsx_runtime34.jsx)("div", { className: "tree-view-container flex-1 min-w-0 flex flex-col h-full", children: isLoading ? /* @__PURE__ */ (0, import_jsx_runtime34.jsx)(import_genre_tree_view4.GenreTreeViewSkeleton, { viewMode }) : viewMode === "wheel" ? (
2443
- // The info panel's top edge aligns with the tree's; the search bar floats `--layout-panel-inset` below it,
2444
- // and the header inset (the bar's height plus that inset) extends the header's fill behind the bar,
2445
- // Google Maps style; the 8px bottom padding mirrors the inset above the bar, and the hidden title's height is zeroed.
2446
- // The panel's own Zoom in button is hidden: the search bar carries it.
2447
- /* @__PURE__ */ (0, import_jsx_runtime34.jsx)("div", { className: "tree-container search-overlay relative flex-1 w-full min-h-0 [--gtv-info-panel-header-inset:calc(var(--layout-search-h,2.5rem)+var(--layout-panel-inset,0.5rem))] [&_.gtv-info-panel-header]:pb-2 [&_.gtv-info-panel-title--hidden]:h-0 [&_.gtv-toolbar]:hidden", children: /* @__PURE__ */ (0, import_jsx_runtime34.jsx)(GenreTreeWheelHandoff, { skeleton: /* @__PURE__ */ (0, import_jsx_runtime34.jsx)(import_genre_tree_view4.GenreTreeWheelSkeleton, {}), onRevealedChange: setIsWheelRevealed, children: /* @__PURE__ */ (0, import_jsx_runtime34.jsx)(
2448
- GenrePlaylistTreeWheel,
2914
+ /* @__PURE__ */ (0, import_jsx_runtime39.jsx)("div", { className: "content-container flex-1 min-h-0 flex flex-row gap-4", children: /* @__PURE__ */ (0, import_jsx_runtime39.jsxs)("div", { className: "tree-view-container relative flex-1 min-w-0 flex flex-col h-full", children: [
2915
+ !isLoading && viewMode === "outline" && floatingControlsLeading && /* @__PURE__ */ (0, import_jsx_runtime39.jsx)("div", { className: "absolute bottom-3 right-3 z-10", children: floatingControlsLeading }),
2916
+ isLoading ? /* @__PURE__ */ (0, import_jsx_runtime39.jsx)(import_genre_tree_view4.GenreTreeViewSkeleton, { viewMode }) : viewMode === "wheel" ? (
2917
+ // The info panel's top edge aligns with the tree's; the search bar floats `--layout-panel-inset` below it,
2918
+ // and the header inset (the bar's height plus that inset) extends the header's fill behind the bar,
2919
+ // Google Maps style; the 8px bottom padding mirrors the inset above the bar, and the hidden title's height is zeroed.
2920
+ // The panel's own Zoom in button is hidden: the search bar carries it.
2921
+ /* @__PURE__ */ (0, import_jsx_runtime39.jsx)("div", { className: "tree-container search-overlay relative flex-1 w-full min-h-0 [--gtv-info-panel-header-inset:calc(var(--layout-search-h,2.5rem)+var(--layout-panel-inset,0.5rem))] [&_.gtv-info-panel-header]:pb-2 [&_.gtv-info-panel-title--hidden]:h-0 [&_.gtv-toolbar]:hidden", children: /* @__PURE__ */ (0, import_jsx_runtime39.jsx)(GenreTreeWheelHandoff, { skeleton: /* @__PURE__ */ (0, import_jsx_runtime39.jsx)(import_genre_tree_view4.GenreTreeWheelSkeleton, {}), onRevealedChange: setIsWheelRevealed, children: /* @__PURE__ */ (0, import_jsx_runtime39.jsx)(
2922
+ GenrePlaylistTreeWheel,
2923
+ {
2924
+ scope,
2925
+ genrePlaylists: visibleGenrePlaylists,
2926
+ handleGenreCreationAction,
2927
+ handleGenreRenameAction,
2928
+ additionalActions,
2929
+ onSelectedNodeChange: handleSelectedNodeChange,
2930
+ hideInfoPanelClose: true,
2931
+ hideInfoPanelTitle: true,
2932
+ onNodeHover: handleNodeHover,
2933
+ renderInfoPanelBody,
2934
+ playback,
2935
+ selectedNodeId,
2936
+ readOnly,
2937
+ allowWheelRotation,
2938
+ sizeMetric: isArtistFiltered || !canSizeByViews ? "songs" : deferredSizeMetric,
2939
+ busy,
2940
+ floatingControlsLeading: floatingControlsLeading || canSizeByViews ? /* @__PURE__ */ (0, import_jsx_runtime39.jsxs)("div", { className: "flex items-center gap-2", children: [
2941
+ floatingControlsLeading,
2942
+ canSizeByViews && /* @__PURE__ */ (0, import_jsx_runtime39.jsx)(SizeMetricToggle, { value: sizeMetric, onChange: changeSizeMetric })
2943
+ ] }) : void 0,
2944
+ defaultViewport,
2945
+ viewportRestore,
2946
+ onViewportChange,
2947
+ ...infoPanelCuration
2948
+ }
2949
+ ) }) })
2950
+ ) : viewMode === "pop-core" ? (
2951
+ // Same as the wheel: the header inset extends its fill behind the floating search bar, Google Maps style.
2952
+ /* @__PURE__ */ (0, import_jsx_runtime39.jsx)("div", { className: "tree-container search-overlay relative flex-1 w-full min-h-0 [--gtv-info-panel-header-inset:calc(var(--layout-search-h,2.5rem)+var(--layout-panel-inset,0.5rem))] [&_.gtv-info-panel-header]:pb-2 [&_.gtv-info-panel-title--hidden]:h-0 [&_.gtv-toolbar]:hidden [&_.gtv-info-panel-zoom-in]:hidden", children: /* @__PURE__ */ (0, import_jsx_runtime39.jsx)(GenreTreeWheelHandoff, { skeleton: /* @__PURE__ */ (0, import_jsx_runtime39.jsx)(import_genre_tree_view4.GenreTreeWheelSkeleton, {}), onRevealedChange: setIsWheelRevealed, children: /* @__PURE__ */ (0, import_jsx_runtime39.jsx)(
2953
+ GenrePlaylistTreeWheelRadialPopCore,
2954
+ {
2955
+ scope,
2956
+ genrePlaylists: visibleGenrePlaylists,
2957
+ handleGenreCreationAction,
2958
+ handleGenreRenameAction,
2959
+ additionalActions,
2960
+ onSelectedNodeChange: handleSelectedNodeChange,
2961
+ hideInfoPanelClose: true,
2962
+ hideInfoPanelTitle: true,
2963
+ onNodeHover: handleNodeHover,
2964
+ renderInfoPanelBody,
2965
+ playback,
2966
+ selectedNodeId,
2967
+ readOnly,
2968
+ allowWheelRotation,
2969
+ sizeMetric: isArtistFiltered || !canSizeByViews ? "songs" : deferredSizeMetric,
2970
+ busy,
2971
+ floatingControlsLeading: floatingControlsLeading || canSizeByViews ? /* @__PURE__ */ (0, import_jsx_runtime39.jsxs)("div", { className: "flex items-center gap-2", children: [
2972
+ floatingControlsLeading,
2973
+ canSizeByViews && /* @__PURE__ */ (0, import_jsx_runtime39.jsx)(SizeMetricToggle, { value: sizeMetric, onChange: changeSizeMetric })
2974
+ ] }) : void 0,
2975
+ defaultViewport,
2976
+ viewportRestore,
2977
+ onViewportChange,
2978
+ ...infoPanelCuration,
2979
+ ...focus
2980
+ }
2981
+ ) }) })
2982
+ ) : viewMode === "outline" ? /* @__PURE__ */ (0, import_jsx_runtime39.jsx)("div", { className: "tree-container flex-1 min-h-0 w-full overflow-y-auto relative [--gtv-info-panel-header-inset:calc(var(--layout-search-h,2.5rem)+var(--layout-panel-inset,0.5rem)+0.75rem)] [&_.gtv-toolbar]:hidden", children: /* @__PURE__ */ (0, import_jsx_runtime39.jsx)(
2983
+ GenrePlaylistTreeWheelRadialPopCore,
2449
2984
  {
2985
+ outline: true,
2450
2986
  scope,
2451
2987
  genrePlaylists: visibleGenrePlaylists,
2452
2988
  handleGenreCreationAction,
@@ -2460,127 +2996,192 @@ function GenreTreeView({
2460
2996
  playback,
2461
2997
  selectedNodeId,
2462
2998
  readOnly,
2463
- allowWheelRotation,
2464
- controlsOrientation,
2465
- sizeMetric: filterArtist ? "songs" : deferredSizeMetric,
2466
- busy,
2467
- floatingControlsLeading: filterArtist ? void 0 : /* @__PURE__ */ (0, import_jsx_runtime34.jsx)(SizeMetricToggle, { value: sizeMetric, onChange: changeSizeMetric }),
2468
- defaultViewport,
2469
- onViewportChange,
2470
2999
  ...infoPanelCuration
2471
3000
  }
2472
- ) }) })
2473
- ) : viewMode === "pop-core" ? (
2474
- // Same as the wheel: the header inset extends its fill behind the floating search bar, Google Maps style.
2475
- /* @__PURE__ */ (0, import_jsx_runtime34.jsx)("div", { className: "tree-container search-overlay relative flex-1 w-full min-h-0 [--gtv-info-panel-header-inset:calc(var(--layout-search-h,2.5rem)+var(--layout-panel-inset,0.5rem))] [&_.gtv-info-panel-header]:pb-2 [&_.gtv-info-panel-title--hidden]:h-0 [&_.gtv-toolbar]:hidden [&_.gtv-info-panel-zoom-in]:hidden", children: /* @__PURE__ */ (0, import_jsx_runtime34.jsx)(GenreTreeWheelHandoff, { skeleton: /* @__PURE__ */ (0, import_jsx_runtime34.jsx)(import_genre_tree_view4.GenreTreeWheelSkeleton, {}), onRevealedChange: setIsWheelRevealed, children: /* @__PURE__ */ (0, import_jsx_runtime34.jsx)(
2476
- GenrePlaylistTreeWheelRadialPopCore,
2477
- {
2478
- scope,
2479
- genrePlaylists: visibleGenrePlaylists,
2480
- handleGenreCreationAction,
2481
- handleGenreRenameAction,
2482
- additionalActions,
2483
- onSelectedNodeChange: handleSelectedNodeChange,
2484
- hideInfoPanelClose: true,
2485
- hideInfoPanelTitle: true,
2486
- onNodeHover: handleNodeHover,
2487
- renderInfoPanelBody,
2488
- playback,
2489
- selectedNodeId,
2490
- readOnly,
2491
- allowWheelRotation,
2492
- controlsOrientation,
2493
- sizeMetric: filterArtist ? "songs" : deferredSizeMetric,
2494
- busy,
2495
- floatingControlsLeading: filterArtist ? void 0 : /* @__PURE__ */ (0, import_jsx_runtime34.jsx)(SizeMetricToggle, { value: sizeMetric, onChange: changeSizeMetric }),
2496
- defaultViewport,
2497
- onViewportChange,
2498
- ...infoPanelCuration,
2499
- ...focus
3001
+ ) }) : /* @__PURE__ */ (0, import_jsx_runtime39.jsx)("div", { className: "tree-container relative flex flex-col gap-4 w-full pt-14 overflow-x-auto overflow-y-auto text-[var(--color-text)] [&_.gtv-toolbar]:hidden", children: Object.entries(groupedGenrePlaylistsByRoot).map(
3002
+ ([uuid, genrePlaylistTreePerRoot], rootIndex) => {
3003
+ return /* @__PURE__ */ (0, import_jsx_runtime39.jsx)(
3004
+ "div",
3005
+ {
3006
+ className: "tree-per-root-container relative shrink-0 h-[28rem] mt-2 mr-16 p-2 bg-[var(--color-neutral-100)] rounded-lg",
3007
+ children: /* @__PURE__ */ (0, import_jsx_runtime39.jsx)("div", { className: "graph-container relative z-10 h-full", children: /* @__PURE__ */ (0, import_jsx_runtime39.jsx)(
3008
+ GenrePlaylistTreePerRoot,
3009
+ {
3010
+ scope,
3011
+ rootUuid: uuid,
3012
+ genrePlaylistTreePerRoot,
3013
+ handleGenreCreationAction,
3014
+ handleGenreRenameAction,
3015
+ additionalActions,
3016
+ onSelectedNodeChange: handleSelectedNodeChange,
3017
+ hideInfoPanelClose: true,
3018
+ hideInfoPanelTitle: true,
3019
+ onNodeHover: handleNodeHover,
3020
+ renderInfoPanelBody,
3021
+ playback,
3022
+ selectedNodeId: uuid === selectedGenrePlaylist?.root.uuid ? selectedNodeId : null,
3023
+ readOnly,
3024
+ wheelZoom: "modifier",
3025
+ defaultViewport: uuid === selectedGenrePlaylist?.root.uuid ? defaultViewport : void 0,
3026
+ viewportRestore: uuid === selectedGenrePlaylist?.root.uuid && uuid === restoreRootUuid ? viewportRestore : void 0,
3027
+ onViewportChange: uuid === selectedGenrePlaylist?.root.uuid ? onViewportChange : void 0,
3028
+ ...infoPanelCuration,
3029
+ overlayPanel: rootIndex === 0 ? activeOverlayPanel : void 0
3030
+ }
3031
+ ) })
3032
+ },
3033
+ uuid
3034
+ );
2500
3035
  }
2501
- ) }) })
2502
- ) : viewMode === "outline" ? /* @__PURE__ */ (0, import_jsx_runtime34.jsx)("div", { className: "tree-container flex-1 min-h-0 w-full overflow-y-auto relative [--gtv-info-panel-header-inset:calc(var(--layout-search-h,2.5rem)+var(--layout-panel-inset,0.5rem)+0.75rem)] [&_.gtv-toolbar]:hidden", children: /* @__PURE__ */ (0, import_jsx_runtime34.jsx)(
2503
- GenrePlaylistTreeWheelRadialPopCore,
2504
- {
2505
- outline: true,
2506
- scope,
2507
- genrePlaylists: visibleGenrePlaylists,
2508
- handleGenreCreationAction,
2509
- handleGenreRenameAction,
2510
- additionalActions,
2511
- onSelectedNodeChange: handleSelectedNodeChange,
2512
- hideInfoPanelClose: true,
2513
- hideInfoPanelTitle: true,
2514
- onNodeHover: handleNodeHover,
2515
- renderInfoPanelBody,
2516
- playback,
2517
- selectedNodeId,
2518
- readOnly,
2519
- ...infoPanelCuration
2520
- }
2521
- ) }) : /* @__PURE__ */ (0, import_jsx_runtime34.jsx)("div", { className: "tree-container relative flex flex-col gap-4 w-full pt-14 overflow-x-auto overflow-y-auto text-[var(--color-text)] [&_.gtv-toolbar]:hidden", children: Object.entries(groupedGenrePlaylistsByRoot).map(
2522
- ([uuid, genrePlaylistTreePerRoot], rootIndex) => {
2523
- return /* @__PURE__ */ (0, import_jsx_runtime34.jsx)(
2524
- "div",
2525
- {
2526
- className: "tree-per-root-container relative shrink-0 h-[28rem] mt-2 mr-16 p-2 bg-[var(--color-neutral-100)] rounded-lg",
2527
- children: /* @__PURE__ */ (0, import_jsx_runtime34.jsx)("div", { className: "graph-container relative z-10 h-full", children: /* @__PURE__ */ (0, import_jsx_runtime34.jsx)(
2528
- GenrePlaylistTreePerRoot,
2529
- {
2530
- scope,
2531
- rootUuid: uuid,
2532
- genrePlaylistTreePerRoot,
2533
- handleGenreCreationAction,
2534
- handleGenreRenameAction,
2535
- additionalActions,
2536
- onSelectedNodeChange: handleSelectedNodeChange,
2537
- hideInfoPanelClose: true,
2538
- hideInfoPanelTitle: true,
2539
- onNodeHover: handleNodeHover,
2540
- renderInfoPanelBody,
2541
- playback,
2542
- selectedNodeId: uuid === selectedGenrePlaylist?.root.uuid ? selectedNodeId : null,
2543
- readOnly,
2544
- wheelZoom: "modifier",
2545
- controlsOrientation,
2546
- defaultViewport: uuid === selectedGenrePlaylist?.root.uuid ? defaultViewport : void 0,
2547
- onViewportChange: uuid === selectedGenrePlaylist?.root.uuid ? onViewportChange : void 0,
2548
- ...infoPanelCuration,
2549
- overlayPanel: rootIndex === 0 ? activeOverlayPanel : void 0
2550
- }
2551
- ) })
2552
- },
2553
- uuid
2554
- );
2555
- }
2556
- ) }) }) })
3036
+ ) })
3037
+ ] }) })
2557
3038
  ]
2558
3039
  }
2559
3040
  );
2560
3041
  }
2561
3042
 
2562
- // src/genre-tree/index.ts
3043
+ // src/genre-tree/GenreDetailPanel.tsx
3044
+ var import_react16 = require("react");
2563
3045
  var import_genre_tree_view5 = require("@behindthemusictree/genre-tree-view");
3046
+ var import_ui17 = require("@behindthemusictree/ui");
3047
+ var import_genre_tree18 = require("@behindthemusictree/app-kit/genre-tree");
3048
+ var import_jsx_runtime40 = require("react/jsx-runtime");
3049
+ var ITEM_COUNT_LABEL = "Songs";
3050
+ var RELATIVE_ITEM_COUNT = 1;
3051
+ function toNode(genre, parentId) {
3052
+ return {
3053
+ id: genre.uuid,
3054
+ parentId,
3055
+ name: genre.name,
3056
+ itemCount: RELATIVE_ITEM_COUNT,
3057
+ side: genre.side ?? void 0,
3058
+ wikidataId: genre.wikidataId
3059
+ };
3060
+ }
3061
+ function GenreDetailPanel({
3062
+ genreUuid,
3063
+ scope,
3064
+ getBackendBaseUrl,
3065
+ onClose,
3066
+ onSelectGenre,
3067
+ onZoomIn,
3068
+ shareUrl,
3069
+ shareTarget,
3070
+ collapsed,
3071
+ expanded,
3072
+ onExpandedChange,
3073
+ extras
3074
+ }) {
3075
+ const { data: detail, isPending, isError } = (0, import_genre_tree18.useFetchGenreDetail)(genreUuid, scope, getBackendBaseUrl);
3076
+ const playback = useGenrePlaylistPlayback({ scope, genrePlaylists: [] });
3077
+ const view = (0, import_react16.useMemo)(() => {
3078
+ if (!detail) return null;
3079
+ const rootColor = (0, import_genre_tree_view5.getGenreTreeColor)(detail.root.wikidataId);
3080
+ const relative = (genre, parentId) => ({
3081
+ node: toNode(genre, parentId),
3082
+ fill: (0, import_genre_tree_view5.tintSurface)(rootColor),
3083
+ textColor: import_genre_tree_view5.TEXT_COLOR
3084
+ });
3085
+ const ancestors = [...detail.ascendants].sort((a, b) => b.degree - a.degree);
3086
+ return {
3087
+ node: {
3088
+ id: detail.uuid,
3089
+ parentId: detail.parent?.uuid ?? null,
3090
+ name: detail.name,
3091
+ itemCount: detail.tracksCount,
3092
+ wikidataId: detail.wikidataId
3093
+ },
3094
+ fill: rootColor,
3095
+ parentNode: detail.parent ? relative(detail.parent, null) : null,
3096
+ childNodes: detail.children.map((child) => relative(child, detail.uuid)),
3097
+ ancestorNodes: ancestors.map(
3098
+ ({ ascendant }, index) => relative(ascendant, index === 0 ? null : ancestors[index - 1].ascendant.uuid)
3099
+ )
3100
+ };
3101
+ }, [detail]);
3102
+ if (isError) {
3103
+ return /* @__PURE__ */ (0, import_jsx_runtime40.jsx)("div", { role: "alert", className: "p-4 text-sm text-[var(--color-text)]", children: "This genre could not be loaded." });
3104
+ }
3105
+ if (isPending || !detail || !view) {
3106
+ return /* @__PURE__ */ (0, import_jsx_runtime40.jsxs)("div", { className: "flex flex-col gap-3 p-4", "aria-busy": "true", "data-testid": "genre-detail-panel-skeleton", children: [
3107
+ /* @__PURE__ */ (0, import_jsx_runtime40.jsx)(import_ui17.Skeleton, { className: "h-6 w-2/3" }),
3108
+ /* @__PURE__ */ (0, import_jsx_runtime40.jsx)(import_ui17.Skeleton, { className: "h-4 w-full" }),
3109
+ /* @__PURE__ */ (0, import_jsx_runtime40.jsx)(import_ui17.Skeleton, { className: "h-4 w-3/4" })
3110
+ ] });
3111
+ }
3112
+ const renderBody = (node, sections) => ({
3113
+ primary: /* @__PURE__ */ (0, import_jsx_runtime40.jsx)(
3114
+ GenreInfoPanelBody,
3115
+ {
3116
+ toolbar: sections.toolbar,
3117
+ family: sections.family,
3118
+ genreUuid: detail.uuid,
3119
+ genrePlaylist: { ...detail.criteriaPlaylist, tracksCount: detail.tracksCount },
3120
+ wikidataId: detail.wikidataId,
3121
+ scope,
3122
+ getBackendBaseUrl,
3123
+ playback,
3124
+ band: { node, onZoomIn, shareUrl, shareTarget: shareUrl ? shareTarget : void 0 },
3125
+ extras
3126
+ }
3127
+ ),
3128
+ aside: null
3129
+ });
3130
+ return /* @__PURE__ */ (0, import_jsx_runtime40.jsx)(
3131
+ import_genre_tree_view5.InfoPanel,
3132
+ {
3133
+ node: view.node,
3134
+ fill: view.fill,
3135
+ textColor: import_genre_tree_view5.ACCENT_TEXT_COLOR,
3136
+ parentNode: view.parentNode,
3137
+ childNodes: view.childNodes,
3138
+ ancestorNodes: view.ancestorNodes,
3139
+ side: "left",
3140
+ onClose,
3141
+ onSelectNode: (nodeId) => onSelectGenre(nodeId),
3142
+ itemCountLabel: ITEM_COUNT_LABEL,
3143
+ renderInfoPanelBody: renderBody,
3144
+ expanded,
3145
+ onExpandedChange,
3146
+ collapsed,
3147
+ hideExpandButton: onExpandedChange === void 0
3148
+ }
3149
+ );
3150
+ }
3151
+
3152
+ // src/genre-tree/index.ts
3153
+ var import_genre_tree_view6 = require("@behindthemusictree/genre-tree-view");
2564
3154
 
2565
3155
  // src/genre-tree/track-list-sidebar/TrackListSidebar.tsx
2566
- var import_react11 = require("react");
3156
+ var import_react17 = require("react");
3157
+ var import_lucide_react16 = require("lucide-react");
2567
3158
  var import_genre_tree19 = require("@behindthemusictree/app-kit/genre-tree");
2568
3159
  var import_genre_tree20 = require("@behindthemusictree/app-kit/genre-tree");
2569
- var import_genre_tree21 = require("@behindthemusictree/app-kit/genre-tree");
2570
3160
  var import_track_noun7 = require("@behindthemusictree/app-kit/track-noun");
2571
- var import_jsx_runtime35 = require("react/jsx-runtime");
3161
+ var import_jsx_runtime41 = require("react/jsx-runtime");
3162
+ var useBrowserLayoutEffect = typeof window === "undefined" ? import_react17.useEffect : import_react17.useLayoutEffect;
2572
3163
  function TrackListSidebar({
2573
- className = "",
3164
+ className: className2 = "",
2574
3165
  renderDuration,
2575
3166
  renderActions,
2576
3167
  layout = "fixed",
3168
+ isCollapsed = false,
3169
+ onExpand,
3170
+ onMinimize,
2577
3171
  renderArtistLink
2578
3172
  }) {
2579
- const { trackList, loadMore } = (0, import_genre_tree19.useTrackList)();
3173
+ const { trackList, loadMore, autoAdvanceScrollRequest } = (0, import_genre_tree19.useTrackList)();
2580
3174
  const trackNoun = (0, import_track_noun7.useTrackNoun)();
2581
- const loadMoreSentinelRef = (0, import_react11.useRef)(null);
3175
+ const trackListRef = (0, import_react17.useRef)(null);
3176
+ const previousOriginRef = (0, import_react17.useRef)(trackList?.origin ?? null);
3177
+ const handledAutoAdvanceRequestIdRef = (0, import_react17.useRef)(null);
3178
+ const shouldFollowPlaybackRef = (0, import_react17.useRef)(true);
3179
+ const programmaticScrollTopRef = (0, import_react17.useRef)(null);
3180
+ const userScrollIntentRef = (0, import_react17.useRef)(false);
3181
+ const userScrollIntentTimerRef = (0, import_react17.useRef)(null);
3182
+ const loadMoreSentinelRef = (0, import_react17.useRef)(null);
2582
3183
  const hasMore = !!trackList && trackList.nextPage !== null;
2583
- (0, import_react11.useEffect)(() => {
3184
+ (0, import_react17.useEffect)(() => {
2584
3185
  const sentinel = loadMoreSentinelRef.current;
2585
3186
  if (!hasMore || !sentinel) return;
2586
3187
  const observer = new IntersectionObserver((entries) => {
@@ -2591,44 +3192,115 @@ function TrackListSidebar({
2591
3192
  observer.observe(sentinel);
2592
3193
  return () => observer.disconnect();
2593
3194
  }, [hasMore, loadMore]);
2594
- const { hideTrackListSidebar } = (0, import_genre_tree20.useTrackListSidebarVisibility)();
2595
- const positionClasses = layout === "inline" ? "relative w-full flex h-full flex-col" : "fixed right-0 w-[50rem]";
2596
- return trackList ? /* @__PURE__ */ (0, import_jsx_runtime35.jsxs)(
3195
+ useBrowserLayoutEffect(() => {
3196
+ const origin = trackList?.origin ?? null;
3197
+ if (previousOriginRef.current === origin) return;
3198
+ previousOriginRef.current = origin;
3199
+ handledAutoAdvanceRequestIdRef.current = null;
3200
+ shouldFollowPlaybackRef.current = true;
3201
+ programmaticScrollTopRef.current = null;
3202
+ if (trackListRef.current) trackListRef.current.scrollTop = 0;
3203
+ }, [trackList?.origin]);
3204
+ (0, import_react17.useEffect)(() => {
3205
+ const list = trackListRef.current;
3206
+ if (!isCollapsed && list?.scrollTop === 0) shouldFollowPlaybackRef.current = true;
3207
+ }, [isCollapsed]);
3208
+ (0, import_react17.useEffect)(
3209
+ () => () => {
3210
+ if (userScrollIntentTimerRef.current !== null) clearTimeout(userScrollIntentTimerRef.current);
3211
+ },
3212
+ []
3213
+ );
3214
+ useBrowserLayoutEffect(() => {
3215
+ if (!autoAdvanceScrollRequest || handledAutoAdvanceRequestIdRef.current === autoAdvanceScrollRequest.id) {
3216
+ return;
3217
+ }
3218
+ if (isCollapsed) return;
3219
+ const list = trackListRef.current;
3220
+ if (!list) return;
3221
+ if (list.scrollTop <= 1) shouldFollowPlaybackRef.current = true;
3222
+ if (!shouldFollowPlaybackRef.current) {
3223
+ handledAutoAdvanceRequestIdRef.current = autoAdvanceScrollRequest.id;
3224
+ return;
3225
+ }
3226
+ const row = Array.from(list.querySelectorAll("[data-track-uuid]")).find(
3227
+ (element) => element.dataset.trackUuid === autoAdvanceScrollRequest.trackUuid
3228
+ );
3229
+ if (!row) return;
3230
+ handledAutoAdvanceRequestIdRef.current = autoAdvanceScrollRequest.id;
3231
+ const rowTop = row.getBoundingClientRect().top - list.getBoundingClientRect().top;
3232
+ const targetScrollTop = list.scrollTop + rowTop;
3233
+ programmaticScrollTopRef.current = targetScrollTop;
3234
+ list.scrollTop = targetScrollTop;
3235
+ programmaticScrollTopRef.current = list.scrollTop;
3236
+ }, [autoAdvanceScrollRequest, isCollapsed, trackList]);
3237
+ const markUserScrollIntent = () => {
3238
+ userScrollIntentRef.current = true;
3239
+ if (userScrollIntentTimerRef.current !== null) clearTimeout(userScrollIntentTimerRef.current);
3240
+ userScrollIntentTimerRef.current = setTimeout(() => {
3241
+ userScrollIntentRef.current = false;
3242
+ userScrollIntentTimerRef.current = null;
3243
+ }, 250);
3244
+ };
3245
+ const handleTrackListScroll = (event) => {
3246
+ const list = event.currentTarget;
3247
+ if (userScrollIntentRef.current) {
3248
+ userScrollIntentRef.current = false;
3249
+ if (userScrollIntentTimerRef.current !== null) clearTimeout(userScrollIntentTimerRef.current);
3250
+ userScrollIntentTimerRef.current = null;
3251
+ programmaticScrollTopRef.current = null;
3252
+ shouldFollowPlaybackRef.current = list.scrollTop <= 1;
3253
+ return;
3254
+ }
3255
+ const programmaticScrollTop = programmaticScrollTopRef.current;
3256
+ if (programmaticScrollTop !== null) {
3257
+ if (Math.abs(list.scrollTop - programmaticScrollTop) <= 1) programmaticScrollTopRef.current = null;
3258
+ return;
3259
+ }
3260
+ };
3261
+ const positionClasses = layout === "inline" ? "relative w-full flex h-full flex-col" : "fixed right-0 w-[50rem] flex flex-col";
3262
+ return trackList ? /* @__PURE__ */ (0, import_jsx_runtime41.jsxs)(
2597
3263
  "div",
2598
3264
  {
2599
3265
  "data-theme": "dark",
2600
- className: `track-list-sidebar ${positionClasses} rounded-2xl bg-[var(--color-surface)] pb-1 ${className}`,
2601
- style: layout === "fixed" ? {
2602
- bottom: "79px"
2603
- /* Match player height exactly */
2604
- } : void 0,
3266
+ className: `track-list-sidebar ${positionClasses} rounded-2xl bg-[var(--color-surface)] pb-1 ${className2}`,
3267
+ style: layout === "fixed" ? { top: "63.5px", maxHeight: "calc(100vh - 63.5px - 79px)" } : void 0,
2605
3268
  children: [
2606
- /* @__PURE__ */ (0, import_jsx_runtime35.jsxs)("div", { className: "header flex h-16 px-4 py-2 text-[var(--color-text)]", children: [
2607
- /* @__PURE__ */ (0, import_jsx_runtime35.jsxs)("div", { className: "origin flex text-xl ", children: [
2608
- /* @__PURE__ */ (0, import_jsx_runtime35.jsx)("div", { className: "from h-auto flex flex-col justify-center items-center mr-2", children: "From" }),
2609
- /* @__PURE__ */ (0, import_jsx_runtime35.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_runtime35.jsx)("div", { className: "name max-w-full truncate", children: trackList.origin.label }) })
2610
- ] }),
2611
- /* @__PURE__ */ (0, import_jsx_runtime35.jsxs)("div", { className: "info flex flex-col justify-center items-center pt-1 mr-2 text-[var(--color-text-muted)]", children: [
2612
- trackList.origin.type === import_genre_tree21.TrackListOriginType.GENRE_PLAYLIST ? "\u2022 Genre playlist \u2022 " : trackList.origin.type === import_genre_tree21.TrackListOriginType.ARTIST ? "\u2022 Artist \u2022 " : `\u2022 ${trackNoun.singular} playlist \u2022 `,
2613
- `${trackList.total} ${trackList.total > 1 ? trackNoun.plural : trackNoun.singular} \u2022`
3269
+ /* @__PURE__ */ (0, import_jsx_runtime41.jsxs)("div", { className: "header flex h-16 items-center px-4 py-2 text-[var(--color-text)]", children: [
3270
+ /* @__PURE__ */ (0, import_jsx_runtime41.jsxs)("div", { className: "min-w-0 flex flex-1 flex-col justify-center", children: [
3271
+ /* @__PURE__ */ (0, import_jsx_runtime41.jsxs)("div", { className: "origin flex text-xl", children: [
3272
+ /* @__PURE__ */ (0, import_jsx_runtime41.jsx)("div", { className: "from h-auto flex flex-col justify-center items-center mr-2", children: "From" }),
3273
+ /* @__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 }) })
3274
+ ] }),
3275
+ /* @__PURE__ */ (0, import_jsx_runtime41.jsxs)("div", { className: "info flex items-center text-sm text-[var(--color-text-muted)]", children: [
3276
+ trackList.origin.type === import_genre_tree20.TrackListOriginType.GENRE_PLAYLIST ? "Genre \u2022 " : trackList.origin.type === import_genre_tree20.TrackListOriginType.ARTIST ? "Artist \u2022 " : `${trackNoun.singular} playlist \u2022 `,
3277
+ `${trackList.total} ${trackList.total > 1 ? trackNoun.plural : trackNoun.singular}`
3278
+ ] })
2614
3279
  ] }),
2615
- /* @__PURE__ */ (0, import_jsx_runtime35.jsx)(
3280
+ (isCollapsed && onExpand || !isCollapsed && onMinimize) && /* @__PURE__ */ (0, import_jsx_runtime41.jsx)(
2616
3281
  "button",
2617
3282
  {
2618
3283
  type: "button",
2619
- "aria-label": "Close track list",
2620
- className: "flex-grow flex flex-col items-end justify-center h-full p-3 cursor-pointer text-inherit",
2621
- onClick: hideTrackListSidebar,
2622
- children: "\u2715"
3284
+ "aria-label": isCollapsed ? "Show tracks" : "Hide tracks",
3285
+ title: isCollapsed ? "Show tracks" : "Hide tracks",
3286
+ "aria-expanded": !isCollapsed,
3287
+ 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"}`,
3288
+ onClick: isCollapsed ? onExpand : onMinimize,
3289
+ children: /* @__PURE__ */ (0, import_jsx_runtime41.jsx)(import_lucide_react16.ListMusic, { "aria-hidden": "true", className: "h-5 w-5" })
2623
3290
  }
2624
3291
  )
2625
3292
  ] }),
2626
- /* @__PURE__ */ (0, import_jsx_runtime35.jsxs)(
3293
+ !isCollapsed && /* @__PURE__ */ (0, import_jsx_runtime41.jsxs)(
2627
3294
  "ul",
2628
3295
  {
2629
- className: layout === "inline" ? "track-list flex-1 min-h-0 overflow-auto list-none p-0 m-0" : "track-list overflow-auto max-h-[calc(100vh-63.5px-79px-56px-3.5px)] list-none p-0 m-0",
3296
+ ref: trackListRef,
3297
+ onScroll: handleTrackListScroll,
3298
+ onWheel: markUserScrollIntent,
3299
+ onTouchMove: markUserScrollIntent,
3300
+ onPointerDown: markUserScrollIntent,
3301
+ 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",
2630
3302
  children: [
2631
- trackList.tracks.map((track, index) => /* @__PURE__ */ (0, import_jsx_runtime35.jsx)("li", { children: /* @__PURE__ */ (0, import_jsx_runtime35.jsx)(
3303
+ trackList.tracks.map((track, index) => /* @__PURE__ */ (0, import_jsx_runtime41.jsx)("li", { "data-track-uuid": track.uuid, children: /* @__PURE__ */ (0, import_jsx_runtime41.jsx)(
2632
3304
  TrackItem,
2633
3305
  {
2634
3306
  track,
@@ -2638,7 +3310,7 @@ function TrackListSidebar({
2638
3310
  renderArtistLink
2639
3311
  }
2640
3312
  ) }, track.uuid)),
2641
- hasMore && /* @__PURE__ */ (0, import_jsx_runtime35.jsx)("li", { ref: loadMoreSentinelRef, "aria-hidden": "true", className: "h-px" })
3313
+ hasMore && /* @__PURE__ */ (0, import_jsx_runtime41.jsx)("li", { ref: loadMoreSentinelRef, "aria-hidden": "true", className: "h-px" })
2642
3314
  ]
2643
3315
  }
2644
3316
  )
@@ -2648,42 +3320,162 @@ function TrackListSidebar({
2648
3320
  }
2649
3321
 
2650
3322
  // src/genre-tree/PlayerPanel.tsx
2651
- var import_react12 = require("react");
2652
- var import_lucide_react13 = require("lucide-react");
3323
+ var import_react18 = require("react");
2653
3324
  var import_tailwind_merge2 = require("tailwind-merge");
2654
- var import_player11 = require("@behindthemusictree/app-kit/player");
3325
+ var import_player12 = require("@behindthemusictree/app-kit/player");
2655
3326
  var import_genre_tree22 = require("@behindthemusictree/app-kit/genre-tree");
2656
- var import_jsx_runtime36 = require("react/jsx-runtime");
3327
+
3328
+ // src/genre-tree/PlayerControls.tsx
3329
+ var import_lucide_react17 = require("lucide-react");
3330
+ var import_genre_tree21 = require("@behindthemusictree/app-kit/genre-tree");
3331
+ var import_player11 = require("@behindthemusictree/app-kit/player");
3332
+ var import_jsx_runtime42 = require("react/jsx-runtime");
3333
+ function PlayerControls() {
3334
+ const {
3335
+ duration,
3336
+ handlePlayPauseAction,
3337
+ isLoading,
3338
+ playState,
3339
+ playerTrackObject,
3340
+ seekTo,
3341
+ setVolume,
3342
+ volume
3343
+ } = (0, import_player11.usePlayer)();
3344
+ const { hasNextTrack, hasPreviousTrack, playNextTrack, playPreviousTrack } = (0, import_genre_tree21.useTrackList)();
3345
+ const currentTime = (0, import_player11.useCurrentTime)();
3346
+ const isBusy = isLoading || playState === import_player11.PlayStates.LOADING;
3347
+ const isReady = !!playerTrackObject?.isReady && !isBusy && !playerTrackObject.loadError;
3348
+ const safeDuration = Number.isFinite(duration) && duration > 0 ? duration : 0;
3349
+ const safeCurrentTime = Number.isFinite(currentTime) ? Math.max(0, Math.min(currentTime, safeDuration)) : 0;
3350
+ const isPlaying = playState === import_player11.PlayStates.PLAYING;
3351
+ return /* @__PURE__ */ (0, import_jsx_runtime42.jsxs)(
3352
+ "section",
3353
+ {
3354
+ "aria-label": "Player controls",
3355
+ className: "player-controls flex shrink-0 flex-col gap-2 border-b border-zinc-800 px-3 py-2 text-zinc-200",
3356
+ children: [
3357
+ /* @__PURE__ */ (0, import_jsx_runtime42.jsxs)("div", { className: "flex min-w-0 items-center gap-2 text-xs tabular-nums text-zinc-400", children: [
3358
+ /* @__PURE__ */ (0, import_jsx_runtime42.jsx)("span", { "aria-label": "Elapsed time", children: (0, import_genre_tree21.formatTime)(safeCurrentTime) }),
3359
+ /* @__PURE__ */ (0, import_jsx_runtime42.jsx)(
3360
+ "input",
3361
+ {
3362
+ "aria-label": "Seek",
3363
+ className: "h-1 min-w-0 flex-1 cursor-pointer accent-white disabled:cursor-not-allowed disabled:opacity-40",
3364
+ type: "range",
3365
+ min: 0,
3366
+ max: safeDuration,
3367
+ step: 0.1,
3368
+ value: safeCurrentTime,
3369
+ disabled: !isReady || safeDuration <= 0,
3370
+ onChange: (event) => seekTo(Number(event.currentTarget.value))
3371
+ }
3372
+ ),
3373
+ /* @__PURE__ */ (0, import_jsx_runtime42.jsx)("span", { "aria-label": "Duration", children: (0, import_genre_tree21.formatTime)(safeDuration) })
3374
+ ] }),
3375
+ /* @__PURE__ */ (0, import_jsx_runtime42.jsxs)("div", { className: "grid min-h-9 grid-cols-[1fr_auto_1fr] items-center gap-3", children: [
3376
+ /* @__PURE__ */ (0, import_jsx_runtime42.jsxs)("div", { className: "col-start-2 flex items-center justify-center gap-2", children: [
3377
+ /* @__PURE__ */ (0, import_jsx_runtime42.jsx)(
3378
+ "button",
3379
+ {
3380
+ type: "button",
3381
+ "aria-label": "Previous track",
3382
+ title: "Previous track",
3383
+ disabled: !isReady || !hasPreviousTrack,
3384
+ onClick: playPreviousTrack,
3385
+ 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",
3386
+ children: /* @__PURE__ */ (0, import_jsx_runtime42.jsx)(import_lucide_react17.SkipBack, { "aria-hidden": "true", className: "h-4 w-4" })
3387
+ }
3388
+ ),
3389
+ /* @__PURE__ */ (0, import_jsx_runtime42.jsx)(
3390
+ "button",
3391
+ {
3392
+ type: "button",
3393
+ "aria-label": isPlaying ? "Pause" : "Play",
3394
+ title: isPlaying ? "Pause" : "Play",
3395
+ disabled: !isReady,
3396
+ onClick: handlePlayPauseAction,
3397
+ 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",
3398
+ children: isPlaying ? /* @__PURE__ */ (0, import_jsx_runtime42.jsx)(import_lucide_react17.Pause, { "aria-hidden": "true", className: "h-4 w-4", fill: "currentColor" }) : /* @__PURE__ */ (0, import_jsx_runtime42.jsx)(import_lucide_react17.Play, { "aria-hidden": "true", className: "h-4 w-4", fill: "currentColor" })
3399
+ }
3400
+ ),
3401
+ /* @__PURE__ */ (0, import_jsx_runtime42.jsx)(
3402
+ "button",
3403
+ {
3404
+ type: "button",
3405
+ "aria-label": "Next track",
3406
+ title: "Next track",
3407
+ disabled: !isReady || !hasNextTrack,
3408
+ onClick: playNextTrack,
3409
+ 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",
3410
+ children: /* @__PURE__ */ (0, import_jsx_runtime42.jsx)(import_lucide_react17.SkipForward, { "aria-hidden": "true", className: "h-4 w-4" })
3411
+ }
3412
+ )
3413
+ ] }),
3414
+ /* @__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: [
3415
+ volume === 0 ? /* @__PURE__ */ (0, import_jsx_runtime42.jsx)(import_lucide_react17.VolumeX, { "aria-hidden": "true", className: "h-[0.7rem] w-[0.7rem] shrink-0" }) : /* @__PURE__ */ (0, import_jsx_runtime42.jsx)(import_lucide_react17.Volume2, { "aria-hidden": "true", className: "h-[0.7rem] w-[0.7rem] shrink-0" }),
3416
+ /* @__PURE__ */ (0, import_jsx_runtime42.jsx)("span", { className: "sr-only", children: "Volume" }),
3417
+ /* @__PURE__ */ (0, import_jsx_runtime42.jsx)(
3418
+ "input",
3419
+ {
3420
+ "aria-label": "Volume",
3421
+ className: "h-[3px] min-w-0 flex-1 cursor-pointer accent-white disabled:cursor-not-allowed disabled:opacity-40",
3422
+ type: "range",
3423
+ min: 0,
3424
+ max: 100,
3425
+ step: 1,
3426
+ value: volume,
3427
+ disabled: !isReady,
3428
+ onChange: (event) => setVolume(Number(event.currentTarget.value))
3429
+ }
3430
+ )
3431
+ ] })
3432
+ ] })
3433
+ ]
3434
+ }
3435
+ );
3436
+ }
3437
+
3438
+ // src/genre-tree/PlayerPanel.tsx
3439
+ var import_jsx_runtime43 = require("react/jsx-runtime");
2657
3440
  var PANEL_ID = "player-panel";
2658
- var iconButtonClassName = "flex h-6 w-6 shrink-0 items-center justify-center p-0 rounded-md hover:bg-white/10";
2659
3441
  var DEFAULT_EXTENDED_WIDTH = "calc(100vw - var(--layout-center-left) - var(--layout-gutter))";
2660
3442
  function PlayerPanel({
2661
- className,
3443
+ className: className2,
2662
3444
  isVisible = true,
2663
- onClose,
2664
3445
  expanded,
2665
3446
  onExpandedChange,
2666
3447
  extendedWidth = DEFAULT_EXTENDED_WIDTH,
2667
3448
  renderArtistLink
2668
3449
  }) {
2669
- const { playerTrackObject } = (0, import_player11.usePlayer)();
3450
+ const { playerTrackObject } = (0, import_player12.usePlayer)();
2670
3451
  const { toggleTrackListSidebar, isTrackListSidebarVisible } = (0, import_genre_tree22.useTrackListSidebarVisibility)();
2671
- const [uncontrolledExtended, setUncontrolledExtended] = (0, import_react12.useState)(false);
3452
+ const [lastLoadedTrackTitle, setLastLoadedTrackTitle] = (0, import_react18.useState)("");
3453
+ const loadedTrackTitle = playerTrackObject?.track.title;
3454
+ (0, import_react18.useEffect)(() => {
3455
+ if (loadedTrackTitle) setLastLoadedTrackTitle(loadedTrackTitle);
3456
+ }, [loadedTrackTitle]);
3457
+ const [uncontrolledExtended, setUncontrolledExtended] = (0, import_react18.useState)(false);
2672
3458
  const isExtendRequested = expanded ?? uncontrolledExtended;
2673
3459
  const setIsExtendRequested = (next) => {
2674
3460
  setUncontrolledExtended(next);
2675
3461
  onExpandedChange?.(next);
2676
3462
  };
2677
3463
  const isExtended = isExtendRequested && isTrackListSidebarVisible;
2678
- const tabEdge = !isTrackListSidebarVisible ? null : isExtended ? {
2679
- label: "Shrink song list panel",
2680
- onClick: () => setIsExtendRequested(false)
2681
- } : { label: "Hide song list", onClick: toggleTrackListSidebar };
2682
- const tabCenter = !isTrackListSidebarVisible ? { label: "Show song list", onClick: toggleTrackListSidebar } : isExtended ? null : {
2683
- label: "Extend song list panel",
2684
- onClick: () => setIsExtendRequested(true)
3464
+ const hideSongList = () => {
3465
+ setIsExtendRequested(false);
3466
+ if (isTrackListSidebarVisible) toggleTrackListSidebar();
2685
3467
  };
2686
- return /* @__PURE__ */ (0, import_jsx_runtime36.jsxs)(
3468
+ const tabEdge = isTrackListSidebarVisible && isExtended ? {
3469
+ label: "Shrink player panel",
3470
+ onClick: () => setIsExtendRequested(false),
3471
+ expanded: true
3472
+ } : null;
3473
+ const tabCenter = isTrackListSidebarVisible && !isExtended ? {
3474
+ label: "Extend player panel",
3475
+ onClick: () => setIsExtendRequested(true),
3476
+ expanded: false
3477
+ } : null;
3478
+ return /* @__PURE__ */ (0, import_jsx_runtime43.jsxs)(
2687
3479
  "div",
2688
3480
  {
2689
3481
  id: PANEL_ID,
@@ -2692,11 +3484,11 @@ function PlayerPanel({
2692
3484
  !isExtended && "w-[var(--layout-right-w)]",
2693
3485
  isTrackListSidebarVisible && "h-[calc(100vh-var(--layout-band-top)-3.5rem)]",
2694
3486
  !isVisible && "hidden",
2695
- className
3487
+ className2
2696
3488
  ),
2697
3489
  style: isExtended ? { width: extendedWidth } : void 0,
2698
3490
  children: [
2699
- /* @__PURE__ */ (0, import_jsx_runtime36.jsx)(
3491
+ /* @__PURE__ */ (0, import_jsx_runtime43.jsx)(
2700
3492
  PanelEdgeTab,
2701
3493
  {
2702
3494
  side: "right",
@@ -2706,7 +3498,7 @@ function PlayerPanel({
2706
3498
  className: "absolute right-full top-1/2 -translate-y-1/2"
2707
3499
  }
2708
3500
  ),
2709
- /* @__PURE__ */ (0, import_jsx_runtime36.jsxs)(
3501
+ /* @__PURE__ */ (0, import_jsx_runtime43.jsxs)(
2710
3502
  "div",
2711
3503
  {
2712
3504
  className: (0, import_tailwind_merge2.twMerge)(
@@ -2714,29 +3506,21 @@ function PlayerPanel({
2714
3506
  isExtended ? "flex-row" : "flex-col"
2715
3507
  ),
2716
3508
  children: [
2717
- /* @__PURE__ */ (0, import_jsx_runtime36.jsxs)("div", { className: "flex w-[var(--layout-right-w)] shrink-0 flex-col", children: [
2718
- /* @__PURE__ */ (0, import_jsx_runtime36.jsxs)("div", { className: "flex shrink-0 items-center justify-between gap-2 border-b border-zinc-800 p-2", children: [
2719
- /* @__PURE__ */ (0, import_jsx_runtime36.jsx)("span", { className: "min-w-0 truncate text-sm font-semibold", children: playerTrackObject?.track?.title }),
2720
- /* @__PURE__ */ (0, import_jsx_runtime36.jsx)("div", { className: "flex shrink-0 items-center gap-1", children: /* @__PURE__ */ (0, import_jsx_runtime36.jsx)(
2721
- "button",
2722
- {
2723
- type: "button",
2724
- onClick: onClose,
2725
- className: iconButtonClassName,
2726
- "aria-label": "Close player",
2727
- children: /* @__PURE__ */ (0, import_jsx_runtime36.jsx)(import_lucide_react13.X, { size: 16 })
2728
- }
2729
- ) })
3509
+ /* @__PURE__ */ (0, import_jsx_runtime43.jsxs)("div", { className: "flex w-[var(--layout-right-w)] shrink-0 flex-col", children: [
3510
+ /* @__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 }) }),
3511
+ /* @__PURE__ */ (0, import_jsx_runtime43.jsxs)("div", { className: "relative w-full shrink-0", children: [
3512
+ /* @__PURE__ */ (0, import_jsx_runtime43.jsx)(import_player12.PlayerVideoSurface, { className: "aspect-video w-full bg-black" }),
3513
+ playerTrackObject?.loadError && /* @__PURE__ */ (0, import_jsx_runtime43.jsx)("span", { className: "absolute bottom-2 left-2 right-2 text-xs text-red-400", children: playerTrackObject.loadError })
2730
3514
  ] }),
2731
- /* @__PURE__ */ (0, import_jsx_runtime36.jsxs)("div", { className: "relative w-full shrink-0", children: [
2732
- /* @__PURE__ */ (0, import_jsx_runtime36.jsx)(import_player11.PlayerVideoSurface, { className: "aspect-video w-full bg-black" }),
2733
- playerTrackObject?.loadError && /* @__PURE__ */ (0, import_jsx_runtime36.jsx)("span", { className: "absolute bottom-2 left-2 right-2 text-xs text-red-400", children: playerTrackObject.loadError })
2734
- ] })
3515
+ /* @__PURE__ */ (0, import_jsx_runtime43.jsx)(PlayerControls, {})
2735
3516
  ] }),
2736
- isTrackListSidebarVisible && /* @__PURE__ */ (0, import_jsx_runtime36.jsx)(
3517
+ /* @__PURE__ */ (0, import_jsx_runtime43.jsx)(
2737
3518
  TrackListSidebar,
2738
3519
  {
2739
3520
  layout: "inline",
3521
+ isCollapsed: !isTrackListSidebarVisible,
3522
+ onExpand: toggleTrackListSidebar,
3523
+ onMinimize: hideSongList,
2740
3524
  className: (0, import_tailwind_merge2.twMerge)(
2741
3525
  "min-h-0 min-w-0 flex-1",
2742
3526
  isExtended && "border-l border-zinc-800"
@@ -2756,11 +3540,11 @@ function PlayerPanel({
2756
3540
  var import_uuid = require("uuid");
2757
3541
  var import_ai = require("react-icons/ai");
2758
3542
  var import_genre_tree23 = require("@behindthemusictree/app-kit/genre-tree");
2759
- var import_jsx_runtime37 = require("react/jsx-runtime");
3543
+ var import_jsx_runtime44 = require("react/jsx-runtime");
2760
3544
  function Rating({ rating, handleChange }) {
2761
3545
  const instanceId = (0, import_uuid.v4)();
2762
- return /* @__PURE__ */ (0, import_jsx_runtime37.jsx)("div", { className: "flex items-center", children: [...Array(6)].map((_, i) => /* @__PURE__ */ (0, import_jsx_runtime37.jsxs)("div", { className: `relative ${i === 0 ? "mr-4" : "mr-1"}`, children: [
2763
- /* @__PURE__ */ (0, import_jsx_runtime37.jsx)(
3546
+ 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: [
3547
+ /* @__PURE__ */ (0, import_jsx_runtime44.jsx)(
2764
3548
  "input",
2765
3549
  {
2766
3550
  id: `star-${instanceId}-${i}`,
@@ -2772,16 +3556,16 @@ function Rating({ rating, handleChange }) {
2772
3556
  onChange: handleChange
2773
3557
  }
2774
3558
  ),
2775
- /* @__PURE__ */ (0, import_jsx_runtime37.jsx)("label", { htmlFor: `star-${instanceId}-${i}`, className: "cursor-pointer", children: i === 0 ? /* @__PURE__ */ (0, import_jsx_runtime37.jsxs)("div", { className: "flex", children: [
2776
- /* @__PURE__ */ (0, import_jsx_runtime37.jsx)(
3559
+ /* @__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: [
3560
+ /* @__PURE__ */ (0, import_jsx_runtime44.jsx)(
2777
3561
  import_ai.AiOutlineStar,
2778
3562
  {
2779
3563
  className: `w-7 h-7 absolute top-0 left-0 ${rating === import_genre_tree23.FORM_RATING_NULL_VALUE ? "text-[var(--color-accent-rating)]" : "text-[var(--color-text-muted)]"}`
2780
3564
  }
2781
3565
  ),
2782
- /* @__PURE__ */ (0, import_jsx_runtime37.jsx)("span", { className: "flex-grow w-8" }),
3566
+ /* @__PURE__ */ (0, import_jsx_runtime44.jsx)("span", { className: "flex-grow w-8" }),
2783
3567
  " "
2784
- ] }) : /* @__PURE__ */ (0, import_jsx_runtime37.jsx)(
3568
+ ] }) : /* @__PURE__ */ (0, import_jsx_runtime44.jsx)(
2785
3569
  import_ai.AiFillStar,
2786
3570
  {
2787
3571
  className: `w-7 h-7 absolute top-0 left-0 ${(rating ?? -1) >= i ? "text-[var(--color-accent-rating)]" : "text-[var(--color-text-muted)]"}`
@@ -2795,6 +3579,9 @@ function Rating({ rating, handleChange }) {
2795
3579
  AuthErrorPopup,
2796
3580
  AuthPopup,
2797
3581
  BasePopup,
3582
+ GenreDetailPanel,
3583
+ GenreInfoPanelBody,
3584
+ GenreNodeActionBand,
2798
3585
  GenrePlaylistTreePerRoot,
2799
3586
  GenreSearch,
2800
3587
  GenreTreeOutlineSkeleton,
@@ -2810,9 +3597,12 @@ function Rating({ rating, handleChange }) {
2810
3597
  PopupTitle,
2811
3598
  Rating,
2812
3599
  SpotifyAuthErrorPopup,
3600
+ Toast,
2813
3601
  TrackItem,
2814
3602
  TrackListSidebar,
2815
3603
  TrackPositionPlayPause,
2816
- TrackUploadPopup
3604
+ TrackUploadPopup,
3605
+ useGenrePlaylistPlayback,
3606
+ useLeftDetailPanelLayout
2817
3607
  });
2818
3608
  //# sourceMappingURL=index.cjs.map