@behindthemusictree/app-kit-ui 26.1.0 → 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,42 +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");
578
+ var import_react13 = require("react");
474
579
  var import_react_query3 = require("@tanstack/react-query");
475
- var import_lucide_react12 = require("lucide-react");
476
- var import_ui17 = require("@behindthemusictree/ui");
580
+ var import_lucide_react15 = require("lucide-react");
581
+ var import_ui16 = require("@behindthemusictree/ui");
477
582
  var import_genre_tree_view4 = require("@behindthemusictree/genre-tree-view");
583
+ var import_genre_tree16 = require("@behindthemusictree/app-kit/genre-tree");
478
584
  var import_genre_tree17 = require("@behindthemusictree/app-kit/genre-tree");
479
- var import_genre_tree18 = require("@behindthemusictree/app-kit/genre-tree");
480
585
  var import_player10 = require("@behindthemusictree/app-kit/player");
481
- var import_popup5 = require("@behindthemusictree/app-kit/popup");
586
+ var import_popup6 = require("@behindthemusictree/app-kit/popup");
482
587
 
483
588
  // src/genre-tree/GenreDetailExtras.tsx
484
589
  var import_ui7 = require("@behindthemusictree/ui");
485
- var import_genre_tree8 = require("@behindthemusictree/app-kit/genre-tree");
590
+ var import_genre_tree7 = require("@behindthemusictree/app-kit/genre-tree");
486
591
 
487
592
  // src/genre-tree/ClampedText.tsx
488
- var import_react = require("react");
489
- var import_jsx_runtime11 = require("react/jsx-runtime");
490
- function ClampedText({ children, className = "" }) {
491
- const ref = (0, import_react.useRef)(null);
492
- const [expanded, setExpanded] = (0, import_react.useState)(false);
493
- const [overflows, setOverflows] = (0, import_react.useState)(false);
494
- (0, import_react.useLayoutEffect)(() => {
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)(() => {
495
600
  const element = ref.current;
496
601
  if (!element || expanded) return;
497
602
  setOverflows(element.scrollHeight > element.clientHeight);
498
603
  }, [children, expanded]);
499
- return /* @__PURE__ */ (0, import_jsx_runtime11.jsxs)(import_jsx_runtime11.Fragment, { children: [
500
- /* @__PURE__ */ (0, import_jsx_runtime11.jsx)("p", { ref, className: `${className} ${expanded ? "" : "line-clamp-3"}`, children }),
501
- (overflows || expanded) && /* @__PURE__ */ (0, import_jsx_runtime11.jsx)(
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)(
502
607
  "button",
503
608
  {
504
609
  type: "button",
@@ -512,10 +617,10 @@ function ClampedText({ children, className = "" }) {
512
617
  }
513
618
 
514
619
  // src/genre-tree/GenrePlaylistTracks.tsx
515
- var import_react2 = require("react");
620
+ var import_react4 = require("react");
516
621
  var import_react_query = require("@tanstack/react-query");
517
622
  var import_zod = require("zod");
518
- var import_fa5 = require("react-icons/fa");
623
+ var import_fa3 = require("react-icons/fa");
519
624
  var import_ui6 = require("@behindthemusictree/ui");
520
625
  var import_auth2 = require("@behindthemusictree/app-kit/auth");
521
626
  var import_transport2 = require("@behindthemusictree/app-kit/transport");
@@ -524,33 +629,22 @@ var import_transport4 = require("@behindthemusictree/app-kit/transport");
524
629
  var import_player = require("@behindthemusictree/app-kit/player");
525
630
  var import_player2 = require("@behindthemusictree/app-kit/player");
526
631
  var import_popup2 = require("@behindthemusictree/app-kit/popup");
527
-
528
- // src/genre-tree/UnplayableReason.tsx
529
- var import_fa3 = require("react-icons/fa");
530
- var import_genre_tree = require("@behindthemusictree/app-kit/genre-tree");
531
- var import_jsx_runtime12 = require("react/jsx-runtime");
532
- function UnplayableReason({ reason }) {
533
- if (reason !== import_genre_tree.NO_VIDEO_LABEL) return /* @__PURE__ */ (0, import_jsx_runtime12.jsx)(import_jsx_runtime12.Fragment, { children: reason });
534
- return /* @__PURE__ */ (0, import_jsx_runtime12.jsxs)("span", { role: "img", "aria-label": reason, className: "relative inline-flex shrink-0 items-center", children: [
535
- /* @__PURE__ */ (0, import_jsx_runtime12.jsx)(import_fa3.FaYoutube, { size: 16, "aria-hidden": "true" }),
536
- /* @__PURE__ */ (0, import_jsx_runtime12.jsx)("span", { "aria-hidden": "true", className: "absolute left-1/2 top-1/2 h-0.5 w-[130%] -translate-x-1/2 -translate-y-1/2 -rotate-45 rounded bg-current" })
537
- ] });
538
- }
539
-
540
- // src/genre-tree/GenrePlaylistTracks.tsx
541
632
  var import_transport5 = require("@behindthemusictree/app-kit/transport");
633
+ var import_genre_tree2 = require("@behindthemusictree/app-kit/genre-tree");
542
634
  var import_genre_tree3 = require("@behindthemusictree/app-kit/genre-tree");
543
635
  var import_genre_tree4 = require("@behindthemusictree/app-kit/genre-tree");
544
636
  var import_genre_tree5 = require("@behindthemusictree/app-kit/genre-tree");
545
- var import_genre_tree6 = require("@behindthemusictree/app-kit/genre-tree");
546
637
 
547
638
  // src/genre-tree/MusicBrainzRecordingLink.tsx
548
- var import_fa4 = require("react-icons/fa");
549
- var import_genre_tree2 = require("@behindthemusictree/app-kit/genre-tree");
639
+ var import_genre_tree = require("@behindthemusictree/app-kit/genre-tree");
550
640
  var import_jsx_runtime13 = require("react/jsx-runtime");
551
- function MusicBrainzRecordingLink({ song }) {
552
- const url = (0, import_genre_tree2.musicBrainzRecordingUrl)(song);
553
- if (!url) return null;
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 });
554
648
  return /* @__PURE__ */ (0, import_jsx_runtime13.jsx)(
555
649
  "a",
556
650
  {
@@ -558,20 +652,19 @@ function MusicBrainzRecordingLink({ song }) {
558
652
  target: "_blank",
559
653
  rel: "noopener noreferrer",
560
654
  "aria-label": `View ${song.title} on MusicBrainz`,
561
- title: "View on MusicBrainz",
562
- className: "inline-flex shrink-0 items-center opacity-50 hover:opacity-100 focus-visible:opacity-100",
655
+ className: className2,
563
656
  onClick: (event) => event.stopPropagation(),
564
- children: /* @__PURE__ */ (0, import_jsx_runtime13.jsx)(import_fa4.FaExternalLinkAlt, { size: 10 })
657
+ children
565
658
  }
566
659
  );
567
660
  }
568
661
 
569
662
  // src/genre-tree/GenrePlaylistTracks.tsx
570
- var import_genre_tree7 = require("@behindthemusictree/app-kit/genre-tree");
663
+ var import_genre_tree6 = require("@behindthemusictree/app-kit/genre-tree");
571
664
  var import_track_noun2 = require("@behindthemusictree/app-kit/track-noun");
572
665
  var import_jsx_runtime14 = require("react/jsx-runtime");
573
666
  var CATALOGUE_PAGE_SIZE = 100;
574
- var SongPageSchema = (0, import_transport3.PaginatedResponseSchema)(import_genre_tree5.SongDetailedSchema).extend({
667
+ var SongPageSchema = (0, import_transport3.PaginatedResponseSchema)(import_genre_tree4.SongDetailedSchema).extend({
575
668
  overallTotal: import_zod.z.number().nullable(),
576
669
  page: import_zod.z.number().nullable(),
577
670
  totalPages: import_zod.z.number().nullable()
@@ -597,7 +690,7 @@ function GenrePlaylistTracks({
597
690
  renderTracksToolbar,
598
691
  artist = null
599
692
  }) {
600
- const { fetchGenrePlaylistTracksPage, playNewTrackListFromGenrePlaylist, playNewTrackListFromArtist } = (0, import_genre_tree6.useTrackList)();
693
+ const { fetchGenrePlaylistTracksPage, playNewTrackListFromGenrePlaylist, playNewTrackListFromArtist } = (0, import_genre_tree5.useTrackList)();
601
694
  const { session, sessionRestored } = (0, import_auth2.useSession)();
602
695
  const { playerTrackObject, playState, handlePlayPauseAction } = (0, import_player.usePlayer)();
603
696
  const { showPopup } = (0, import_popup2.usePopup)();
@@ -605,11 +698,11 @@ function GenrePlaylistTracks({
605
698
  const { uuid } = genrePlaylist;
606
699
  const { fetch } = (0, import_transport2.useFetchWrapper)(getBackendBaseUrl);
607
700
  const { data, isPending, isError, refetch, hasNextPage, isFetchingNextPage, fetchNextPage } = (0, import_react_query.useInfiniteQuery)({
608
- queryKey: scope === "me" ? import_genre_tree3.genrePlaylistQueryKeys.me.tracks(uuid) : artist ? [...import_genre_tree4.libraryQueryKeys.reference.songs.byGenre(getBackendBaseUrl(), genreUuid), "artist", artist.uuid] : import_genre_tree4.libraryQueryKeys.reference.songs.byGenre(getBackendBaseUrl(), genreUuid),
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),
609
702
  queryFn: async ({ pageParam }) => {
610
703
  if (scope === "me") return fetchGenrePlaylistTracksPage(uuid, scope, Number(pageParam));
611
704
  const response = await fetch(
612
- import_genre_tree4.libraryEndpoints.reference.songs.list(),
705
+ import_genre_tree3.libraryEndpoints.reference.songs.list(),
613
706
  true,
614
707
  false,
615
708
  {},
@@ -622,8 +715,8 @@ function GenrePlaylistTracks({
622
715
  getNextPageParam: (lastPage) => lastPage.nextPage ?? void 0,
623
716
  enabled: scope !== "me" || sessionRestored && !!session?.accessToken
624
717
  });
625
- const sentinelRef = (0, import_react2.useRef)(null);
626
- (0, import_react2.useEffect)(() => {
718
+ const sentinelRef = (0, import_react4.useRef)(null);
719
+ (0, import_react4.useEffect)(() => {
627
720
  const sentinel = sentinelRef.current;
628
721
  if (!hasNextPage || isFetchingNextPage || !sentinel) return;
629
722
  const observer = new IntersectionObserver((entries) => {
@@ -662,16 +755,16 @@ function GenrePlaylistTracks({
662
755
  const isCurrent = playerTrackObject?.track.id === track.uuid;
663
756
  const isPlaying = isCurrent && playState === import_player2.PlayStates.PLAYING;
664
757
  const text = /* @__PURE__ */ (0, import_jsx_runtime14.jsxs)("span", { className: TEXT_CELL, children: [
665
- /* @__PURE__ */ (0, import_jsx_runtime14.jsx)("span", { className: TITLE_CELL, children: track.title }),
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 }),
666
759
  track.artists?.length ? /* @__PURE__ */ (0, import_jsx_runtime14.jsx)("span", { className: SEPARATOR, children: " \u2014 " }) : null,
667
- /* @__PURE__ */ (0, import_jsx_runtime14.jsx)("span", { className: COLUMN_CELL, children: track.artists?.length ? /* @__PURE__ */ (0, import_jsx_runtime14.jsx)(import_genre_tree7.ArtistNames, { artists: track.artists, renderArtistLink }) : null }),
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 }),
668
761
  track.album && /* @__PURE__ */ (0, import_jsx_runtime14.jsx)("span", { className: ALBUM_CELL, children: track.album.name })
669
762
  ] });
670
763
  const leading = renderSongLeading?.(track, index);
671
764
  const actions = renderSongActions?.(track);
672
- const unplayableReason = (0, import_genre_tree5.isSong)(track) ? (0, import_genre_tree5.songUnplayableReason)(track) : null;
673
- if (unplayableReason) {
674
- return /* @__PURE__ */ (0, import_jsx_runtime14.jsxs)("li", { className: "flex min-h-8 items-center gap-1 px-1 opacity-50", title: unplayableReason, children: [
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: [
675
768
  leading,
676
769
  /* @__PURE__ */ (0, import_jsx_runtime14.jsx)(
677
770
  "button",
@@ -679,14 +772,12 @@ function GenrePlaylistTracks({
679
772
  type: "button",
680
773
  disabled: true,
681
774
  "aria-disabled": "true",
682
- "aria-label": `${label} \u2014 ${unplayableReason}`,
775
+ "aria-label": label,
683
776
  className: "flex h-6 w-6 shrink-0 items-center justify-center rounded-full opacity-0 cursor-not-allowed",
684
- children: /* @__PURE__ */ (0, import_jsx_runtime14.jsx)(import_fa5.FaPlay, { size: 10 })
777
+ children: /* @__PURE__ */ (0, import_jsx_runtime14.jsx)(import_fa3.FaPlay, { size: 10 })
685
778
  }
686
779
  ),
687
780
  text,
688
- (0, import_genre_tree5.isSong)(track) && /* @__PURE__ */ (0, import_jsx_runtime14.jsx)(MusicBrainzRecordingLink, { song: track }),
689
- /* @__PURE__ */ (0, import_jsx_runtime14.jsx)("span", { className: "shrink-0 text-xs", children: /* @__PURE__ */ (0, import_jsx_runtime14.jsx)(UnplayableReason, { reason: unplayableReason }) }),
690
781
  actions
691
782
  ] }, track.uuid);
692
783
  }
@@ -703,11 +794,10 @@ function GenrePlaylistTracks({
703
794
  "aria-label": `${isPlaying ? "Pause" : "Play"} ${label}`,
704
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"}`,
705
796
  onClick: () => isCurrent ? handlePlayPauseAction() : play(track.uuid),
706
- children: isCurrent && playState === import_player2.PlayStates.LOADING ? /* @__PURE__ */ (0, import_jsx_runtime14.jsx)(import_ui6.RingLoader, { size: 10 }) : isPlaying ? /* @__PURE__ */ (0, import_jsx_runtime14.jsx)(import_fa5.FaPause, { size: 10 }) : /* @__PURE__ */ (0, import_jsx_runtime14.jsx)(import_fa5.FaPlay, { size: 10 })
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 })
707
798
  }
708
799
  ),
709
800
  text,
710
- (0, import_genre_tree5.isSong)(track) && /* @__PURE__ */ (0, import_jsx_runtime14.jsx)(MusicBrainzRecordingLink, { song: track }),
711
801
  actions
712
802
  ]
713
803
  },
@@ -721,10 +811,12 @@ function GenrePlaylistTracks({
721
811
 
722
812
  // src/genre-tree/GenreDetailExtras.tsx
723
813
  var import_track_noun3 = require("@behindthemusictree/app-kit/track-noun");
814
+ var import_si = require("react-icons/si");
724
815
  var import_jsx_runtime15 = require("react/jsx-runtime");
725
816
  function GenreDetailExtras({
726
817
  genreUuid,
727
818
  genrePlaylist,
819
+ wikidataId,
728
820
  scope,
729
821
  getBackendBaseUrl,
730
822
  criteriaOverviewSchema,
@@ -736,7 +828,7 @@ function GenreDetailExtras({
736
828
  ...trackSlots
737
829
  }) {
738
830
  const trackNoun = (0, import_track_noun3.useTrackNoun)();
739
- const { data: overview, isPending } = (0, import_genre_tree8.useFetchGenreOverview)(
831
+ const { data: overview, isPending } = (0, import_genre_tree7.useFetchGenreOverview)(
740
832
  genreUuid,
741
833
  scope,
742
834
  getBackendBaseUrl,
@@ -752,33 +844,44 @@ function GenreDetailExtras({
752
844
  ] });
753
845
  }
754
846
  if (!overview) return /* @__PURE__ */ (0, import_jsx_runtime15.jsx)(import_jsx_runtime15.Fragment, { children: family });
755
- const { summary, description, summarySourceUrl, essentialSongs } = overview;
847
+ const { summary, summarySourceUrl, essentialSongs } = overview;
848
+ const wikidataUrl = wikidataId ? `https://www.wikidata.org/wiki/${encodeURIComponent(wikidataId)}` : null;
756
849
  return /* @__PURE__ */ (0, import_jsx_runtime15.jsxs)(import_jsx_runtime15.Fragment, { children: [
757
- description && /* @__PURE__ */ (0, import_jsx_runtime15.jsx)("div", { className: "px-3.5 py-2.5", children: /* @__PURE__ */ (0, import_jsx_runtime15.jsx)(ClampedText, { className: "text-[0.8125rem] leading-[1.45] text-[var(--color-text-muted)]", children: description }, genreUuid) }),
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
+ ] }),
758
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: [
759
882
  /* @__PURE__ */ (0, import_jsx_runtime15.jsx)("span", { className: "gtv-info-panel-children-title", children: "Summary" }),
760
- /* @__PURE__ */ (0, import_jsx_runtime15.jsx)(ClampedText, { className: "mt-1", children: summary ?? "\u2014" }, genreUuid),
761
- summarySourceUrl && /* @__PURE__ */ (0, import_jsx_runtime15.jsx)(
762
- "a",
763
- {
764
- href: summarySourceUrl,
765
- target: "_blank",
766
- rel: "noopener noreferrer",
767
- className: "mt-1 inline-block text-xs text-[var(--color-text-muted)] underline hover:text-[var(--color-text)]",
768
- children: "Source: Wikipedia"
769
- }
770
- )
883
+ /* @__PURE__ */ (0, import_jsx_runtime15.jsx)(ClampedText, { className: "mt-1", children: summary ?? "\u2014" }, genreUuid)
771
884
  ] }),
772
- renderSummary && summarySourceUrl && /* @__PURE__ */ (0, import_jsx_runtime15.jsx)(
773
- "a",
774
- {
775
- href: summarySourceUrl,
776
- target: "_blank",
777
- rel: "noopener noreferrer",
778
- className: "mx-3.5 mb-2.5 inline-block text-xs text-[var(--color-text-muted)] underline hover:text-[var(--color-text)]",
779
- children: "Source: Wikipedia"
780
- }
781
- ),
782
885
  family,
783
886
  renderGenreDetailExtras?.(overview),
784
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: [
@@ -803,30 +906,30 @@ function GenreDetailExtras({
803
906
  }
804
907
 
805
908
  // src/genre-tree/playlist-tree/TreePerRoot.tsx
806
- var import_react4 = require("react");
909
+ var import_react6 = require("react");
807
910
  var import_genre_tree_view = require("@behindthemusictree/genre-tree-view");
808
911
  var import_player3 = require("@behindthemusictree/app-kit/player");
809
912
  var import_popup3 = require("@behindthemusictree/app-kit/popup");
810
913
  var import_transport6 = require("@behindthemusictree/app-kit/transport");
811
914
 
812
915
  // src/genre-tree/playlist-tree/useGenrePlaylistPlayback.ts
813
- var import_react3 = require("react");
814
- 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");
815
918
  function useGenrePlaylistPlayback({
816
919
  scope,
817
920
  genrePlaylists,
818
921
  artist = null
819
922
  }) {
820
- const { trackList, playNewTrackListFromGenrePlaylist, playNewTrackListFromArtist } = (0, import_genre_tree9.useTrackList)();
923
+ const { trackList, playNewTrackListFromGenrePlaylist, playNewTrackListFromArtist } = (0, import_genre_tree8.useTrackList)();
821
924
  const origin = trackList?.origin;
822
- const playingNodeId = (0, import_react3.useMemo)(() => {
925
+ const playingNodeId = (0, import_react5.useMemo)(() => {
823
926
  if (!origin) return null;
824
- if (!artist) return origin.type === import_genre_tree9.TrackListOriginType.GENRE_PLAYLIST ? origin.uuid : null;
825
- if (origin.type !== import_genre_tree9.TrackListOriginType.ARTIST || origin.uuid !== artist.uuid) return null;
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;
826
929
  const { genreUuid } = origin;
827
930
  return genreUuid ? genrePlaylists.find((gp) => gp.criteria?.uuid === genreUuid)?.uuid ?? null : null;
828
931
  }, [origin, artist, genrePlaylists]);
829
- const play = (0, import_react3.useCallback)(
932
+ const play = (0, import_react5.useCallback)(
830
933
  async (genrePlaylist) => {
831
934
  if (!artist) return playNewTrackListFromGenrePlaylist(genrePlaylist, scope);
832
935
  if (!genrePlaylist.criteria) return;
@@ -834,7 +937,7 @@ function useGenrePlaylistPlayback({
834
937
  },
835
938
  [artist, scope, playNewTrackListFromGenrePlaylist, playNewTrackListFromArtist]
836
939
  );
837
- return (0, import_react3.useMemo)(() => ({ playingNodeId, play }), [playingNodeId, play]);
940
+ return (0, import_react5.useMemo)(() => ({ playingNodeId, play }), [playingNodeId, play]);
838
941
  }
839
942
 
840
943
  // src/genre-tree/playlist-tree/TreePerRoot.tsx
@@ -842,7 +945,7 @@ var import_track_noun4 = require("@behindthemusictree/app-kit/track-noun");
842
945
  var import_jsx_runtime16 = require("react/jsx-runtime");
843
946
  function GenrePlaylistTreePerRoot({
844
947
  scope,
845
- className,
948
+ className: className2,
846
949
  rootUuid,
847
950
  genrePlaylistTreePerRoot,
848
951
  handleGenreCreationAction,
@@ -869,7 +972,7 @@ function GenrePlaylistTreePerRoot({
869
972
  const { playingNodeId, play } = playback ?? defaultPlayback;
870
973
  const trackNoun = (0, import_track_noun4.useTrackNoun)();
871
974
  const { showPopup } = (0, import_popup3.usePopup)();
872
- const nodes = (0, import_react4.useMemo)(
975
+ const nodes = (0, import_react6.useMemo)(
873
976
  () => genrePlaylistTreePerRoot.map((genrePlaylist) => ({
874
977
  id: genrePlaylist.uuid,
875
978
  parentId: genrePlaylist.parent?.uuid ?? null,
@@ -882,7 +985,7 @@ function GenrePlaylistTreePerRoot({
882
985
  })),
883
986
  [genrePlaylistTreePerRoot]
884
987
  );
885
- const handlePlayPause = (0, import_react4.useCallback)(
988
+ const handlePlayPause = (0, import_react6.useCallback)(
886
989
  (nodeId) => {
887
990
  const genrePlaylist = genrePlaylistTreePerRoot.find((g) => g.uuid === nodeId);
888
991
  if (!genrePlaylist) return;
@@ -907,7 +1010,7 @@ function GenrePlaylistTreePerRoot({
907
1010
  showPopup
908
1011
  ]
909
1012
  );
910
- const handleAddChild = (0, import_react4.useCallback)(
1013
+ const handleAddChild = (0, import_react6.useCallback)(
911
1014
  (parentId) => {
912
1015
  const genrePlaylist = genrePlaylistTreePerRoot.find((g) => g.uuid === parentId);
913
1016
  if (!genrePlaylist?.criteria) return;
@@ -915,7 +1018,7 @@ function GenrePlaylistTreePerRoot({
915
1018
  },
916
1019
  [genrePlaylistTreePerRoot, handleGenreCreationAction]
917
1020
  );
918
- const handleRenameRequest = (0, import_react4.useCallback)(
1021
+ const handleRenameRequest = (0, import_react6.useCallback)(
919
1022
  (node) => {
920
1023
  const genrePlaylist = genrePlaylistTreePerRoot.find((g) => g.uuid === node.id);
921
1024
  if (!genrePlaylist?.criteria) return;
@@ -923,14 +1026,14 @@ function GenrePlaylistTreePerRoot({
923
1026
  },
924
1027
  [genrePlaylistTreePerRoot, handleGenreRenameAction]
925
1028
  );
926
- const handleDeleteRequest = (0, import_react4.useCallback)((node) => {
1029
+ const handleDeleteRequest = (0, import_react6.useCallback)((node) => {
927
1030
  if (confirm(`Are you sure you want to delete "${node.name}"?`)) {
928
1031
  }
929
1032
  }, []);
930
1033
  return /* @__PURE__ */ (0, import_jsx_runtime16.jsx)(
931
1034
  import_genre_tree_view.GenreTree,
932
1035
  {
933
- className,
1036
+ className: className2,
934
1037
  nodes,
935
1038
  itemCountLabel: `${(0, import_track_noun4.capitalize)(trackNoun.singular)} count`,
936
1039
  rootColor: (0, import_genre_tree_view.getGenreTreeColor)(rootUuid),
@@ -960,21 +1063,28 @@ function GenrePlaylistTreePerRoot({
960
1063
  }
961
1064
 
962
1065
  // src/genre-tree/playlist-tree/TreeWheel.tsx
963
- var import_react5 = require("react");
1066
+ var import_react7 = require("react");
964
1067
  var import_genre_tree_view2 = require("@behindthemusictree/genre-tree-view");
965
1068
  var import_player4 = require("@behindthemusictree/app-kit/player");
966
1069
  var import_popup4 = require("@behindthemusictree/app-kit/popup");
967
1070
  var import_transport7 = require("@behindthemusictree/app-kit/transport");
968
1071
 
969
1072
  // src/genre-tree/playlist-tree/size-metric.ts
970
- var genrePlaylistSize = (genrePlaylist, metric) => metric === "views" ? genrePlaylist.viewsCount : genrePlaylist.tracksCount;
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
+ };
971
1081
 
972
1082
  // src/genre-tree/playlist-tree/TreeWheel.tsx
973
1083
  var import_track_noun5 = require("@behindthemusictree/app-kit/track-noun");
974
1084
  var import_jsx_runtime17 = require("react/jsx-runtime");
975
1085
  function GenrePlaylistTreeWheel({
976
1086
  scope,
977
- className,
1087
+ className: className2,
978
1088
  genrePlaylists,
979
1089
  handleGenreCreationAction,
980
1090
  handleGenreRenameAction,
@@ -1003,7 +1113,7 @@ function GenrePlaylistTreeWheel({
1003
1113
  const { playingNodeId, play } = playback ?? defaultPlayback;
1004
1114
  const trackNoun = (0, import_track_noun5.useTrackNoun)();
1005
1115
  const { showPopup } = (0, import_popup4.usePopup)();
1006
- const nodes = (0, import_react5.useMemo)(
1116
+ const nodes = (0, import_react7.useMemo)(
1007
1117
  () => genrePlaylists.map((genrePlaylist) => ({
1008
1118
  id: genrePlaylist.uuid,
1009
1119
  parentId: genrePlaylist.parent?.uuid ?? null,
@@ -1015,7 +1125,7 @@ function GenrePlaylistTreeWheel({
1015
1125
  })),
1016
1126
  [genrePlaylists, sizeMetric]
1017
1127
  );
1018
- const handlePlayPause = (0, import_react5.useCallback)(
1128
+ const handlePlayPause = (0, import_react7.useCallback)(
1019
1129
  (nodeId) => {
1020
1130
  const genrePlaylist = genrePlaylists.find((g) => g.uuid === nodeId);
1021
1131
  if (!genrePlaylist) return;
@@ -1040,7 +1150,7 @@ function GenrePlaylistTreeWheel({
1040
1150
  showPopup
1041
1151
  ]
1042
1152
  );
1043
- const handleAddChild = (0, import_react5.useCallback)(
1153
+ const handleAddChild = (0, import_react7.useCallback)(
1044
1154
  (parentId) => {
1045
1155
  const genrePlaylist = genrePlaylists.find((g) => g.uuid === parentId);
1046
1156
  if (!genrePlaylist?.criteria) return;
@@ -1048,7 +1158,7 @@ function GenrePlaylistTreeWheel({
1048
1158
  },
1049
1159
  [genrePlaylists, handleGenreCreationAction]
1050
1160
  );
1051
- const handleRenameRequest = (0, import_react5.useCallback)(
1161
+ const handleRenameRequest = (0, import_react7.useCallback)(
1052
1162
  (node) => {
1053
1163
  const genrePlaylist = genrePlaylists.find((g) => g.uuid === node.id);
1054
1164
  if (!genrePlaylist?.criteria) return;
@@ -1056,14 +1166,14 @@ function GenrePlaylistTreeWheel({
1056
1166
  },
1057
1167
  [genrePlaylists, handleGenreRenameAction]
1058
1168
  );
1059
- const handleDeleteRequest = (0, import_react5.useCallback)((node) => {
1169
+ const handleDeleteRequest = (0, import_react7.useCallback)((node) => {
1060
1170
  if (confirm(`Are you sure you want to delete "${node.name}"?`)) {
1061
1171
  }
1062
1172
  }, []);
1063
1173
  return /* @__PURE__ */ (0, import_jsx_runtime17.jsx)(
1064
1174
  import_genre_tree_view2.GenreTreeWheel,
1065
1175
  {
1066
- className,
1176
+ className: className2,
1067
1177
  nodes,
1068
1178
  itemCountLabel: sizeMetric === "views" ? "Views" : `${(0, import_track_noun5.capitalize)(trackNoun.singular)} count`,
1069
1179
  playingNodeId,
@@ -1094,7 +1204,7 @@ function GenrePlaylistTreeWheel({
1094
1204
  }
1095
1205
 
1096
1206
  // src/genre-tree/playlist-tree/TreeWheelRadialPopCore.tsx
1097
- var import_react6 = require("react");
1207
+ var import_react8 = require("react");
1098
1208
  var import_genre_tree_view3 = require("@behindthemusictree/genre-tree-view");
1099
1209
  var import_player5 = require("@behindthemusictree/app-kit/player");
1100
1210
  var import_track_noun6 = require("@behindthemusictree/app-kit/track-noun");
@@ -1102,7 +1212,7 @@ var import_jsx_runtime18 = require("react/jsx-runtime");
1102
1212
  function GenrePlaylistTreeWheelRadialPopCore(props) {
1103
1213
  const {
1104
1214
  scope,
1105
- className,
1215
+ className: className2,
1106
1216
  genrePlaylists,
1107
1217
  handleGenreCreationAction,
1108
1218
  handleGenreRenameAction,
@@ -1139,7 +1249,7 @@ function GenrePlaylistTreeWheelRadialPopCore(props) {
1139
1249
  const defaultPlayback = useGenrePlaylistPlayback({ scope, genrePlaylists });
1140
1250
  const { playingNodeId, play } = playback ?? defaultPlayback;
1141
1251
  const trackNoun = (0, import_track_noun6.useTrackNoun)();
1142
- const nodes = (0, import_react6.useMemo)(
1252
+ const nodes = (0, import_react8.useMemo)(
1143
1253
  () => genrePlaylists.map((genrePlaylist) => ({
1144
1254
  id: genrePlaylist.uuid,
1145
1255
  parentId: genrePlaylist.parent?.uuid ?? null,
@@ -1154,7 +1264,7 @@ function GenrePlaylistTreeWheelRadialPopCore(props) {
1154
1264
  })),
1155
1265
  [genrePlaylists, sizeMetric]
1156
1266
  );
1157
- const handlePlayPause = (0, import_react6.useCallback)(
1267
+ const handlePlayPause = (0, import_react8.useCallback)(
1158
1268
  (nodeId) => {
1159
1269
  const genrePlaylist = genrePlaylists.find((g) => g.uuid === nodeId);
1160
1270
  if (!genrePlaylist) return;
@@ -1171,7 +1281,7 @@ function GenrePlaylistTreeWheelRadialPopCore(props) {
1171
1281
  },
1172
1282
  [genrePlaylists, playingNodeId, isPlaying, setIsPlaying, play]
1173
1283
  );
1174
- const handleAddChild = (0, import_react6.useCallback)(
1284
+ const handleAddChild = (0, import_react8.useCallback)(
1175
1285
  (parentId) => {
1176
1286
  const genrePlaylist = genrePlaylists.find((g) => g.uuid === parentId);
1177
1287
  if (!genrePlaylist?.criteria) return;
@@ -1179,7 +1289,7 @@ function GenrePlaylistTreeWheelRadialPopCore(props) {
1179
1289
  },
1180
1290
  [genrePlaylists, handleGenreCreationAction]
1181
1291
  );
1182
- const handleRenameRequest = (0, import_react6.useCallback)(
1292
+ const handleRenameRequest = (0, import_react8.useCallback)(
1183
1293
  (node) => {
1184
1294
  const genrePlaylist = genrePlaylists.find((g) => g.uuid === node.id);
1185
1295
  if (!genrePlaylist?.criteria) return;
@@ -1187,12 +1297,12 @@ function GenrePlaylistTreeWheelRadialPopCore(props) {
1187
1297
  },
1188
1298
  [genrePlaylists, handleGenreRenameAction]
1189
1299
  );
1190
- const handleDeleteRequest = (0, import_react6.useCallback)((node) => {
1300
+ const handleDeleteRequest = (0, import_react8.useCallback)((node) => {
1191
1301
  if (confirm(`Are you sure you want to delete "${node.name}"?`)) {
1192
1302
  }
1193
1303
  }, []);
1194
1304
  const treeProps = {
1195
- className,
1305
+ className: className2,
1196
1306
  nodes,
1197
1307
  itemCountLabel: sizeMetric === "views" ? "Views" : `${(0, import_track_noun6.capitalize)(trackNoun.singular)} count`,
1198
1308
  playingNodeId,
@@ -1229,13 +1339,13 @@ function GenrePlaylistTreeWheelRadialPopCore(props) {
1229
1339
  }
1230
1340
 
1231
1341
  // src/genre-tree/GenreTreeWheelHandoff.tsx
1232
- var import_react7 = require("react");
1342
+ var import_react9 = require("react");
1233
1343
  var import_jsx_runtime19 = require("react/jsx-runtime");
1234
1344
  function GenreTreeWheelHandoff({ skeleton, children, onRevealedChange }) {
1235
- const [revealed, setRevealed] = (0, import_react7.useState)(false);
1236
- const onRevealedChangeRef = (0, import_react7.useRef)(onRevealedChange);
1345
+ const [revealed, setRevealed] = (0, import_react9.useState)(false);
1346
+ const onRevealedChangeRef = (0, import_react9.useRef)(onRevealedChange);
1237
1347
  onRevealedChangeRef.current = onRevealedChange;
1238
- (0, import_react7.useEffect)(() => {
1348
+ (0, import_react9.useEffect)(() => {
1239
1349
  let innerFrame = 0;
1240
1350
  const outerFrame = requestAnimationFrame(() => {
1241
1351
  innerFrame = requestAnimationFrame(() => {
@@ -1266,7 +1376,7 @@ function GenreTreeWheelHandoff({ skeleton, children, onRevealedChange }) {
1266
1376
  // src/genre-tree/GenreSearch.tsx
1267
1377
  var import_lucide_react7 = require("lucide-react");
1268
1378
  var import_ui8 = require("@behindthemusictree/ui");
1269
- var import_genre_tree10 = require("@behindthemusictree/app-kit/genre-tree");
1379
+ var import_genre_tree9 = require("@behindthemusictree/app-kit/genre-tree");
1270
1380
  var import_jsx_runtime20 = require("react/jsx-runtime");
1271
1381
  function GenreSearch({
1272
1382
  genrePlaylists,
@@ -1278,7 +1388,7 @@ function GenreSearch({
1278
1388
  onQueryChange,
1279
1389
  extraResults
1280
1390
  }) {
1281
- 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)({
1282
1392
  genrePlaylists,
1283
1393
  onSelect,
1284
1394
  selectedName,
@@ -1289,14 +1399,14 @@ function GenreSearch({
1289
1399
  const loading = query !== "" && query !== selectedName && (extraResults?.loading ?? false);
1290
1400
  const showList = results.length > 0 || extraItems.length > 0;
1291
1401
  const heading = "px-3 pt-2 pb-1 text-xs font-medium uppercase text-[var(--color-text-muted)]";
1292
- return /* @__PURE__ */ (0, import_jsx_runtime20.jsxs)("div", { className: "genre-search relative w-[calc(var(--layout-detail-panel-w,25.5rem)-2*var(--layout-panel-inset,1rem))] shrink-0", children: [
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: [
1293
1403
  onPrevious && /* @__PURE__ */ (0, import_jsx_runtime20.jsx)(
1294
1404
  "button",
1295
1405
  {
1296
1406
  type: "button",
1297
1407
  "aria-label": "Previous",
1298
1408
  title: "Previous",
1299
- className: "genre-search-previous absolute left-2 top-1/2 z-10 hidden p-1.5 text-[var(--color-text-muted)] rounded-full -translate-y-1/2 hover:bg-[var(--color-surface-muted)] hover:text-[var(--color-text)]",
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)]",
1300
1410
  onClick: onPrevious,
1301
1411
  children: /* @__PURE__ */ (0, import_jsx_runtime20.jsx)(import_lucide_react7.ArrowLeft, { "aria-hidden": "true", className: "w-4 h-4" })
1302
1412
  }
@@ -1313,6 +1423,9 @@ function GenreSearch({
1313
1423
  placeholder: placeholder ?? `${label}\u2026`,
1314
1424
  "aria-label": label,
1315
1425
  "aria-busy": loading,
1426
+ inputMode: "search",
1427
+ enterKeyHint: "search",
1428
+ autoComplete: "off",
1316
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"
1317
1430
  }
1318
1431
  ),
@@ -1345,7 +1458,7 @@ function GenreSearch({
1345
1458
  {
1346
1459
  type: "button",
1347
1460
  "aria-label": "Clear search",
1348
- className: "p-2 text-[var(--color-text-muted)] rounded-full hover:bg-[var(--color-surface-muted)] hover:text-[var(--color-text)]",
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)]",
1349
1462
  onClick: () => {
1350
1463
  clear();
1351
1464
  onQueryChange?.("");
@@ -1355,13 +1468,13 @@ function GenreSearch({
1355
1468
  )
1356
1469
  ] }),
1357
1470
  extraResults?.status && /* @__PURE__ */ (0, import_jsx_runtime20.jsx)("p", { role: "status", "aria-live": "polite", className: "sr-only", children: extraResults.status }),
1358
- showList && /* @__PURE__ */ (0, import_jsx_runtime20.jsxs)("ul", { className: "genre-search-results absolute z-40 w-full max-h-64 mt-1 overflow-y-auto bg-[var(--color-surface)] border border-[var(--color-border)] rounded-xl shadow-md", children: [
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: [
1359
1472
  extraResults && results.length > 0 && /* @__PURE__ */ (0, import_jsx_runtime20.jsx)("li", { className: heading, children: "Genres" }),
1360
1473
  results.map((genrePlaylist) => /* @__PURE__ */ (0, import_jsx_runtime20.jsx)("li", { children: /* @__PURE__ */ (0, import_jsx_runtime20.jsxs)(
1361
1474
  "button",
1362
1475
  {
1363
1476
  type: "button",
1364
- 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)]",
1365
1478
  onClick: () => {
1366
1479
  select(genrePlaylist);
1367
1480
  onQueryChange?.("");
@@ -1377,7 +1490,7 @@ function GenreSearch({
1377
1490
  "button",
1378
1491
  {
1379
1492
  type: "button",
1380
- 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)]",
1381
1494
  onClick: () => {
1382
1495
  setQuery("");
1383
1496
  onQueryChange?.("");
@@ -1391,8 +1504,227 @@ function GenreSearch({
1391
1504
  }
1392
1505
 
1393
1506
  // src/genre-tree/GenreActionBand.tsx
1394
- 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
1395
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");
1396
1728
  function GenreActionBand({
1397
1729
  node,
1398
1730
  onPlayPause,
@@ -1401,7 +1733,12 @@ function GenreActionBand({
1401
1733
  onAddChild,
1402
1734
  onRename,
1403
1735
  onChangeParent,
1404
- additionalActions
1736
+ side,
1737
+ onToggleSide,
1738
+ toggleSideDisabledReason,
1739
+ additionalActions,
1740
+ shareTarget,
1741
+ shareUrl
1405
1742
  }) {
1406
1743
  const items = [];
1407
1744
  const adminItems = [];
@@ -1409,7 +1746,7 @@ function GenreActionBand({
1409
1746
  items.push({
1410
1747
  key: "play-pause",
1411
1748
  label: isPlaying ? "Pause" : "Play",
1412
- icon: isPlaying ? /* @__PURE__ */ (0, import_jsx_runtime21.jsx)(import_lucide_react8.Pause, { "aria-hidden": "true", className: "w-5 h-5" }) : /* @__PURE__ */ (0, import_jsx_runtime21.jsx)(import_lucide_react8.Play, { "aria-hidden": "true", className: "w-5 h-5" }),
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" }),
1413
1750
  onClick: onPlayPause
1414
1751
  });
1415
1752
  }
@@ -1417,18 +1754,28 @@ function GenreActionBand({
1417
1754
  items.push({
1418
1755
  key: "zoom-in",
1419
1756
  label: "Focus",
1420
- 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" }),
1421
1758
  onClick: onZoomIn
1422
1759
  });
1423
1760
  }
1424
1761
  if (onAddChild) {
1425
- adminItems.push({ key: "add-child", label: "Add", icon: /* @__PURE__ */ (0, import_jsx_runtime21.jsx)(import_lucide_react8.Plus, { "aria-hidden": "true", className: "w-5 h-5" }), onClick: onAddChild });
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 });
1426
1763
  }
1427
1764
  if (onRename) {
1428
- adminItems.push({ key: "rename", label: "Rename", icon: /* @__PURE__ */ (0, import_jsx_runtime21.jsx)(import_lucide_react8.Pencil, { "aria-hidden": "true", className: "w-5 h-5" }), onClick: onRename });
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 });
1429
1766
  }
1430
1767
  if (onChangeParent) {
1431
- adminItems.push({ key: "change-parent", label: "Parent", icon: /* @__PURE__ */ (0, import_jsx_runtime21.jsx)(import_lucide_react8.MoveUpRight, { "aria-hidden": "true", className: "w-5 h-5" }), onClick: onChangeParent });
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
+ });
1432
1779
  }
1433
1780
  for (const action of additionalActions?.(node) ?? []) {
1434
1781
  items.push({
@@ -1439,31 +1786,35 @@ function GenreActionBand({
1439
1786
  disabled: action.enabled ? !action.enabled(node) : false
1440
1787
  });
1441
1788
  }
1442
- if (items.length === 0 && adminItems.length === 0) return null;
1443
- 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)(
1444
1791
  "button",
1445
1792
  {
1446
1793
  type: "button",
1447
1794
  disabled: item.disabled,
1795
+ title: item.title,
1448
1796
  onClick: item.onClick,
1449
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",
1450
1798
  children: [
1451
- /* @__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 }),
1452
1800
  item.label
1453
1801
  ]
1454
1802
  },
1455
1803
  item.key
1456
1804
  ));
1457
- return /* @__PURE__ */ (0, import_jsx_runtime21.jsxs)("div", { className: "genre-action-band border-b border-[var(--color-border)]", children: [
1458
- items.length > 0 && /* @__PURE__ */ (0, import_jsx_runtime21.jsx)("div", { className: "flex flex-wrap gap-1 px-3.5 py-3", children: renderItems(items) }),
1459
- 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)(
1460
1811
  "section",
1461
1812
  {
1462
1813
  "aria-label": "Admin actions",
1463
1814
  className: `px-3.5 py-3 ${items.length > 0 ? "border-t border-[var(--color-border)]" : ""}`,
1464
1815
  children: [
1465
- /* @__PURE__ */ (0, import_jsx_runtime21.jsx)("h3", { className: "mb-2 text-xs font-semibold uppercase tracking-wide text-[var(--color-text-muted)]", children: "Admin" }),
1466
- /* @__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) })
1467
1818
  ]
1468
1819
  }
1469
1820
  )
@@ -1471,16 +1822,16 @@ function GenreActionBand({
1471
1822
  }
1472
1823
 
1473
1824
  // src/genre-tree/PanelEdgeTab.tsx
1474
- var import_lucide_react9 = require("lucide-react");
1825
+ var import_lucide_react12 = require("lucide-react");
1475
1826
  var import_tailwind_merge = require("tailwind-merge");
1476
- var import_jsx_runtime22 = require("react/jsx-runtime");
1827
+ var import_jsx_runtime26 = require("react/jsx-runtime");
1477
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)]";
1478
- function PanelEdgeTab({ side, edge, center, controls, className }) {
1829
+ function PanelEdgeTab({ side, edge, center, controls, className: className2 }) {
1479
1830
  if (!edge && !center) return null;
1480
1831
  const isLeft = side === "left";
1481
- const EdgeIcon = isLeft ? import_lucide_react9.ChevronLeft : import_lucide_react9.ChevronRight;
1482
- const CenterIcon = isLeft ? import_lucide_react9.ChevronRight : import_lucide_react9.ChevronLeft;
1483
- const renderButton = (action, icon) => /* @__PURE__ */ (0, import_jsx_runtime22.jsx)(
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)(
1484
1835
  "button",
1485
1836
  {
1486
1837
  type: "button",
@@ -1493,20 +1844,20 @@ function PanelEdgeTab({ side, edge, center, controls, className }) {
1493
1844
  children: icon
1494
1845
  }
1495
1846
  );
1496
- const edgeButton = edge && renderButton(edge, /* @__PURE__ */ (0, import_jsx_runtime22.jsx)(EdgeIcon, { "aria-hidden": "true", className: "h-5 w-5" }));
1497
- const centerButton = center && renderButton(center, /* @__PURE__ */ (0, import_jsx_runtime22.jsx)(CenterIcon, { "aria-hidden": "true", className: "h-5 w-5" }));
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" }));
1498
1849
  const buttons = isLeft ? [edgeButton, centerButton] : [centerButton, edgeButton];
1499
- return /* @__PURE__ */ (0, import_jsx_runtime22.jsxs)(
1850
+ return /* @__PURE__ */ (0, import_jsx_runtime26.jsxs)(
1500
1851
  "div",
1501
1852
  {
1502
1853
  className: (0, import_tailwind_merge.twMerge)(
1503
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)]",
1504
1855
  isLeft ? "rounded-r-lg" : "rounded-l-lg [clip-path:inset(-0.5rem_0_-0.5rem_-0.5rem)]",
1505
- className
1856
+ className2
1506
1857
  ),
1507
1858
  children: [
1508
1859
  buttons[0],
1509
- buttons[0] && buttons[1] && /* @__PURE__ */ (0, import_jsx_runtime22.jsx)("div", { role: "presentation", className: "h-px shrink-0 bg-[var(--color-border)]" }),
1860
+ buttons[0] && buttons[1] && /* @__PURE__ */ (0, import_jsx_runtime26.jsx)("div", { role: "presentation", className: "h-px shrink-0 bg-[var(--color-border)]" }),
1510
1861
  buttons[1]
1511
1862
  ]
1512
1863
  }
@@ -1514,9 +1865,9 @@ function PanelEdgeTab({ side, edge, center, controls, className }) {
1514
1865
  }
1515
1866
 
1516
1867
  // src/genre-tree/ChangeParentPopup.tsx
1517
- var import_react8 = require("react");
1518
- var import_genre_tree11 = require("@behindthemusictree/app-kit/genre-tree");
1519
- var import_jsx_runtime23 = require("react/jsx-runtime");
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");
1520
1871
  function ChangeParentPopup({
1521
1872
  genrePlaylist,
1522
1873
  genrePlaylists,
@@ -1525,9 +1876,9 @@ function ChangeParentPopup({
1525
1876
  onClose,
1526
1877
  onParentChanged
1527
1878
  }) {
1528
- const { mutate, isPending, formErrors } = (0, import_genre_tree11.useUpdateGenre)(scope, getBackendBaseUrl);
1529
- const candidates = (0, import_react8.useMemo)(
1530
- () => (0, import_genre_tree11.getReparentCandidates)(genrePlaylists, genrePlaylist.uuid),
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),
1531
1882
  [genrePlaylists, genrePlaylist.uuid]
1532
1883
  );
1533
1884
  const handleSelect = (newParent) => {
@@ -1544,19 +1895,19 @@ function ChangeParentPopup({
1544
1895
  }
1545
1896
  );
1546
1897
  };
1547
- (0, import_react8.useEffect)(() => {
1898
+ (0, import_react11.useEffect)(() => {
1548
1899
  const handleKeyDown = (event) => {
1549
1900
  if (event.key === "Escape") onClose();
1550
1901
  };
1551
1902
  window.addEventListener("keydown", handleKeyDown);
1552
1903
  return () => window.removeEventListener("keydown", handleKeyDown);
1553
1904
  }, [onClose]);
1554
- return /* @__PURE__ */ (0, import_jsx_runtime23.jsxs)(BasePopup_default, { title: `Change parent of ${genrePlaylist.name}`, onClose, showCancelButton: true, fitContent: true, children: [
1555
- /* @__PURE__ */ (0, import_jsx_runtime23.jsxs)("p", { className: "mb-3 text-sm text-[var(--color-text-muted)]", children: [
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: [
1556
1907
  "Current parent: ",
1557
1908
  genrePlaylist.parent?.name ?? "none"
1558
1909
  ] }),
1559
- /* @__PURE__ */ (0, import_jsx_runtime23.jsx)("div", { "aria-busy": isPending, className: isPending ? "pointer-events-none opacity-60" : void 0, children: /* @__PURE__ */ (0, import_jsx_runtime23.jsx)(
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)(
1560
1911
  GenreSearch,
1561
1912
  {
1562
1913
  genrePlaylists: candidates,
@@ -1564,32 +1915,32 @@ function ChangeParentPopup({
1564
1915
  placeholder: "Search a new parent\u2026"
1565
1916
  }
1566
1917
  ) }),
1567
- formErrors.length > 0 && /* @__PURE__ */ (0, import_jsx_runtime23.jsx)("p", { role: "alert", className: "mt-3 text-sm text-[var(--color-red-500)]", children: formErrors.map(({ message }) => message).join(" ") })
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(" ") })
1568
1919
  ] });
1569
1920
  }
1570
1921
 
1571
1922
  // src/genre-tree/artist/useArtistPanel.tsx
1572
- var import_ui15 = require("@behindthemusictree/ui");
1573
- 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");
1574
1925
 
1575
1926
  // src/genre-tree/artist/ArtistHeader.tsx
1576
1927
  var import_ui10 = require("@behindthemusictree/ui");
1577
1928
 
1578
1929
  // src/genre-tree/artist/ArtistLinks.tsx
1579
- var import_lucide_react10 = require("lucide-react");
1580
- var import_si = require("react-icons/si");
1930
+ var import_lucide_react13 = require("lucide-react");
1931
+ var import_si3 = require("react-icons/si");
1581
1932
  var import_ui9 = require("@behindthemusictree/ui");
1582
- var import_jsx_runtime24 = require("react/jsx-runtime");
1933
+ var import_jsx_runtime28 = require("react/jsx-runtime");
1583
1934
  var LINK_KINDS = {
1584
- official: { label: "Official website", Icon: import_lucide_react10.Globe },
1585
- wikipedia: { label: "Wikipedia", Icon: import_si.SiWikipedia },
1586
- wikidata: { label: "Wikidata", Icon: import_si.SiWikidata },
1587
- spotify: { label: "Spotify", Icon: import_si.SiSpotify },
1588
- youtube: { label: "YouTube", Icon: import_si.SiYoutube },
1589
- bandcamp: { label: "Bandcamp", Icon: import_si.SiBandcamp },
1590
- soundcloud: { label: "SoundCloud", Icon: import_si.SiSoundcloud },
1591
- discogs: { label: "Discogs", Icon: import_si.SiDiscogs },
1592
- social: { label: "Social", Icon: import_lucide_react10.Share2 }
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 }
1593
1944
  };
1594
1945
  function socialLabel(url) {
1595
1946
  try {
@@ -1609,14 +1960,14 @@ function artistLinks(artist) {
1609
1960
  links.push({
1610
1961
  key: "musicbrainz",
1611
1962
  url: `https://musicbrainz.org/artist/${artist.musicbrainzArtistId}`,
1612
- Icon: import_si.SiMusicbrainz,
1963
+ Icon: import_si3.SiMusicbrainz,
1613
1964
  label: "MusicBrainz"
1614
1965
  });
1615
1966
  }
1616
1967
  return links;
1617
1968
  }
1618
1969
  function ArtistLinks({ links }) {
1619
- return /* @__PURE__ */ (0, import_jsx_runtime24.jsx)("ul", { className: "flex flex-wrap gap-1.5", children: links.map(({ key, url, Icon, label }) => /* @__PURE__ */ (0, import_jsx_runtime24.jsx)("li", { children: /* @__PURE__ */ (0, import_jsx_runtime24.jsx)(import_ui9.Chip, { href: url, icon: /* @__PURE__ */ (0, import_jsx_runtime24.jsx)(Icon, { className: "h-3.5 w-3.5 shrink-0", "aria-hidden": "true" }), children: label }) }, key)) });
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)) });
1620
1971
  }
1621
1972
 
1622
1973
  // src/genre-tree/artist/formatYears.ts
@@ -1627,7 +1978,7 @@ function formatYears(beginYear, endYear, ended) {
1627
1978
  }
1628
1979
 
1629
1980
  // src/genre-tree/artist/ArtistHeader.tsx
1630
- var import_jsx_runtime25 = require("react/jsx-runtime");
1981
+ var import_jsx_runtime29 = require("react/jsx-runtime");
1631
1982
  var countryNames = new Intl.DisplayNames(["en"], { type: "region" });
1632
1983
  function ArtistHeader({ artist }) {
1633
1984
  const fields = [];
@@ -1637,35 +1988,35 @@ function ArtistHeader({ artist }) {
1637
1988
  const years = formatYears(artist.beginYear, artist.endYear, artist.ended);
1638
1989
  if (years) fields.push({ label: "Active", value: years });
1639
1990
  const links = artistLinks(artist);
1640
- 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 }) });
1641
1992
  if (fields.length === 0) return null;
1642
- 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 });
1643
1994
  }
1644
1995
 
1645
1996
  // src/genre-tree/artist/ArtistPhoto.tsx
1646
- var import_jsx_runtime26 = require("react/jsx-runtime");
1997
+ var import_jsx_runtime30 = require("react/jsx-runtime");
1647
1998
  function ExternalLink({ href, children }) {
1648
- return /* @__PURE__ */ (0, import_jsx_runtime26.jsx)("a", { href, target: "_blank", rel: "noopener noreferrer", className: "underline-offset-2 hover:underline", children });
1999
+ return /* @__PURE__ */ (0, import_jsx_runtime30.jsx)("a", { href, target: "_blank", rel: "noopener noreferrer", className: "underline-offset-2 hover:underline", children });
1649
2000
  }
1650
2001
  function ArtistPhoto({ image, name }) {
1651
- return /* @__PURE__ */ (0, import_jsx_runtime26.jsxs)("figure", { className: "flex flex-col gap-1.5 px-6 py-2.5", children: [
1652
- /* @__PURE__ */ (0, import_jsx_runtime26.jsx)("img", { src: image.url, alt: name, className: "h-64 w-full object-contain bg-[var(--color-surface-muted)] rounded" }),
1653
- /* @__PURE__ */ (0, import_jsx_runtime26.jsxs)("figcaption", { className: "text-xs text-[var(--color-text-muted)]", children: [
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: [
1654
2005
  "Photo: ",
1655
- /* @__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" }),
1656
2007
  ",",
1657
2008
  " ",
1658
- 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
1659
2010
  ] })
1660
2011
  ] });
1661
2012
  }
1662
2013
 
1663
2014
  // src/genre-tree/artist/ArtistMembers.tsx
1664
2015
  var import_ui11 = require("@behindthemusictree/ui");
1665
- var import_jsx_runtime27 = require("react/jsx-runtime");
2016
+ var import_jsx_runtime31 = require("react/jsx-runtime");
1666
2017
  function MembershipList({ title, memberships, renderArtistLink }) {
1667
2018
  if (memberships.length === 0) return null;
1668
- return /* @__PURE__ */ (0, import_jsx_runtime27.jsx)(import_ui11.PanelGroup, { title, children: /* @__PURE__ */ (0, import_jsx_runtime27.jsx)("ul", { children: memberships.map((membership, index) => {
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) => {
1669
2020
  const details = [
1670
2021
  formatYears(membership.beginYear, membership.endYear, membership.ended),
1671
2022
  ...membership.roles
@@ -1673,9 +2024,9 @@ function MembershipList({ title, memberships, renderArtistLink }) {
1673
2024
  const detailsText = details.filter(Boolean).join(" \xB7 ");
1674
2025
  return (
1675
2026
  // A member can have several periods in the same group.
1676
- /* @__PURE__ */ (0, import_jsx_runtime27.jsxs)("li", { className: "truncate py-0.5", title: detailsText || void 0, children: [
1677
- /* @__PURE__ */ (0, import_jsx_runtime27.jsx)("span", { className: "font-medium text-[var(--color-text)]", children: renderArtistLink(membership.artist) }),
1678
- detailsText && /* @__PURE__ */ (0, import_jsx_runtime27.jsxs)("span", { className: "text-[var(--color-text-muted)]", children: [
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: [
1679
2030
  " \u2014 ",
1680
2031
  detailsText
1681
2032
  ] })
@@ -1686,9 +2037,9 @@ function MembershipList({ title, memberships, renderArtistLink }) {
1686
2037
  function ArtistMembers({ title, memberships, renderArtistLink }) {
1687
2038
  if (memberships.length === 0) return null;
1688
2039
  const isCurrent = (membership) => !membership.ended && membership.endYear === null;
1689
- return /* @__PURE__ */ (0, import_jsx_runtime27.jsxs)(import_ui11.PanelSection, { title, children: [
1690
- /* @__PURE__ */ (0, import_jsx_runtime27.jsx)(MembershipList, { title: "Current", memberships: memberships.filter(isCurrent), renderArtistLink }),
1691
- /* @__PURE__ */ (0, import_jsx_runtime27.jsx)(
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)(
1692
2043
  MembershipList,
1693
2044
  {
1694
2045
  title: "Past",
@@ -1701,7 +2052,7 @@ function ArtistMembers({ title, memberships, renderArtistLink }) {
1701
2052
 
1702
2053
  // src/genre-tree/artist/ArtistDiscography.tsx
1703
2054
  var import_ui12 = require("@behindthemusictree/ui");
1704
- var import_jsx_runtime28 = require("react/jsx-runtime");
2055
+ var import_jsx_runtime32 = require("react/jsx-runtime");
1705
2056
  var GROUPS = ["Albums", "EPs", "Singles", "Live", "Compilations", "Other"];
1706
2057
  var PRIMARY_GROUPS = { album: "Albums", ep: "EPs", single: "Singles" };
1707
2058
  function discographyGroup({ primaryType, secondaryTypes }) {
@@ -1721,77 +2072,85 @@ function byYear(a, b) {
1721
2072
  function ArtistDiscography({ albums }) {
1722
2073
  if (albums.length === 0) return null;
1723
2074
  const sorted = [...albums].sort(byYear);
1724
- 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) => {
1725
2076
  const groupAlbums = sorted.filter((album) => discographyGroup(album) === group);
1726
2077
  if (groupAlbums.length === 0) return null;
1727
- return /* @__PURE__ */ (0, import_jsx_runtime28.jsx)(import_ui12.PanelGroup, { title: group, children: /* @__PURE__ */ (0, import_jsx_runtime28.jsx)("ul", { children: groupAlbums.map((album) => /* @__PURE__ */ (0, import_jsx_runtime28.jsxs)("li", { className: "grid grid-cols-[2.5rem_minmax(0,1fr)] gap-2 py-0.5", children: [
1728
- /* @__PURE__ */ (0, import_jsx_runtime28.jsx)("span", { className: "tabular-nums text-[var(--color-text-muted)]", children: album.year ?? "\u2014" }),
1729
- /* @__PURE__ */ (0, import_jsx_runtime28.jsx)("span", { className: "truncate text-[var(--color-text)]", title: album.name, children: album.name })
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 })
1730
2081
  ] }, album.uuid)) }) }, group);
1731
2082
  }) });
1732
2083
  }
1733
2084
 
1734
2085
  // src/genre-tree/artist/ArtistSongs.tsx
1735
- var import_react9 = require("react");
2086
+ var import_react12 = require("react");
1736
2087
  var import_react_query2 = require("@tanstack/react-query");
1737
- var import_lucide_react11 = require("lucide-react");
1738
- var import_ui14 = require("@behindthemusictree/ui");
2088
+ var import_lucide_react14 = require("lucide-react");
2089
+ var import_ui13 = require("@behindthemusictree/ui");
1739
2090
  var import_player9 = require("@behindthemusictree/app-kit/player");
1740
- var import_genre_tree15 = require("@behindthemusictree/app-kit/genre-tree");
2091
+ var import_genre_tree14 = require("@behindthemusictree/app-kit/genre-tree");
1741
2092
  var import_transport8 = require("@behindthemusictree/app-kit/transport");
1742
2093
 
1743
2094
  // src/genre-tree/track-list-sidebar/TrackItem.tsx
1744
2095
  var import_player8 = require("@behindthemusictree/app-kit/player");
1745
- var import_genre_tree12 = require("@behindthemusictree/app-kit/genre-tree");
2096
+ var import_genre_tree11 = require("@behindthemusictree/app-kit/genre-tree");
1746
2097
 
1747
2098
  // src/genre-tree/TrackPositionPlayPause.tsx
1748
- var import_fa6 = require("react-icons/fa");
2099
+ var import_fa5 = require("react-icons/fa");
1749
2100
  var import_player6 = require("@behindthemusictree/app-kit/player");
1750
2101
  var import_player7 = require("@behindthemusictree/app-kit/player");
1751
- var import_ui13 = require("@behindthemusictree/ui");
1752
- var import_jsx_runtime29 = require("react/jsx-runtime");
1753
- function TrackPositionPlayPause({ position, uuid, handlePlayPauseClick }) {
1754
- const { playerTrackObject, playState } = (0, import_player6.usePlayer)();
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)();
1755
2110
  const isThisTrack = playerTrackObject?.track.id === uuid;
1756
- 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)(
1757
2117
  "div",
1758
2118
  {
1759
- 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",
1760
2120
  style: { minWidth: "4rem", maxWidth: "4rem" },
1761
2121
  onClick: handlePlayPauseClick,
1762
2122
  children: [
1763
- /* @__PURE__ */ (0, import_jsx_runtime29.jsx)("div", { className: "group-hover:hidden", children: /* @__PURE__ */ (0, import_jsx_runtime29.jsx)("div", { children: isThisTrack && playState !== import_player7.PlayStates.STOPPED ? /* @__PURE__ */ (0, import_jsx_runtime29.jsxs)("div", { className: "flex space-x-1 items-end", children: [
1764
- /* @__PURE__ */ (0, import_jsx_runtime29.jsx)(
1765
- "div",
1766
- {
1767
- className: `w-playingbar bg-[var(--color-green-500)] h-3 origin-bottom ${playState === import_player7.PlayStates.PLAYING ? "animate-scale-pulse" : ""}`
1768
- }
1769
- ),
1770
- /* @__PURE__ */ (0, import_jsx_runtime29.jsx)(
1771
- "div",
1772
- {
1773
- className: `w-playingbar bg-[var(--color-green-500)] h-4 origin-bottom ${playState === import_player7.PlayStates.PLAYING ? "animate-scale-pulse delay-200" : ""}`,
1774
- style: { animationDelay: "0.2s" }
1775
- }
1776
- ),
1777
- /* @__PURE__ */ (0, import_jsx_runtime29.jsx)(
1778
- "div",
1779
- {
1780
- className: `w-playingbar bg-[var(--color-green-500)] h-3 origin-bottom ${playState === import_player7.PlayStates.PLAYING ? "animate-scale-pulse delay-400" : ""}`,
1781
- style: { animationDelay: "0.3s" }
1782
- }
1783
- )
1784
- ] }) : position }) }),
1785
- /* @__PURE__ */ (0, import_jsx_runtime29.jsx)("div", { className: "hidden group-hover:flex items-center justify-center", children: isThisTrack && playState === import_player7.PlayStates.LOADING ? /* @__PURE__ */ (0, import_jsx_runtime29.jsx)(import_ui13.RingLoader, { size: 16 }) : isThisTrack && playState === import_player7.PlayStates.PLAYING ? /* @__PURE__ */ (0, import_jsx_runtime29.jsx)(import_fa6.FaPause, {}) : /* @__PURE__ */ (0, import_jsx_runtime29.jsx)(import_fa6.FaPlay, {}) })
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
+ )
1786
2145
  ]
1787
2146
  }
1788
2147
  );
1789
2148
  }
1790
2149
 
1791
2150
  // src/genre-tree/track-list-sidebar/TrackItem.tsx
2151
+ var import_genre_tree12 = require("@behindthemusictree/app-kit/genre-tree");
1792
2152
  var import_genre_tree13 = require("@behindthemusictree/app-kit/genre-tree");
1793
- var import_genre_tree14 = require("@behindthemusictree/app-kit/genre-tree");
1794
- var import_jsx_runtime30 = require("react/jsx-runtime");
2153
+ var import_jsx_runtime34 = require("react/jsx-runtime");
1795
2154
  function TrackItem({
1796
2155
  track,
1797
2156
  position,
@@ -1800,25 +2159,26 @@ function TrackItem({
1800
2159
  renderArtistLink
1801
2160
  }) {
1802
2161
  const { handlePlayPauseAction, playerTrackObject, loadTrackForPlayer } = (0, import_player8.usePlayer)();
1803
- const { trackList, toTrackAtPosition } = (0, import_genre_tree12.useTrackList)();
2162
+ const { trackList, selectedTrack, toTrackAtPosition, runtimeUnplayableTrackIds } = (0, import_genre_tree11.useTrackList)();
1804
2163
  const scope = trackList?.origin?.scope ?? null;
1805
- 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;
1806
2167
  const handlePlayPauseClick = (event) => {
1807
2168
  event.stopPropagation();
1808
- if (playerTrackObject && playerTrackObject.track.id === track.uuid) {
2169
+ if (isCurrentTrack) {
1809
2170
  handlePlayPauseAction();
1810
2171
  } else if (scope != null) {
1811
2172
  toTrackAtPosition(position);
1812
2173
  loadTrackForPlayer(track.uuid);
1813
2174
  }
1814
2175
  };
1815
- return /* @__PURE__ */ (0, import_jsx_runtime30.jsxs)(
2176
+ return /* @__PURE__ */ (0, import_jsx_runtime34.jsxs)(
1816
2177
  "div",
1817
2178
  {
1818
- className: `track-item flex h-14 text-[var(--color-text)] ${unplayableReason ? "opacity-50" : "hover:bg-[var(--color-surface-muted)] group"}`,
1819
- title: unplayableReason ?? void 0,
2179
+ className: `track-item flex h-14 text-[var(--color-text)] ${isUnplayable ? "opacity-50" : "hover:bg-[var(--color-surface-muted)] group"}`,
1820
2180
  children: [
1821
- unplayableReason ? /* @__PURE__ */ (0, import_jsx_runtime30.jsx)(
2181
+ isUnplayable ? /* @__PURE__ */ (0, import_jsx_runtime34.jsx)(
1822
2182
  "div",
1823
2183
  {
1824
2184
  className: "flex items-center justify-center text-lg w-16",
@@ -1826,18 +2186,21 @@ function TrackItem({
1826
2186
  "aria-disabled": "true",
1827
2187
  children: position
1828
2188
  }
1829
- ) : /* @__PURE__ */ (0, import_jsx_runtime30.jsx)(TrackPositionPlayPause, { position, uuid: track.uuid, handlePlayPauseClick }),
1830
- /* @__PURE__ */ (0, import_jsx_runtime30.jsxs)("div", { className: "title-artist-container flex flex-col items-start justify-center min-w-0 flex-1", children: [
1831
- /* @__PURE__ */ (0, import_jsx_runtime30.jsxs)("div", { className: "flex max-w-full items-center gap-2", children: [
1832
- /* @__PURE__ */ (0, import_jsx_runtime30.jsx)("div", { className: "title text-lg font-bold text-[var(--color-text)] max-w-full truncate", children: track.title }),
1833
- (0, import_genre_tree13.isSong)(track) && /* @__PURE__ */ (0, import_jsx_runtime30.jsx)(MusicBrainzRecordingLink, { song: track })
1834
- ] }),
1835
- track.artists && track.artists.length > 0 ? /* @__PURE__ */ (0, import_jsx_runtime30.jsx)("div", { className: "artist text-base max-w-full truncate text-[var(--color-text-muted)]", children: /* @__PURE__ */ (0, import_jsx_runtime30.jsx)(import_genre_tree14.ArtistNames, { artists: track.artists, renderArtistLink }) }) : "",
1836
- unplayableReason && /* @__PURE__ */ (0, import_jsx_runtime30.jsx)("div", { className: "text-xs", children: /* @__PURE__ */ (0, import_jsx_runtime30.jsx)(UnplayableReason, { reason: unplayableReason }) })
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 }) }) : ""
1837
2201
  ] }),
1838
- /* @__PURE__ */ (0, import_jsx_runtime30.jsx)("div", { className: "album-name items-start justify-center w-1/3 min-w-0 ml-2 max-w-full truncate text-[var(--color-text-muted)]", children: track.album ? track.album.name : "" }),
1839
- /* @__PURE__ */ (0, import_jsx_runtime30.jsx)("div", { className: "genre-name-container flex items-center justify-end w-1/6 min-w-0 shrink-0", children: track.genre ? /* @__PURE__ */ (0, import_jsx_runtime30.jsx)("div", { className: "genre-name font-bold p-1 border border-[var(--color-text-muted)] text-xs max-w-full truncate", children: track.genre.name }) : "" }),
1840
- renderDuration && /* @__PURE__ */ (0, import_jsx_runtime30.jsx)(
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)(
1841
2204
  "div",
1842
2205
  {
1843
2206
  className: "duration flex text-base w-16 items-center justify-center",
@@ -1845,28 +2208,28 @@ function TrackItem({
1845
2208
  children: renderDuration(track)
1846
2209
  }
1847
2210
  ),
1848
- renderActions && /* @__PURE__ */ (0, import_jsx_runtime30.jsx)("div", { className: "edit flex text-base w-6 items-center justify-center mr-2 cursor-pointer", children: renderActions(track) })
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) })
1849
2212
  ]
1850
2213
  }
1851
2214
  );
1852
2215
  }
1853
2216
 
1854
2217
  // src/genre-tree/artist/ArtistSongs.tsx
1855
- var import_jsx_runtime31 = require("react/jsx-runtime");
1856
- var SongPageSchema2 = (0, import_transport8.PaginatedResponseSchema)(import_genre_tree15.SongDetailedSchema);
2218
+ var import_jsx_runtime35 = require("react/jsx-runtime");
2219
+ var SongPageSchema2 = (0, import_transport8.PaginatedResponseSchema)(import_genre_tree14.SongDetailedSchema);
1857
2220
  var SORTS = [
1858
2221
  { ordering: "title", label: "Title" },
1859
2222
  { ordering: "-albumYear", label: "Year" }
1860
2223
  ];
1861
2224
  function ArtistSongs({ artist, getBackendBaseUrl, renderArtistLink }) {
1862
- const [page, setPage] = (0, import_react9.useState)(1);
1863
- 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");
1864
2227
  const { fetch } = (0, import_transport8.useFetchWrapper)(getBackendBaseUrl);
1865
- const { playNewTrackListFromArtist } = (0, import_genre_tree15.useTrackList)();
2228
+ const { playNewTrackListFromArtist } = (0, import_genre_tree14.useTrackList)();
1866
2229
  const { playerTrackObject } = (0, import_player9.usePlayer)();
1867
2230
  const { data, isPending, isError } = (0, import_transport8.useQueryWithParse)({
1868
2231
  queryKey: ["referenceSongs", "artist", artist.uuid, ordering, page],
1869
- 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, {
1870
2233
  artist: artist.uuid,
1871
2234
  ordering,
1872
2235
  page
@@ -1881,25 +2244,25 @@ function ArtistSongs({ artist, getBackendBaseUrl, renderArtistLink }) {
1881
2244
  });
1882
2245
  const handleRowClickCapture = (event, song) => {
1883
2246
  if (!(event.target instanceof Element) || !event.target.closest(".track-position-play-pause")) return;
1884
- if (playerTrackObject?.track.id === song.uuid || (0, import_genre_tree15.songUnplayableReason)(song) !== null) return;
2247
+ if (playerTrackObject?.track.id === song.uuid || (0, import_genre_tree14.songUnplayableReason)(song) !== null) return;
1885
2248
  event.stopPropagation();
1886
2249
  void play(song.uuid);
1887
2250
  };
1888
- return /* @__PURE__ */ (0, import_jsx_runtime31.jsx)(import_ui14.PanelSection, { title: "Songs", children: isPending ? /* @__PURE__ */ (0, import_jsx_runtime31.jsx)(import_ui14.Skeleton, { className: "h-14", "aria-busy": "true" }) : isError ? /* @__PURE__ */ (0, import_jsx_runtime31.jsx)("p", { className: "text-red-600", children: "Could not load songs." }) : data.results.length === 0 ? /* @__PURE__ */ (0, import_jsx_runtime31.jsx)("p", { className: "text-[var(--color-text-muted)]", children: "No songs yet." }) : /* @__PURE__ */ (0, import_jsx_runtime31.jsxs)(import_jsx_runtime31.Fragment, { children: [
1889
- /* @__PURE__ */ (0, import_jsx_runtime31.jsxs)("div", { className: "flex flex-wrap items-center justify-between gap-2 mb-2", children: [
1890
- /* @__PURE__ */ (0, import_jsx_runtime31.jsxs)(
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)(
1891
2254
  "button",
1892
2255
  {
1893
2256
  type: "button",
1894
2257
  onClick: () => void play(),
1895
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",
1896
2259
  children: [
1897
- /* @__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" }),
1898
2261
  "Play all"
1899
2262
  ]
1900
2263
  }
1901
2264
  ),
1902
- /* @__PURE__ */ (0, import_jsx_runtime31.jsx)("div", { role: "group", "aria-label": "Sort songs by", className: "flex gap-1 text-xs", children: SORTS.map((sort) => /* @__PURE__ */ (0, import_jsx_runtime31.jsx)(
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)(
1903
2266
  "button",
1904
2267
  {
1905
2268
  type: "button",
@@ -1914,7 +2277,7 @@ function ArtistSongs({ artist, getBackendBaseUrl, renderArtistLink }) {
1914
2277
  sort.ordering
1915
2278
  )) })
1916
2279
  ] }),
1917
- /* @__PURE__ */ (0, import_jsx_runtime31.jsx)("ul", { className: "overflow-hidden mb-2 bg-zinc-950 rounded-lg", children: data.results.map((song, index) => /* @__PURE__ */ (0, import_jsx_runtime31.jsx)("li", { onClickCapture: (event) => handleRowClickCapture(event, song), children: /* @__PURE__ */ (0, import_jsx_runtime31.jsx)(
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)(
1918
2281
  TrackItem,
1919
2282
  {
1920
2283
  track: song,
@@ -1922,21 +2285,23 @@ function ArtistSongs({ artist, getBackendBaseUrl, renderArtistLink }) {
1922
2285
  renderArtistLink
1923
2286
  }
1924
2287
  ) }, song.uuid)) }),
1925
- /* @__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 })
1926
2289
  ] }) });
1927
2290
  }
1928
2291
 
1929
2292
  // src/genre-tree/artist/useArtistPanel.tsx
1930
- var import_jsx_runtime32 = require("react/jsx-runtime");
2293
+ var import_jsx_runtime36 = require("react/jsx-runtime");
1931
2294
  function useArtistPanel({
1932
2295
  uuid,
2296
+ scope,
1933
2297
  getBackendBaseUrl,
2298
+ getShareUrl,
1934
2299
  renderArtistLink,
1935
2300
  expanded,
1936
2301
  onExpandedChange,
1937
2302
  onClose
1938
2303
  }) {
1939
- const result = (0, import_genre_tree16.useArtist)(uuid, getBackendBaseUrl);
2304
+ const result = (0, import_genre_tree15.useArtist)(uuid, getBackendBaseUrl);
1940
2305
  const panel = { expanded, onExpandedChange, onClose };
1941
2306
  switch (result.status) {
1942
2307
  case "idle":
@@ -1945,38 +2310,41 @@ function useArtistPanel({
1945
2310
  return {
1946
2311
  ...panel,
1947
2312
  title: "Artist",
1948
- content: /* @__PURE__ */ (0, import_jsx_runtime32.jsxs)("div", { "aria-busy": "true", className: "flex flex-col gap-2.5 px-6 py-2.5", children: [
1949
- /* @__PURE__ */ (0, import_jsx_runtime32.jsx)(import_ui15.Skeleton, { className: "h-3 w-16" }),
1950
- /* @__PURE__ */ (0, import_jsx_runtime32.jsx)(import_ui15.Skeleton, { className: "h-4 w-32" }),
1951
- /* @__PURE__ */ (0, import_jsx_runtime32.jsx)(import_ui15.Skeleton, { className: "h-3 w-16" }),
1952
- /* @__PURE__ */ (0, import_jsx_runtime32.jsx)(import_ui15.Skeleton, { className: "h-4 w-24" }),
1953
- /* @__PURE__ */ (0, import_jsx_runtime32.jsx)(import_ui15.Skeleton, { className: "h-16 mt-2.5" }),
1954
- /* @__PURE__ */ (0, import_jsx_runtime32.jsx)(import_ui15.Skeleton, { className: "h-16" })
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" })
1955
2320
  ] })
1956
2321
  };
1957
2322
  case "not-found":
1958
2323
  return {
1959
2324
  ...panel,
1960
2325
  title: "Artist not found",
1961
- content: /* @__PURE__ */ (0, import_jsx_runtime32.jsx)("p", { className: "px-6 py-2.5 text-[var(--color-text-muted)]", children: "This artist does not exist." })
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." })
1962
2327
  };
1963
2328
  case "error":
1964
2329
  return {
1965
2330
  ...panel,
1966
2331
  title: "Artist",
1967
- content: /* @__PURE__ */ (0, import_jsx_runtime32.jsx)("p", { className: "px-6 py-2.5 text-red-600", children: "Could not load this artist." })
2332
+ content: /* @__PURE__ */ (0, import_jsx_runtime36.jsx)("p", { className: "px-6 py-2.5 text-red-600", children: "Could not load this artist." })
1968
2333
  };
1969
2334
  case "success": {
1970
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;
1971
2338
  return {
1972
2339
  ...panel,
1973
2340
  title: artist.name,
1974
- content: /* @__PURE__ */ (0, import_jsx_runtime32.jsxs)(import_jsx_runtime32.Fragment, { children: [
1975
- artist.image && /* @__PURE__ */ (0, import_jsx_runtime32.jsx)(ArtistPhoto, { image: artist.image, name: artist.name }),
1976
- /* @__PURE__ */ (0, import_jsx_runtime32.jsx)(ArtistHeader, { artist }),
1977
- /* @__PURE__ */ (0, import_jsx_runtime32.jsx)(ArtistMembers, { title: "Members", memberships: artist.members, renderArtistLink }),
1978
- /* @__PURE__ */ (0, import_jsx_runtime32.jsx)(ArtistMembers, { title: "Member of", memberships: artist.memberOf, renderArtistLink }),
1979
- artist.tracksCount > 0 && /* @__PURE__ */ (0, import_jsx_runtime32.jsx)(
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)(
1980
2348
  ArtistSongs,
1981
2349
  {
1982
2350
  artist: { uuid: artist.uuid, name: artist.name },
@@ -1985,7 +2353,7 @@ function useArtistPanel({
1985
2353
  }
1986
2354
  )
1987
2355
  ] }),
1988
- 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
1989
2357
  };
1990
2358
  }
1991
2359
  }
@@ -1993,28 +2361,28 @@ function useArtistPanel({
1993
2361
 
1994
2362
  // src/genre-tree/SizeMetricToggle.tsx
1995
2363
  var import_md2 = require("react-icons/md");
1996
- var import_ui16 = require("@behindthemusictree/ui");
1997
- var import_jsx_runtime33 = require("react/jsx-runtime");
2364
+ var import_ui15 = require("@behindthemusictree/ui");
2365
+ var import_jsx_runtime37 = require("react/jsx-runtime");
1998
2366
  function SizeMetricToggle({
1999
2367
  value,
2000
2368
  onChange
2001
2369
  }) {
2002
- return /* @__PURE__ */ (0, import_jsx_runtime33.jsx)(
2003
- import_ui16.SegmentedControl,
2370
+ return /* @__PURE__ */ (0, import_jsx_runtime37.jsx)(
2371
+ import_ui15.SegmentedControl,
2004
2372
  {
2005
2373
  "aria-label": "Size nodes by",
2006
2374
  value,
2007
2375
  onChange,
2008
2376
  options: [
2009
- { value: "songs", label: /* @__PURE__ */ (0, import_jsx_runtime33.jsx)(import_md2.MdMusicNote, { size: 18, "aria-hidden": true }), "aria-label": "Size by songs", title: "Node size by song count" },
2010
- { value: "views", label: /* @__PURE__ */ (0, import_jsx_runtime33.jsx)(import_md2.MdVisibility, { size: 18, "aria-hidden": true }), "aria-label": "Size by views", title: "Node size by song views" }
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" }
2011
2379
  ]
2012
2380
  }
2013
2381
  );
2014
2382
  }
2015
2383
 
2016
2384
  // src/genre-tree/GenreTreeView.tsx
2017
- var import_jsx_runtime34 = require("react/jsx-runtime");
2385
+ var import_jsx_runtime38 = require("react/jsx-runtime");
2018
2386
  var PREVIOUS_BUTTON_CSS = `
2019
2387
  .genre-tree-view .search-overlay .gtv-info-panel-previous { display: none; }
2020
2388
  .genre-tree-view:has(.search-overlay .gtv-info-panel-previous) .genre-search input { padding-left: 2.75rem; }
@@ -2036,7 +2404,7 @@ function GenreNodeActionBand({ genrePlaylist, scope, playback, ...bandProps }) {
2036
2404
  console.error("Failed to load genre playlist tracks:", error);
2037
2405
  });
2038
2406
  };
2039
- return /* @__PURE__ */ (0, import_jsx_runtime34.jsx)(
2407
+ return /* @__PURE__ */ (0, import_jsx_runtime38.jsx)(
2040
2408
  GenreActionBand,
2041
2409
  {
2042
2410
  ...bandProps,
@@ -2060,13 +2428,14 @@ function GenreTreeView({
2060
2428
  defaultViewport,
2061
2429
  onViewportChange,
2062
2430
  readOnly = false,
2063
- className,
2431
+ className: className2,
2064
2432
  showDisplayOptions = true,
2065
2433
  showAddRoot = true,
2066
2434
  allowWheelRotation: controlledAllowWheelRotation,
2067
2435
  criteriaOverviewSchema,
2068
2436
  renderGenreDetailExtras,
2069
2437
  controlsOrientation = "horizontal",
2438
+ floatingControlsLeading,
2070
2439
  sizeMetric: controlledSizeMetric,
2071
2440
  onSizeMetricChange,
2072
2441
  renderArtistLink,
@@ -2074,6 +2443,7 @@ function GenreTreeView({
2074
2443
  onArtistUuidChange,
2075
2444
  artistExpanded: controlledArtistExpanded,
2076
2445
  onArtistExpandedChange,
2446
+ getShareUrl,
2077
2447
  artistSearch = true,
2078
2448
  onArtistSelect,
2079
2449
  artistSearchBaseUrl,
@@ -2091,12 +2461,14 @@ function GenreTreeView({
2091
2461
  placement = "default",
2092
2462
  ...focus
2093
2463
  }) {
2094
- 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);
2095
2467
  const isArtistControlled = controlledArtistUuid !== void 0;
2096
2468
  const artistUuid = isArtistControlled ? controlledArtistUuid : internalArtistUuid;
2097
- const [internalSizeMetric, setInternalSizeMetric] = (0, import_react10.useState)("songs");
2469
+ const [internalSizeMetric, setInternalSizeMetric] = (0, import_react13.useState)("songs");
2098
2470
  const sizeMetric = controlledSizeMetric ?? internalSizeMetric;
2099
- const deferredSizeMetric = (0, import_react10.useDeferredValue)(sizeMetric);
2471
+ const deferredSizeMetric = (0, import_react13.useDeferredValue)(sizeMetric);
2100
2472
  const changeSizeMetric = (metric) => {
2101
2473
  if (controlledSizeMetric === void 0) setInternalSizeMetric(metric);
2102
2474
  onSizeMetricChange?.(metric);
@@ -2123,7 +2495,7 @@ function GenreTreeView({
2123
2495
  allowWheelRotation: internalAllowWheelRotation,
2124
2496
  toggleWheelRotation,
2125
2497
  onNodeHover: handleNodeHover
2126
- } = (0, import_genre_tree18.useGenreTreeViewState)({
2498
+ } = (0, import_genre_tree17.useGenreTreeViewState)({
2127
2499
  scope,
2128
2500
  getBackendBaseUrl,
2129
2501
  viewMode: controlledViewMode,
@@ -2132,25 +2504,31 @@ function GenreTreeView({
2132
2504
  criteriaOverviewSchema,
2133
2505
  artistUuid
2134
2506
  });
2135
- const isGenreUpdating = (0, import_react_query3.useIsMutating)({ mutationKey: import_genre_tree18.genreUpdateMutationKey }) > 0;
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);
2136
2509
  const busy = deferredSizeMetric !== sizeMetric || isTreeFetching || isGenreUpdating;
2137
- const artistResult = (0, import_genre_tree18.useArtist)(artistUuid, artistSearchBaseUrl ?? getBackendBaseUrl);
2510
+ const isArtistFiltered = artistUuid !== null;
2511
+ const canSizeByViews = !isArtistFiltered && hasViewsCount(visibleGenrePlaylists);
2512
+ const artistResult = (0, import_genre_tree17.useArtist)(artistUuid, artistSearchBaseUrl ?? getBackendBaseUrl);
2138
2513
  const filterArtistName = artistResult.status === "success" ? artistResult.artist.name : null;
2139
- const filterArtist = (0, import_react10.useMemo)(
2514
+ const filterArtist = (0, import_react13.useMemo)(
2140
2515
  () => artistUuid !== null && filterArtistName !== null ? { uuid: artistUuid, name: filterArtistName } : null,
2141
2516
  [artistUuid, filterArtistName]
2142
2517
  );
2518
+ const hasSelectedGenreNode = selectedGenrePlaylist !== void 0 && visibleGenrePlaylists.some(({ uuid }) => uuid === selectedGenrePlaylist.uuid);
2143
2519
  const playback = useGenrePlaylistPlayback({ scope, genrePlaylists: visibleGenrePlaylists, artist: filterArtist });
2144
2520
  const openArtist = (uuid) => {
2145
2521
  if (selectedNodeId !== null) clearSelection();
2146
2522
  changeArtist(uuid);
2147
2523
  };
2148
- const [expandedArtistUuid, setExpandedArtistUuid] = (0, import_react10.useState)(null);
2524
+ const [expandedArtistUuid, setExpandedArtistUuid] = (0, import_react13.useState)(null);
2149
2525
  const artistBaseUrl = artistSearchBaseUrl ?? getBackendBaseUrl;
2150
2526
  const artistPanel = useArtistPanel({
2151
2527
  uuid: artistUuid,
2528
+ scope,
2152
2529
  getBackendBaseUrl: artistBaseUrl,
2153
- renderArtistLink: renderArtistLink ?? ((artist) => artist.isSpecialPurpose ? artist.name : /* @__PURE__ */ (0, import_jsx_runtime34.jsx)("button", { type: "button", onClick: () => openArtist(artist.uuid), className: "hover:underline", children: artist.name })),
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 })),
2154
2532
  expanded: controlledArtistExpanded ?? (artistUuid !== null && expandedArtistUuid === artistUuid),
2155
2533
  onExpandedChange: (on) => {
2156
2534
  if (controlledArtistExpanded === void 0) setExpandedArtistUuid(on ? artistUuid : null);
@@ -2158,10 +2536,10 @@ function GenreTreeView({
2158
2536
  },
2159
2537
  onClose: () => changeArtist(null)
2160
2538
  });
2161
- const builtInArtistPanel = overlayPanel || selectedNodeId !== null ? void 0 : artistPanel;
2539
+ const builtInArtistPanel = overlayPanel || hasSelectedGenreNode ? void 0 : artistPanel;
2162
2540
  const activeOverlayPanel = overlayPanel ?? (builtInArtistPanel && { ...builtInArtistPanel, hideTitle: true, hideClose: true });
2163
- const [uncontrolledCollapsed, setUncontrolledCollapsed] = (0, import_react10.useState)(false);
2164
- const [uncontrolledExpanded, setUncontrolledExpanded] = (0, import_react10.useState)(false);
2541
+ const [uncontrolledCollapsed, setUncontrolledCollapsed] = (0, import_react13.useState)(false);
2542
+ const [uncontrolledExpanded, setUncontrolledExpanded] = (0, import_react13.useState)(false);
2165
2543
  const isCollapsed = collapsed ?? uncontrolledCollapsed;
2166
2544
  const isExpanded = expanded ?? uncontrolledExpanded;
2167
2545
  const changeCollapsed = (next) => {
@@ -2172,11 +2550,11 @@ function GenreTreeView({
2172
2550
  setUncontrolledExpanded(next);
2173
2551
  onExpandedChange?.(next);
2174
2552
  };
2175
- (0, import_react10.useEffect)(() => {
2553
+ (0, import_react13.useEffect)(() => {
2176
2554
  setUncontrolledCollapsed(false);
2177
2555
  }, [selectedNodeId, artistUuid]);
2178
- const isDetailHidden = isCollapsed || !selectedNodeId && !activeOverlayPanel;
2179
- (0, import_react10.useEffect)(() => {
2556
+ const isDetailHidden = isCollapsed || !hasSelectedGenreNode && !activeOverlayPanel;
2557
+ (0, import_react13.useEffect)(() => {
2180
2558
  if (placement !== "regions" || !isDetailHidden) return void 0;
2181
2559
  const root = document.documentElement;
2182
2560
  root.style.setProperty("--layout-left-w", "0px");
@@ -2195,29 +2573,38 @@ function GenreTreeView({
2195
2573
  hideExpandButton: placement === "regions" ? true : void 0,
2196
2574
  overlayPanel: detailOverlay ? { ...detailOverlay, collapsed: isCollapsed, hideExpandButton: true } : activeOverlayPanel
2197
2575
  };
2198
- const [isWheelRevealed, setIsWheelRevealed] = (0, import_react10.useState)(false);
2576
+ const [isWheelRevealed, setIsWheelRevealed] = (0, import_react13.useState)(false);
2199
2577
  const usesHandoff = viewMode === "wheel" || viewMode === "pop-core";
2200
2578
  const isTreeReady = !isLoading && (!usesHandoff || isWheelRevealed);
2201
- const rootRef = (0, import_react10.useRef)(null);
2579
+ const rootRef = (0, import_react13.useRef)(null);
2202
2580
  const handlePrevious = () => {
2203
2581
  rootRef.current?.querySelector(".search-overlay .gtv-info-panel-previous")?.click();
2204
2582
  };
2205
- const { showPopup, hidePopup } = (0, import_popup5.usePopup)();
2206
- const [searchText, setSearchText] = (0, import_react10.useState)("");
2207
- const artistSearchResult = (0, import_genre_tree17.useArtistSearch)(artistBaseUrl, artistSearch ? searchText : "");
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 : "");
2208
2595
  const artistExtraResults = artistSearch ? {
2209
2596
  label: "Artists",
2210
2597
  items: artistSearchResult.artists.map((artist) => ({
2211
2598
  key: artist.uuid,
2212
- content: /* @__PURE__ */ (0, import_jsx_runtime34.jsxs)(import_jsx_runtime34.Fragment, { children: [
2213
- /* @__PURE__ */ (0, import_jsx_runtime34.jsxs)("span", { className: "block truncate font-medium", children: [
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: [
2214
2601
  artist.name,
2215
- artist.disambiguation && /* @__PURE__ */ (0, import_jsx_runtime34.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: [
2216
2603
  " \xB7 ",
2217
2604
  artist.disambiguation
2218
2605
  ] })
2219
2606
  ] }),
2220
- artist.matchedAlias && /* @__PURE__ */ (0, import_jsx_runtime34.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: [
2221
2608
  "also known as ",
2222
2609
  artist.matchedAlias
2223
2610
  ] })
@@ -2243,31 +2630,47 @@ function GenreTreeView({
2243
2630
  const handleGenreSearchZoomIn = () => {
2244
2631
  if (selectedUuid) focus.onFocusGenreChange?.(selectedUuid, "zoom-in");
2245
2632
  };
2246
- const zoomInRef = (0, import_react10.useRef)(void 0);
2633
+ const zoomInRef = (0, import_react13.useRef)(void 0);
2247
2634
  zoomInRef.current = canZoomIn ? handleGenreSearchZoomIn : void 0;
2248
- const renderInfoPanelBody = (0, import_react10.useCallback)(
2635
+ const renderInfoPanelBody = (0, import_react13.useCallback)(
2249
2636
  (node, { toolbar, ancestors, family }) => {
2250
2637
  const genrePlaylist = genrePlaylistByNodeId.get(node.id);
2251
2638
  const criteria = genrePlaylist?.criteria;
2252
- if (!genrePlaylist || !criteria) return { primary: /* @__PURE__ */ (0, import_jsx_runtime34.jsxs)(import_jsx_runtime34.Fragment, { children: [
2639
+ if (!genrePlaylist || !criteria) return { primary: /* @__PURE__ */ (0, import_jsx_runtime38.jsxs)(import_jsx_runtime38.Fragment, { children: [
2253
2640
  toolbar,
2254
2641
  family
2255
2642
  ] }), aside: ancestors };
2256
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;
2257
2658
  return {
2258
- primary: /* @__PURE__ */ (0, import_jsx_runtime34.jsxs)(import_jsx_runtime34.Fragment, { children: [
2659
+ primary: /* @__PURE__ */ (0, import_jsx_runtime38.jsxs)(import_jsx_runtime38.Fragment, { children: [
2259
2660
  toolbar,
2260
- /* @__PURE__ */ (0, import_jsx_runtime34.jsx)(
2661
+ /* @__PURE__ */ (0, import_jsx_runtime38.jsx)(
2261
2662
  GenreNodeActionBand,
2262
2663
  {
2263
2664
  node,
2264
2665
  genrePlaylist,
2265
2666
  scope,
2667
+ shareTarget: shareUrl ? shareTarget : void 0,
2668
+ shareUrl,
2266
2669
  onZoomIn: zoomInRef.current,
2267
2670
  onAddChild: readOnly ? void 0 : () => handleGenreCreationAction(criteria),
2268
2671
  onRename: readOnly ? void 0 : () => handleGenreRenameAction(criteria),
2269
2672
  onChangeParent: readOnly ? void 0 : () => showPopup(
2270
- /* @__PURE__ */ (0, import_jsx_runtime34.jsx)(
2673
+ /* @__PURE__ */ (0, import_jsx_runtime38.jsx)(
2271
2674
  ChangeParentPopup,
2272
2675
  {
2273
2676
  genrePlaylist: { ...genrePlaylist, criteria },
@@ -2279,16 +2682,20 @@ function GenreTreeView({
2279
2682
  }
2280
2683
  )
2281
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,
2282
2688
  additionalActions,
2283
2689
  playback
2284
2690
  }
2285
2691
  ),
2286
- /* @__PURE__ */ (0, import_jsx_runtime34.jsx)(
2692
+ /* @__PURE__ */ (0, import_jsx_runtime38.jsx)(
2287
2693
  GenreDetailExtras,
2288
2694
  {
2289
2695
  family,
2290
2696
  genreUuid,
2291
2697
  genrePlaylist,
2698
+ wikidataId: criteria.wikidataId,
2292
2699
  scope,
2293
2700
  getBackendBaseUrl,
2294
2701
  criteriaOverviewSchema,
@@ -2317,6 +2724,7 @@ function GenreTreeView({
2317
2724
  hidePopup,
2318
2725
  onParentChanged,
2319
2726
  visibleGenrePlaylists,
2727
+ updateGenre,
2320
2728
  playback,
2321
2729
  filterArtist,
2322
2730
  scope,
@@ -2333,7 +2741,7 @@ function GenreTreeView({
2333
2741
  );
2334
2742
  const isRotationControlled = controlledAllowWheelRotation !== void 0;
2335
2743
  const allowWheelRotation = controlledAllowWheelRotation ?? internalAllowWheelRotation;
2336
- const detailTab = placement === "regions" && (selectedNodeId || detailOverlay) ? /* @__PURE__ */ (0, import_jsx_runtime34.jsx)(
2744
+ const detailTab = placement === "regions" && (hasSelectedGenreNode || detailOverlay) ? /* @__PURE__ */ (0, import_jsx_runtime38.jsx)(
2337
2745
  PanelEdgeTab,
2338
2746
  {
2339
2747
  side: "left",
@@ -2341,25 +2749,14 @@ function GenreTreeView({
2341
2749
  center: detailExpanded ? null : isCollapsed ? { label: "Show panel", onClick: () => changeCollapsed(false) } : onDetailExpandedChange ? { label: "Extend panel", onClick: () => onDetailExpandedChange(true) } : null
2342
2750
  }
2343
2751
  ) : null;
2344
- const actions = /* @__PURE__ */ (0, import_jsx_runtime34.jsxs)(import_jsx_runtime34.Fragment, { children: [
2345
- !isLoading && /* @__PURE__ */ (0, import_jsx_runtime34.jsx)(
2346
- GenreSearch,
2347
- {
2348
- genrePlaylists: visibleGenrePlaylists,
2349
- onSelect: handleGenreSearchSelect,
2350
- selectedName: selectedGenrePlaylist?.name ?? builtInArtistPanel?.title ?? null,
2351
- onClear: builtInArtistPanel ? () => changeArtist(null) : handleGenreSearchClear,
2352
- onPrevious: handlePrevious,
2353
- onQueryChange: artistSearch ? setSearchText : void 0,
2354
- extraResults: artistExtraResults
2355
- }
2356
- ),
2357
- !isLoading && filterArtist && /* @__PURE__ */ (0, import_jsx_runtime34.jsxs)("span", { className: "inline-flex items-center overflow-hidden rounded-full bg-[var(--color-neutral-100)] text-sm", children: [
2358
- /* @__PURE__ */ (0, import_jsx_runtime34.jsx)(
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)(
2359
2756
  "button",
2360
2757
  {
2361
2758
  type: "button",
2362
- 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",
2363
2760
  "aria-label": `Show artist ${filterArtist.name}`,
2364
2761
  onClick: () => {
2365
2762
  if (selectedNodeId !== null) clearSelection();
@@ -2367,86 +2764,174 @@ function GenreTreeView({
2367
2764
  children: filterArtist.name
2368
2765
  }
2369
2766
  ),
2370
- /* @__PURE__ */ (0, import_jsx_runtime34.jsx)(
2767
+ /* @__PURE__ */ (0, import_jsx_runtime38.jsx)(
2371
2768
  "button",
2372
2769
  {
2373
2770
  type: "button",
2374
- 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)]",
2375
2772
  "aria-label": `Clear artist filter ${filterArtist.name}`,
2376
2773
  onClick: () => changeArtist(null),
2377
2774
  children: "\xD7"
2378
2775
  }
2379
2776
  )
2380
- ] }),
2381
- !isLoading && !isControlled && /* @__PURE__ */ (0, import_jsx_runtime34.jsx)(
2382
- import_ui17.SegmentedControl,
2383
- {
2384
- "aria-label": "Tree view mode",
2385
- value: viewMode,
2386
- onChange: setViewMode,
2387
- options: [
2388
- { value: "pop-core", label: "Pop/Core", disabled: !canShowPopCore, title: canShowPopCore ? void 0 : NO_POP_ROOT_TITLE },
2389
- { value: "wheel", label: "Wheel" },
2390
- { value: "stacked", label: "Stacked" },
2391
- { value: "outline", label: "Outline", disabled: !canShowPopCore, title: canShowPopCore ? void 0 : NO_POP_ROOT_TITLE }
2392
- ]
2393
- }
2394
- ),
2395
- !isLoading && showDisplayOptions && /* @__PURE__ */ (0, import_jsx_runtime34.jsx)(
2396
- "div",
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,
2397
2782
  {
2398
- className: "flex items-center gap-1",
2399
- role: "group",
2400
- "aria-label": "Tree display options",
2401
- children: !isRotationControlled && viewMode !== "stacked" && viewMode !== "outline" && /* @__PURE__ */ (0, import_jsx_runtime34.jsx)(
2402
- import_ui17.Button,
2403
- {
2404
- variant: allowWheelRotation ? "default" : "outline",
2405
- size: "sm",
2406
- onClick: toggleWheelRotation,
2407
- children: "Rotation"
2408
- }
2409
- )
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
2410
2790
  }
2411
2791
  ),
2412
- !isLoading && !readOnly && showAddRoot && /* @__PURE__ */ (0, import_jsx_runtime34.jsx)(
2413
- import_ui17.IconTextButton,
2414
- {
2415
- icon: import_lucide_react12.Plus,
2416
- text: "Add root",
2417
- onClick: () => handleGenreCreationAction(null)
2418
- }
2419
- )
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
+ ] })
2420
2834
  ] });
2421
- return /* @__PURE__ */ (0, import_jsx_runtime34.jsxs)(
2835
+ return /* @__PURE__ */ (0, import_jsx_runtime38.jsxs)(
2422
2836
  "div",
2423
2837
  {
2424
2838
  ref: rootRef,
2425
- 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(" "),
2426
2840
  children: [
2427
- /* @__PURE__ */ (0, import_jsx_runtime34.jsx)("style", { children: PREVIOUS_BUTTON_CSS }),
2428
- /* @__PURE__ */ (0, import_jsx_runtime34.jsx)(
2841
+ /* @__PURE__ */ (0, import_jsx_runtime38.jsx)("style", { children: PREVIOUS_BUTTON_CSS }),
2842
+ /* @__PURE__ */ (0, import_jsx_runtime38.jsx)(
2429
2843
  "div",
2430
2844
  {
2431
- className: `actions-container absolute z-30 flex items-center gap-2 ${placement === "regions" ? "left-[calc(var(--layout-gutter)+var(--layout-panel-inset))] top-[calc(var(--layout-gutter)+var(--layout-panel-inset))]" : "left-5 top-5"}`,
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"}`,
2432
2846
  children: actions
2433
2847
  }
2434
2848
  ),
2435
- detailTab && /* @__PURE__ */ (0, import_jsx_runtime34.jsx)(
2849
+ !isLoading && filterArtist && /* @__PURE__ */ (0, import_jsx_runtime38.jsx)(
2436
2850
  "div",
2437
2851
  {
2438
- className: `absolute top-1/2 z-30 -translate-y-1/2 transition-[opacity,visibility] duration-150 ease-out ${isTreeReady ? "opacity-100" : "pointer-events-none invisible opacity-0"} ${detailExpanded ? "right-[calc(var(--layout-center-right)-var(--layout-edge-tab-w))]" : isCollapsed ? "left-0" : "left-[calc(var(--layout-gutter)+var(--layout-left-w))]"}`,
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)(
2857
+ "div",
2858
+ {
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))]"}`,
2439
2860
  children: detailTab
2440
2861
  }
2441
2862
  ),
2442
- /* @__PURE__ */ (0, import_jsx_runtime34.jsx)("div", { className: "content-container flex-1 min-h-0 flex flex-row gap-4", children: /* @__PURE__ */ (0, import_jsx_runtime34.jsx)("div", { className: "tree-view-container flex-1 min-w-0 flex flex-col h-full", children: isLoading ? /* @__PURE__ */ (0, import_jsx_runtime34.jsx)(import_genre_tree_view4.GenreTreeViewSkeleton, { viewMode }) : viewMode === "wheel" ? (
2443
- // The info panel's top edge aligns with the tree's; the search bar floats `--layout-panel-inset` below it,
2444
- // and the header inset (the bar's height plus that inset) extends the header's fill behind the bar,
2445
- // Google Maps style; the 8px bottom padding mirrors the inset above the bar, and the hidden title's height is zeroed.
2446
- // The panel's own Zoom in button is hidden: the search bar carries it.
2447
- /* @__PURE__ */ (0, import_jsx_runtime34.jsx)("div", { className: "tree-container search-overlay relative flex-1 w-full min-h-0 [--gtv-info-panel-header-inset:calc(var(--layout-search-h,2.5rem)+var(--layout-panel-inset,0.5rem))] [&_.gtv-info-panel-header]:pb-2 [&_.gtv-info-panel-title--hidden]:h-0 [&_.gtv-toolbar]:hidden", children: /* @__PURE__ */ (0, import_jsx_runtime34.jsx)(GenreTreeWheelHandoff, { skeleton: /* @__PURE__ */ (0, import_jsx_runtime34.jsx)(import_genre_tree_view4.GenreTreeWheelSkeleton, {}), onRevealedChange: setIsWheelRevealed, children: /* @__PURE__ */ (0, import_jsx_runtime34.jsx)(
2448
- GenrePlaylistTreeWheel,
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,
2449
2933
  {
2934
+ outline: true,
2450
2935
  scope,
2451
2936
  genrePlaylists: visibleGenrePlaylists,
2452
2937
  handleGenreCreationAction,
@@ -2460,100 +2945,45 @@ function GenreTreeView({
2460
2945
  playback,
2461
2946
  selectedNodeId,
2462
2947
  readOnly,
2463
- allowWheelRotation,
2464
- controlsOrientation,
2465
- sizeMetric: filterArtist ? "songs" : deferredSizeMetric,
2466
- busy,
2467
- floatingControlsLeading: filterArtist ? void 0 : /* @__PURE__ */ (0, import_jsx_runtime34.jsx)(SizeMetricToggle, { value: sizeMetric, onChange: changeSizeMetric }),
2468
- defaultViewport,
2469
- onViewportChange,
2470
2948
  ...infoPanelCuration
2471
2949
  }
2472
- ) }) })
2473
- ) : viewMode === "pop-core" ? (
2474
- // Same as the wheel: the header inset extends its fill behind the floating search bar, Google Maps style.
2475
- /* @__PURE__ */ (0, import_jsx_runtime34.jsx)("div", { className: "tree-container search-overlay relative flex-1 w-full min-h-0 [--gtv-info-panel-header-inset:calc(var(--layout-search-h,2.5rem)+var(--layout-panel-inset,0.5rem))] [&_.gtv-info-panel-header]:pb-2 [&_.gtv-info-panel-title--hidden]:h-0 [&_.gtv-toolbar]:hidden [&_.gtv-info-panel-zoom-in]:hidden", children: /* @__PURE__ */ (0, import_jsx_runtime34.jsx)(GenreTreeWheelHandoff, { skeleton: /* @__PURE__ */ (0, import_jsx_runtime34.jsx)(import_genre_tree_view4.GenreTreeWheelSkeleton, {}), onRevealedChange: setIsWheelRevealed, children: /* @__PURE__ */ (0, import_jsx_runtime34.jsx)(
2476
- GenrePlaylistTreeWheelRadialPopCore,
2477
- {
2478
- scope,
2479
- genrePlaylists: visibleGenrePlaylists,
2480
- handleGenreCreationAction,
2481
- handleGenreRenameAction,
2482
- additionalActions,
2483
- onSelectedNodeChange: handleSelectedNodeChange,
2484
- hideInfoPanelClose: true,
2485
- hideInfoPanelTitle: true,
2486
- onNodeHover: handleNodeHover,
2487
- renderInfoPanelBody,
2488
- playback,
2489
- selectedNodeId,
2490
- readOnly,
2491
- allowWheelRotation,
2492
- controlsOrientation,
2493
- sizeMetric: filterArtist ? "songs" : deferredSizeMetric,
2494
- busy,
2495
- floatingControlsLeading: filterArtist ? void 0 : /* @__PURE__ */ (0, import_jsx_runtime34.jsx)(SizeMetricToggle, { value: sizeMetric, onChange: changeSizeMetric }),
2496
- defaultViewport,
2497
- onViewportChange,
2498
- ...infoPanelCuration,
2499
- ...focus
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
+ );
2500
2984
  }
2501
- ) }) })
2502
- ) : viewMode === "outline" ? /* @__PURE__ */ (0, import_jsx_runtime34.jsx)("div", { className: "tree-container flex-1 min-h-0 w-full overflow-y-auto relative [--gtv-info-panel-header-inset:calc(var(--layout-search-h,2.5rem)+var(--layout-panel-inset,0.5rem)+0.75rem)] [&_.gtv-toolbar]:hidden", children: /* @__PURE__ */ (0, import_jsx_runtime34.jsx)(
2503
- GenrePlaylistTreeWheelRadialPopCore,
2504
- {
2505
- outline: true,
2506
- scope,
2507
- genrePlaylists: visibleGenrePlaylists,
2508
- handleGenreCreationAction,
2509
- handleGenreRenameAction,
2510
- additionalActions,
2511
- onSelectedNodeChange: handleSelectedNodeChange,
2512
- hideInfoPanelClose: true,
2513
- hideInfoPanelTitle: true,
2514
- onNodeHover: handleNodeHover,
2515
- renderInfoPanelBody,
2516
- playback,
2517
- selectedNodeId,
2518
- readOnly,
2519
- ...infoPanelCuration
2520
- }
2521
- ) }) : /* @__PURE__ */ (0, import_jsx_runtime34.jsx)("div", { className: "tree-container relative flex flex-col gap-4 w-full pt-14 overflow-x-auto overflow-y-auto text-[var(--color-text)] [&_.gtv-toolbar]:hidden", children: Object.entries(groupedGenrePlaylistsByRoot).map(
2522
- ([uuid, genrePlaylistTreePerRoot], rootIndex) => {
2523
- return /* @__PURE__ */ (0, import_jsx_runtime34.jsx)(
2524
- "div",
2525
- {
2526
- className: "tree-per-root-container relative shrink-0 h-[28rem] mt-2 mr-16 p-2 bg-[var(--color-neutral-100)] rounded-lg",
2527
- children: /* @__PURE__ */ (0, import_jsx_runtime34.jsx)("div", { className: "graph-container relative z-10 h-full", children: /* @__PURE__ */ (0, import_jsx_runtime34.jsx)(
2528
- GenrePlaylistTreePerRoot,
2529
- {
2530
- scope,
2531
- rootUuid: uuid,
2532
- genrePlaylistTreePerRoot,
2533
- handleGenreCreationAction,
2534
- handleGenreRenameAction,
2535
- additionalActions,
2536
- onSelectedNodeChange: handleSelectedNodeChange,
2537
- hideInfoPanelClose: true,
2538
- hideInfoPanelTitle: true,
2539
- onNodeHover: handleNodeHover,
2540
- renderInfoPanelBody,
2541
- playback,
2542
- selectedNodeId: uuid === selectedGenrePlaylist?.root.uuid ? selectedNodeId : null,
2543
- readOnly,
2544
- wheelZoom: "modifier",
2545
- controlsOrientation,
2546
- defaultViewport: uuid === selectedGenrePlaylist?.root.uuid ? defaultViewport : void 0,
2547
- onViewportChange: uuid === selectedGenrePlaylist?.root.uuid ? onViewportChange : void 0,
2548
- ...infoPanelCuration,
2549
- overlayPanel: rootIndex === 0 ? activeOverlayPanel : void 0
2550
- }
2551
- ) })
2552
- },
2553
- uuid
2554
- );
2555
- }
2556
- ) }) }) })
2985
+ ) })
2986
+ ] }) })
2557
2987
  ]
2558
2988
  }
2559
2989
  );
@@ -2563,24 +2993,35 @@ function GenreTreeView({
2563
2993
  var import_genre_tree_view5 = require("@behindthemusictree/genre-tree-view");
2564
2994
 
2565
2995
  // src/genre-tree/track-list-sidebar/TrackListSidebar.tsx
2566
- 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");
2567
2999
  var import_genre_tree19 = require("@behindthemusictree/app-kit/genre-tree");
2568
- var import_genre_tree20 = require("@behindthemusictree/app-kit/genre-tree");
2569
- var import_genre_tree21 = require("@behindthemusictree/app-kit/genre-tree");
2570
3000
  var import_track_noun7 = require("@behindthemusictree/app-kit/track-noun");
2571
- var import_jsx_runtime35 = 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;
2572
3003
  function TrackListSidebar({
2573
- className = "",
3004
+ className: className2 = "",
2574
3005
  renderDuration,
2575
3006
  renderActions,
2576
3007
  layout = "fixed",
3008
+ isCollapsed = false,
3009
+ onExpand,
3010
+ onMinimize,
2577
3011
  renderArtistLink
2578
3012
  }) {
2579
- const { trackList, loadMore } = (0, import_genre_tree19.useTrackList)();
3013
+ const { trackList, loadMore, autoAdvanceScrollRequest } = (0, import_genre_tree18.useTrackList)();
2580
3014
  const trackNoun = (0, import_track_noun7.useTrackNoun)();
2581
- 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);
2582
3023
  const hasMore = !!trackList && trackList.nextPage !== null;
2583
- (0, import_react11.useEffect)(() => {
3024
+ (0, import_react14.useEffect)(() => {
2584
3025
  const sentinel = loadMoreSentinelRef.current;
2585
3026
  if (!hasMore || !sentinel) return;
2586
3027
  const observer = new IntersectionObserver((entries) => {
@@ -2591,44 +3032,115 @@ function TrackListSidebar({
2591
3032
  observer.observe(sentinel);
2592
3033
  return () => observer.disconnect();
2593
3034
  }, [hasMore, loadMore]);
2594
- const { hideTrackListSidebar } = (0, import_genre_tree20.useTrackListSidebarVisibility)();
2595
- const positionClasses = layout === "inline" ? "relative w-full flex h-full flex-col" : "fixed right-0 w-[50rem]";
2596
- return trackList ? /* @__PURE__ */ (0, import_jsx_runtime35.jsxs)(
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)(
2597
3103
  "div",
2598
3104
  {
2599
3105
  "data-theme": "dark",
2600
- className: `track-list-sidebar ${positionClasses} rounded-2xl bg-[var(--color-surface)] pb-1 ${className}`,
2601
- style: layout === "fixed" ? {
2602
- bottom: "79px"
2603
- /* Match player height exactly */
2604
- } : void 0,
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,
2605
3108
  children: [
2606
- /* @__PURE__ */ (0, import_jsx_runtime35.jsxs)("div", { className: "header flex h-16 px-4 py-2 text-[var(--color-text)]", children: [
2607
- /* @__PURE__ */ (0, import_jsx_runtime35.jsxs)("div", { className: "origin flex text-xl ", children: [
2608
- /* @__PURE__ */ (0, import_jsx_runtime35.jsx)("div", { className: "from h-auto flex flex-col justify-center items-center mr-2", children: "From" }),
2609
- /* @__PURE__ */ (0, import_jsx_runtime35.jsx)("div", { className: "name-container flex flex-col justify-center items-center text-[var(--color-text)] font-bold pr-2 max-w-[27.5rem]", children: /* @__PURE__ */ (0, import_jsx_runtime35.jsx)("div", { className: "name max-w-full truncate", children: trackList.origin.label }) })
2610
- ] }),
2611
- /* @__PURE__ */ (0, import_jsx_runtime35.jsxs)("div", { className: "info flex flex-col justify-center items-center pt-1 mr-2 text-[var(--color-text-muted)]", children: [
2612
- trackList.origin.type === import_genre_tree21.TrackListOriginType.GENRE_PLAYLIST ? "\u2022 Genre playlist \u2022 " : trackList.origin.type === import_genre_tree21.TrackListOriginType.ARTIST ? "\u2022 Artist \u2022 " : `\u2022 ${trackNoun.singular} playlist \u2022 `,
2613
- `${trackList.total} ${trackList.total > 1 ? trackNoun.plural : trackNoun.singular} \u2022`
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
+ ] })
2614
3119
  ] }),
2615
- /* @__PURE__ */ (0, import_jsx_runtime35.jsx)(
3120
+ (isCollapsed && onExpand || !isCollapsed && onMinimize) && /* @__PURE__ */ (0, import_jsx_runtime39.jsx)(
2616
3121
  "button",
2617
3122
  {
2618
3123
  type: "button",
2619
- "aria-label": "Close track list",
2620
- className: "flex-grow flex flex-col items-end justify-center h-full p-3 cursor-pointer text-inherit",
2621
- onClick: hideTrackListSidebar,
2622
- children: "\u2715"
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" })
2623
3130
  }
2624
3131
  )
2625
3132
  ] }),
2626
- /* @__PURE__ */ (0, import_jsx_runtime35.jsxs)(
3133
+ !isCollapsed && /* @__PURE__ */ (0, import_jsx_runtime39.jsxs)(
2627
3134
  "ul",
2628
3135
  {
2629
- className: layout === "inline" ? "track-list flex-1 min-h-0 overflow-auto list-none p-0 m-0" : "track-list overflow-auto max-h-[calc(100vh-63.5px-79px-56px-3.5px)] list-none p-0 m-0",
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",
2630
3142
  children: [
2631
- trackList.tracks.map((track, index) => /* @__PURE__ */ (0, import_jsx_runtime35.jsx)("li", { children: /* @__PURE__ */ (0, import_jsx_runtime35.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)(
2632
3144
  TrackItem,
2633
3145
  {
2634
3146
  track,
@@ -2638,7 +3150,7 @@ function TrackListSidebar({
2638
3150
  renderArtistLink
2639
3151
  }
2640
3152
  ) }, track.uuid)),
2641
- hasMore && /* @__PURE__ */ (0, import_jsx_runtime35.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" })
2642
3154
  ]
2643
3155
  }
2644
3156
  )
@@ -2648,42 +3160,162 @@ function TrackListSidebar({
2648
3160
  }
2649
3161
 
2650
3162
  // src/genre-tree/PlayerPanel.tsx
2651
- var import_react12 = require("react");
2652
- var import_lucide_react13 = require("lucide-react");
3163
+ var import_react15 = require("react");
2653
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");
2654
3171
  var import_player11 = require("@behindthemusictree/app-kit/player");
2655
- var import_genre_tree22 = require("@behindthemusictree/app-kit/genre-tree");
2656
- var import_jsx_runtime36 = 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");
2657
3280
  var PANEL_ID = "player-panel";
2658
- var iconButtonClassName = "flex h-6 w-6 shrink-0 items-center justify-center p-0 rounded-md hover:bg-white/10";
2659
3281
  var DEFAULT_EXTENDED_WIDTH = "calc(100vw - var(--layout-center-left) - var(--layout-gutter))";
2660
3282
  function PlayerPanel({
2661
- className,
3283
+ className: className2,
2662
3284
  isVisible = true,
2663
- onClose,
2664
3285
  expanded,
2665
3286
  onExpandedChange,
2666
3287
  extendedWidth = DEFAULT_EXTENDED_WIDTH,
2667
3288
  renderArtistLink
2668
3289
  }) {
2669
- const { playerTrackObject } = (0, import_player11.usePlayer)();
2670
- const { toggleTrackListSidebar, isTrackListSidebarVisible } = (0, import_genre_tree22.useTrackListSidebarVisibility)();
2671
- 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);
2672
3298
  const isExtendRequested = expanded ?? uncontrolledExtended;
2673
3299
  const setIsExtendRequested = (next) => {
2674
3300
  setUncontrolledExtended(next);
2675
3301
  onExpandedChange?.(next);
2676
3302
  };
2677
3303
  const isExtended = isExtendRequested && isTrackListSidebarVisible;
2678
- const tabEdge = !isTrackListSidebarVisible ? null : isExtended ? {
2679
- label: "Shrink song list panel",
2680
- onClick: () => setIsExtendRequested(false)
2681
- } : { label: "Hide song list", onClick: toggleTrackListSidebar };
2682
- const tabCenter = !isTrackListSidebarVisible ? { label: "Show song list", onClick: toggleTrackListSidebar } : isExtended ? null : {
2683
- label: "Extend song list panel",
2684
- onClick: () => setIsExtendRequested(true)
3304
+ const hideSongList = () => {
3305
+ setIsExtendRequested(false);
3306
+ if (isTrackListSidebarVisible) toggleTrackListSidebar();
2685
3307
  };
2686
- return /* @__PURE__ */ (0, import_jsx_runtime36.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)(
2687
3319
  "div",
2688
3320
  {
2689
3321
  id: PANEL_ID,
@@ -2692,11 +3324,11 @@ function PlayerPanel({
2692
3324
  !isExtended && "w-[var(--layout-right-w)]",
2693
3325
  isTrackListSidebarVisible && "h-[calc(100vh-var(--layout-band-top)-3.5rem)]",
2694
3326
  !isVisible && "hidden",
2695
- className
3327
+ className2
2696
3328
  ),
2697
3329
  style: isExtended ? { width: extendedWidth } : void 0,
2698
3330
  children: [
2699
- /* @__PURE__ */ (0, import_jsx_runtime36.jsx)(
3331
+ /* @__PURE__ */ (0, import_jsx_runtime41.jsx)(
2700
3332
  PanelEdgeTab,
2701
3333
  {
2702
3334
  side: "right",
@@ -2706,7 +3338,7 @@ function PlayerPanel({
2706
3338
  className: "absolute right-full top-1/2 -translate-y-1/2"
2707
3339
  }
2708
3340
  ),
2709
- /* @__PURE__ */ (0, import_jsx_runtime36.jsxs)(
3341
+ /* @__PURE__ */ (0, import_jsx_runtime41.jsxs)(
2710
3342
  "div",
2711
3343
  {
2712
3344
  className: (0, import_tailwind_merge2.twMerge)(
@@ -2714,29 +3346,21 @@ function PlayerPanel({
2714
3346
  isExtended ? "flex-row" : "flex-col"
2715
3347
  ),
2716
3348
  children: [
2717
- /* @__PURE__ */ (0, import_jsx_runtime36.jsxs)("div", { className: "flex w-[var(--layout-right-w)] shrink-0 flex-col", children: [
2718
- /* @__PURE__ */ (0, import_jsx_runtime36.jsxs)("div", { className: "flex shrink-0 items-center justify-between gap-2 border-b border-zinc-800 p-2", children: [
2719
- /* @__PURE__ */ (0, import_jsx_runtime36.jsx)("span", { className: "min-w-0 truncate text-sm font-semibold", children: playerTrackObject?.track?.title }),
2720
- /* @__PURE__ */ (0, import_jsx_runtime36.jsx)("div", { className: "flex shrink-0 items-center gap-1", children: /* @__PURE__ */ (0, import_jsx_runtime36.jsx)(
2721
- "button",
2722
- {
2723
- type: "button",
2724
- onClick: onClose,
2725
- className: iconButtonClassName,
2726
- "aria-label": "Close player",
2727
- children: /* @__PURE__ */ (0, import_jsx_runtime36.jsx)(import_lucide_react13.X, { size: 16 })
2728
- }
2729
- ) })
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 })
2730
3354
  ] }),
2731
- /* @__PURE__ */ (0, import_jsx_runtime36.jsxs)("div", { className: "relative w-full shrink-0", children: [
2732
- /* @__PURE__ */ (0, import_jsx_runtime36.jsx)(import_player11.PlayerVideoSurface, { className: "aspect-video w-full bg-black" }),
2733
- playerTrackObject?.loadError && /* @__PURE__ */ (0, import_jsx_runtime36.jsx)("span", { className: "absolute bottom-2 left-2 right-2 text-xs text-red-400", children: playerTrackObject.loadError })
2734
- ] })
3355
+ /* @__PURE__ */ (0, import_jsx_runtime41.jsx)(PlayerControls, {})
2735
3356
  ] }),
2736
- isTrackListSidebarVisible && /* @__PURE__ */ (0, import_jsx_runtime36.jsx)(
3357
+ /* @__PURE__ */ (0, import_jsx_runtime41.jsx)(
2737
3358
  TrackListSidebar,
2738
3359
  {
2739
3360
  layout: "inline",
3361
+ isCollapsed: !isTrackListSidebarVisible,
3362
+ onExpand: toggleTrackListSidebar,
3363
+ onMinimize: hideSongList,
2740
3364
  className: (0, import_tailwind_merge2.twMerge)(
2741
3365
  "min-h-0 min-w-0 flex-1",
2742
3366
  isExtended && "border-l border-zinc-800"
@@ -2755,33 +3379,33 @@ function PlayerPanel({
2755
3379
  // src/genre-tree/Rating.tsx
2756
3380
  var import_uuid = require("uuid");
2757
3381
  var import_ai = require("react-icons/ai");
2758
- var import_genre_tree23 = require("@behindthemusictree/app-kit/genre-tree");
2759
- var import_jsx_runtime37 = require("react/jsx-runtime");
3382
+ var import_genre_tree22 = require("@behindthemusictree/app-kit/genre-tree");
3383
+ var import_jsx_runtime42 = require("react/jsx-runtime");
2760
3384
  function Rating({ rating, handleChange }) {
2761
3385
  const instanceId = (0, import_uuid.v4)();
2762
- return /* @__PURE__ */ (0, import_jsx_runtime37.jsx)("div", { className: "flex items-center", children: [...Array(6)].map((_, i) => /* @__PURE__ */ (0, import_jsx_runtime37.jsxs)("div", { className: `relative ${i === 0 ? "mr-4" : "mr-1"}`, children: [
2763
- /* @__PURE__ */ (0, import_jsx_runtime37.jsx)(
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)(
2764
3388
  "input",
2765
3389
  {
2766
3390
  id: `star-${instanceId}-${i}`,
2767
3391
  className: "w-6 h-6 opacity-0",
2768
3392
  type: "radio",
2769
3393
  name: "rating",
2770
- value: i === 0 ? import_genre_tree23.FORM_RATING_NULL_VALUE : i,
3394
+ value: i === 0 ? import_genre_tree22.FORM_RATING_NULL_VALUE : i,
2771
3395
  checked: rating === (i === 0 ? null : i),
2772
3396
  onChange: handleChange
2773
3397
  }
2774
3398
  ),
2775
- /* @__PURE__ */ (0, import_jsx_runtime37.jsx)("label", { htmlFor: `star-${instanceId}-${i}`, className: "cursor-pointer", children: i === 0 ? /* @__PURE__ */ (0, import_jsx_runtime37.jsxs)("div", { className: "flex", children: [
2776
- /* @__PURE__ */ (0, import_jsx_runtime37.jsx)(
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)(
2777
3401
  import_ai.AiOutlineStar,
2778
3402
  {
2779
- 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)]"}`
2780
3404
  }
2781
3405
  ),
2782
- /* @__PURE__ */ (0, import_jsx_runtime37.jsx)("span", { className: "flex-grow w-8" }),
3406
+ /* @__PURE__ */ (0, import_jsx_runtime42.jsx)("span", { className: "flex-grow w-8" }),
2783
3407
  " "
2784
- ] }) : /* @__PURE__ */ (0, import_jsx_runtime37.jsx)(
3408
+ ] }) : /* @__PURE__ */ (0, import_jsx_runtime42.jsx)(
2785
3409
  import_ai.AiFillStar,
2786
3410
  {
2787
3411
  className: `w-7 h-7 absolute top-0 left-0 ${(rating ?? -1) >= i ? "text-[var(--color-accent-rating)]" : "text-[var(--color-text-muted)]"}`
@@ -2810,6 +3434,7 @@ function Rating({ rating, handleChange }) {
2810
3434
  PopupTitle,
2811
3435
  Rating,
2812
3436
  SpotifyAuthErrorPopup,
3437
+ Toast,
2813
3438
  TrackItem,
2814
3439
  TrackListSidebar,
2815
3440
  TrackPositionPlayPause,