@behindthemusictree/app-kit-ui 24.0.1 → 28.0.2

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