@behindthemusictree/app-kit-ui 34.0.0 → 35.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.cjs CHANGED
@@ -587,13 +587,13 @@ function AuthCallbackHandler({ renderStatus, ...options }) {
587
587
  }
588
588
 
589
589
  // src/genre-tree/GenreTreeView.tsx
590
- var import_react16 = require("react");
590
+ var import_react18 = require("react");
591
591
  var import_react_query3 = require("@tanstack/react-query");
592
- var import_lucide_react15 = require("lucide-react");
593
592
  var import_lucide_react16 = require("lucide-react");
594
- var import_ui19 = require("@behindthemusictree/ui");
595
- var import_genre_tree_view4 = require("@behindthemusictree/genre-tree-view");
596
- var import_genre_tree27 = require("@behindthemusictree/app-kit/genre-tree");
593
+ var import_lucide_react17 = require("lucide-react");
594
+ var import_ui20 = require("@behindthemusictree/ui");
595
+ var import_genre_tree_view5 = require("@behindthemusictree/genre-tree-view");
596
+ var import_genre_tree28 = require("@behindthemusictree/app-kit/genre-tree");
597
597
  var import_popup6 = require("@behindthemusictree/app-kit/popup");
598
598
 
599
599
  // src/genre-tree/playlist-tree/size-metric.ts
@@ -1027,6 +1027,9 @@ function GenreTreeWheelHandoff({ skeleton, children, onRevealedChange }) {
1027
1027
  ] });
1028
1028
  }
1029
1029
 
1030
+ // src/genre-tree/GenreTreeSearch.tsx
1031
+ var import_react9 = require("react");
1032
+
1030
1033
  // src/genre-tree/GenreSearch.tsx
1031
1034
  var import_lucide_react7 = require("lucide-react");
1032
1035
  var import_ui7 = require("@behindthemusictree/ui");
@@ -1227,6 +1230,52 @@ function GenreSearch({
1227
1230
  ) });
1228
1231
  }
1229
1232
 
1233
+ // src/genre-tree/GenreTreeSearch.tsx
1234
+ var import_jsx_runtime21 = require("react/jsx-runtime");
1235
+ function GenreTreeSearch({
1236
+ artistBaseUrl,
1237
+ songBaseUrl,
1238
+ onArtistSelect,
1239
+ onSongSelect,
1240
+ artistSearch,
1241
+ songSearch,
1242
+ SearchGroups,
1243
+ onSelectGenre,
1244
+ extraGroups = [],
1245
+ onQueryChange,
1246
+ onFocus,
1247
+ ...props
1248
+ }) {
1249
+ const [query, setQuery] = (0, import_react9.useState)("");
1250
+ const [focused, setFocused] = (0, import_react9.useState)(false);
1251
+ const deferredQuery = (0, import_react9.useDeferredValue)(query);
1252
+ const artistGroup = useArtistSearchGroup(artistBaseUrl, deferredQuery, onArtistSelect, artistSearch);
1253
+ const songGroup = useSongSearchGroup(songBaseUrl, deferredQuery, onSongSelect, songSearch);
1254
+ const render = (hostGroups) => /* @__PURE__ */ (0, import_jsx_runtime21.jsx)(
1255
+ GenreSearch,
1256
+ {
1257
+ ...props,
1258
+ onQueryChange: (text) => {
1259
+ setQuery(text);
1260
+ onQueryChange?.(text);
1261
+ },
1262
+ onFocus: () => {
1263
+ setFocused(true);
1264
+ onFocus?.();
1265
+ },
1266
+ extraGroups: [...extraGroups, ...hostGroups, artistGroup, songGroup].filter((group) => group !== void 0)
1267
+ }
1268
+ );
1269
+ return SearchGroups ? /* @__PURE__ */ (0, import_jsx_runtime21.jsx)(SearchGroups, { query: deferredQuery, focused, onSelectGenre, children: render }) : render([]);
1270
+ }
1271
+
1272
+ // src/genre-tree/GenreDetailPanel.tsx
1273
+ var import_react14 = require("react");
1274
+ var import_genre_tree_view4 = require("@behindthemusictree/genre-tree-view");
1275
+ var import_ui10 = require("@behindthemusictree/ui");
1276
+ var import_lucide_react12 = require("lucide-react");
1277
+ var import_genre_tree19 = require("@behindthemusictree/app-kit/genre-tree");
1278
+
1230
1279
  // src/genre-tree/GenreInfoPanelBody.tsx
1231
1280
  var import_genre_tree18 = require("@behindthemusictree/app-kit/genre-tree");
1232
1281
  var import_track_noun4 = require("@behindthemusictree/app-kit/track-noun");
@@ -1240,18 +1289,18 @@ var import_lucide_react10 = require("lucide-react");
1240
1289
  var import_popup4 = require("@behindthemusictree/app-kit/popup");
1241
1290
 
1242
1291
  // src/genre-tree/SharePopup.tsx
1243
- var import_react9 = require("react");
1292
+ var import_react10 = require("react");
1244
1293
  var import_lucide_react9 = require("lucide-react");
1245
1294
  var import_fa3 = require("react-icons/fa");
1246
1295
  var import_si = require("react-icons/si");
1247
1296
 
1248
1297
  // src/genre-tree/ShareDestinationButton.tsx
1249
- var import_jsx_runtime21 = require("react/jsx-runtime");
1298
+ var import_jsx_runtime22 = require("react/jsx-runtime");
1250
1299
  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";
1251
1300
  function Contents({ icon, label }) {
1252
- return /* @__PURE__ */ (0, import_jsx_runtime21.jsxs)(import_jsx_runtime21.Fragment, { children: [
1253
- /* @__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 }),
1254
- /* @__PURE__ */ (0, import_jsx_runtime21.jsx)("span", { className: "max-w-full truncate", children: label })
1301
+ return /* @__PURE__ */ (0, import_jsx_runtime22.jsxs)(import_jsx_runtime22.Fragment, { children: [
1302
+ /* @__PURE__ */ (0, import_jsx_runtime22.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 }),
1303
+ /* @__PURE__ */ (0, import_jsx_runtime22.jsx)("span", { className: "max-w-full truncate", children: label })
1255
1304
  ] });
1256
1305
  }
1257
1306
  function ShareDestinationButton({
@@ -1263,29 +1312,29 @@ function ShareDestinationButton({
1263
1312
  ariaLabel
1264
1313
  }) {
1265
1314
  if (href) {
1266
- 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 }) });
1315
+ return /* @__PURE__ */ (0, import_jsx_runtime22.jsx)("a", { href, target: "_blank", rel: "noopener noreferrer", "aria-label": ariaLabel, className, children: /* @__PURE__ */ (0, import_jsx_runtime22.jsx)(Contents, { icon, label }) });
1267
1316
  }
1268
- 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 }) });
1317
+ return /* @__PURE__ */ (0, import_jsx_runtime22.jsx)("button", { type: "button", onClick, disabled, "aria-label": ariaLabel, className, children: /* @__PURE__ */ (0, import_jsx_runtime22.jsx)(Contents, { icon, label }) });
1269
1318
  }
1270
1319
 
1271
1320
  // src/genre-tree/SharePreviewCard.tsx
1272
1321
  var import_lucide_react8 = require("lucide-react");
1273
- var import_jsx_runtime22 = require("react/jsx-runtime");
1322
+ var import_jsx_runtime23 = require("react/jsx-runtime");
1274
1323
  function SharePreviewCard({ target, url }) {
1275
1324
  const Icon = { genre: import_lucide_react8.Music2, artist: import_lucide_react8.UserRound, song: import_lucide_react8.Music }[target.kind];
1276
1325
  const typeLabel = { genre: "Genre", artist: "Artist", song: "Song" }[target.kind];
1277
- 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: [
1278
- /* @__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" }) }),
1279
- /* @__PURE__ */ (0, import_jsx_runtime22.jsxs)("div", { className: "flex min-w-0 flex-1 flex-col justify-center gap-1 p-4", children: [
1280
- /* @__PURE__ */ (0, import_jsx_runtime22.jsx)("span", { className: "text-xs font-medium uppercase tracking-wide text-[var(--color-text-muted)]", children: typeLabel }),
1281
- /* @__PURE__ */ (0, import_jsx_runtime22.jsx)("span", { className: "truncate text-base font-semibold text-[var(--color-text)]", title: target.name, children: target.name }),
1282
- /* @__PURE__ */ (0, import_jsx_runtime22.jsx)("span", { className: "truncate text-sm text-[var(--color-primary)]", title: url, children: url })
1326
+ return /* @__PURE__ */ (0, import_jsx_runtime23.jsx)("div", { className: "rounded-3xl bg-[var(--color-surface-muted)] p-4", children: /* @__PURE__ */ (0, import_jsx_runtime23.jsxs)("div", { className: "flex min-h-32 overflow-hidden rounded-2xl bg-[var(--color-surface)] shadow-lg", children: [
1327
+ /* @__PURE__ */ (0, import_jsx_runtime23.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_runtime23.jsx)(Icon, { "aria-hidden": "true", className: "h-10 w-10" }) }),
1328
+ /* @__PURE__ */ (0, import_jsx_runtime23.jsxs)("div", { className: "flex min-w-0 flex-1 flex-col justify-center gap-1 p-4", children: [
1329
+ /* @__PURE__ */ (0, import_jsx_runtime23.jsx)("span", { className: "text-xs font-medium uppercase tracking-wide text-[var(--color-text-muted)]", children: typeLabel }),
1330
+ /* @__PURE__ */ (0, import_jsx_runtime23.jsx)("span", { className: "truncate text-base font-semibold text-[var(--color-text)]", title: target.name, children: target.name }),
1331
+ /* @__PURE__ */ (0, import_jsx_runtime23.jsx)("span", { className: "truncate text-sm text-[var(--color-primary)]", title: url, children: url })
1283
1332
  ] })
1284
1333
  ] }) });
1285
1334
  }
1286
1335
 
1287
1336
  // src/genre-tree/SharePopup.tsx
1288
- var import_jsx_runtime23 = require("react/jsx-runtime");
1337
+ var import_jsx_runtime24 = require("react/jsx-runtime");
1289
1338
  function resolveAbsoluteUrl(url) {
1290
1339
  try {
1291
1340
  const parsed = new URL(url, window.location.href);
@@ -1318,20 +1367,20 @@ async function copyText(text) {
1318
1367
  }
1319
1368
  }
1320
1369
  function SharePopup({ target, url, onClose }) {
1321
- const [absoluteUrl, setAbsoluteUrl] = (0, import_react9.useState)(url);
1322
- const [copyStatus, setCopyStatus] = (0, import_react9.useState)("idle");
1323
- const [canUseNativeShare, setCanUseNativeShare] = (0, import_react9.useState)(false);
1324
- const [shareError, setShareError] = (0, import_react9.useState)(null);
1325
- const [toastId, setToastId] = (0, import_react9.useState)(0);
1326
- (0, import_react9.useEffect)(() => {
1370
+ const [absoluteUrl, setAbsoluteUrl] = (0, import_react10.useState)(url);
1371
+ const [copyStatus, setCopyStatus] = (0, import_react10.useState)("idle");
1372
+ const [canUseNativeShare, setCanUseNativeShare] = (0, import_react10.useState)(false);
1373
+ const [shareError, setShareError] = (0, import_react10.useState)(null);
1374
+ const [toastId, setToastId] = (0, import_react10.useState)(0);
1375
+ (0, import_react10.useEffect)(() => {
1327
1376
  setAbsoluteUrl(resolveAbsoluteUrl(url) ?? "");
1328
1377
  setCopyStatus("idle");
1329
1378
  setShareError(null);
1330
1379
  }, [url]);
1331
- (0, import_react9.useEffect)(() => {
1380
+ (0, import_react10.useEffect)(() => {
1332
1381
  setCanUseNativeShare(typeof navigator !== "undefined" && typeof navigator.share === "function");
1333
1382
  }, []);
1334
- const destinationUrls = (0, import_react9.useMemo)(() => {
1383
+ const destinationUrls = (0, import_react10.useMemo)(() => {
1335
1384
  if (!absoluteUrl) return null;
1336
1385
  const message = `${target.name} \u2014 ${absoluteUrl}`;
1337
1386
  return {
@@ -1349,7 +1398,7 @@ function SharePopup({ target, url, onClose }) {
1349
1398
  setCopyStatus(copied ? "idle" : "failed");
1350
1399
  if (copied) setToastId((id) => id + 1);
1351
1400
  };
1352
- const dismissToast = (0, import_react9.useCallback)(() => setToastId(0), []);
1401
+ const dismissToast = (0, import_react10.useCallback)(() => setToastId(0), []);
1353
1402
  const handleNativeShare = async () => {
1354
1403
  if (!absoluteUrl || !navigator.share) return;
1355
1404
  setShareError(null);
@@ -1360,8 +1409,8 @@ function SharePopup({ target, url, onClose }) {
1360
1409
  setShareError("Could not open the share options.");
1361
1410
  }
1362
1411
  };
1363
- return /* @__PURE__ */ (0, import_jsx_runtime23.jsxs)(import_jsx_runtime23.Fragment, { children: [
1364
- /* @__PURE__ */ (0, import_jsx_runtime23.jsx)(
1412
+ return /* @__PURE__ */ (0, import_jsx_runtime24.jsxs)(import_jsx_runtime24.Fragment, { children: [
1413
+ /* @__PURE__ */ (0, import_jsx_runtime24.jsx)(
1365
1414
  BasePopup_default,
1366
1415
  {
1367
1416
  title: "Share",
@@ -1376,67 +1425,67 @@ function SharePopup({ target, url, onClose }) {
1376
1425
  titleTextClassName: "!text-xl !font-semibold !text-[var(--color-text)]",
1377
1426
  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)]",
1378
1427
  contentClassName: "!rounded-none !bg-transparent !px-6 !pb-6 !pt-2",
1379
- children: /* @__PURE__ */ (0, import_jsx_runtime23.jsxs)("div", { className: "flex flex-col gap-4", children: [
1380
- /* @__PURE__ */ (0, import_jsx_runtime23.jsx)(SharePreviewCard, { target, url: absoluteUrl || url }),
1381
- /* @__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: [
1382
- /* @__PURE__ */ (0, import_jsx_runtime23.jsx)(
1428
+ children: /* @__PURE__ */ (0, import_jsx_runtime24.jsxs)("div", { className: "flex flex-col gap-4", children: [
1429
+ /* @__PURE__ */ (0, import_jsx_runtime24.jsx)(SharePreviewCard, { target, url: absoluteUrl || url }),
1430
+ /* @__PURE__ */ (0, import_jsx_runtime24.jsxs)("div", { className: "grid grid-cols-3 gap-1 rounded-3xl bg-[var(--color-surface)] p-3 sm:grid-cols-4", children: [
1431
+ /* @__PURE__ */ (0, import_jsx_runtime24.jsx)(
1383
1432
  ShareDestinationButton,
1384
1433
  {
1385
1434
  label: "Copy Link",
1386
- icon: /* @__PURE__ */ (0, import_jsx_runtime23.jsx)(import_lucide_react9.Link2, { "aria-hidden": "true", className: "h-6 w-6" }),
1435
+ icon: /* @__PURE__ */ (0, import_jsx_runtime24.jsx)(import_lucide_react9.Link2, { "aria-hidden": "true", className: "h-6 w-6" }),
1387
1436
  onClick: () => void handleCopy(),
1388
1437
  ariaLabel: "Copy link",
1389
1438
  disabled: !absoluteUrl
1390
1439
  }
1391
1440
  ),
1392
- /* @__PURE__ */ (0, import_jsx_runtime23.jsx)(
1441
+ /* @__PURE__ */ (0, import_jsx_runtime24.jsx)(
1393
1442
  ShareDestinationButton,
1394
1443
  {
1395
1444
  label: "WhatsApp",
1396
- icon: /* @__PURE__ */ (0, import_jsx_runtime23.jsx)(import_fa3.FaWhatsapp, { "aria-hidden": "true", className: "h-9 w-9 text-[#25D366]" }),
1445
+ icon: /* @__PURE__ */ (0, import_jsx_runtime24.jsx)(import_fa3.FaWhatsapp, { "aria-hidden": "true", className: "h-9 w-9 text-[#25D366]" }),
1397
1446
  href: destinationUrls?.whatsapp,
1398
1447
  ariaLabel: `Share ${target.name} via WhatsApp`,
1399
1448
  disabled: !absoluteUrl
1400
1449
  }
1401
1450
  ),
1402
- /* @__PURE__ */ (0, import_jsx_runtime23.jsx)(
1451
+ /* @__PURE__ */ (0, import_jsx_runtime24.jsx)(
1403
1452
  ShareDestinationButton,
1404
1453
  {
1405
1454
  label: "Gmail",
1406
- icon: /* @__PURE__ */ (0, import_jsx_runtime23.jsx)(import_si.SiGmail, { "aria-hidden": "true", className: "h-8 w-8 text-[#EA4335]" }),
1455
+ icon: /* @__PURE__ */ (0, import_jsx_runtime24.jsx)(import_si.SiGmail, { "aria-hidden": "true", className: "h-8 w-8 text-[#EA4335]" }),
1407
1456
  href: destinationUrls?.gmail,
1408
1457
  ariaLabel: `Email ${target.name} with Gmail`,
1409
1458
  disabled: !absoluteUrl
1410
1459
  }
1411
1460
  ),
1412
- canUseNativeShare && /* @__PURE__ */ (0, import_jsx_runtime23.jsx)(
1461
+ canUseNativeShare && /* @__PURE__ */ (0, import_jsx_runtime24.jsx)(
1413
1462
  ShareDestinationButton,
1414
1463
  {
1415
1464
  label: "More",
1416
- icon: /* @__PURE__ */ (0, import_jsx_runtime23.jsx)(import_lucide_react9.Share2, { "aria-hidden": "true", className: "h-6 w-6" }),
1465
+ icon: /* @__PURE__ */ (0, import_jsx_runtime24.jsx)(import_lucide_react9.Share2, { "aria-hidden": "true", className: "h-6 w-6" }),
1417
1466
  onClick: () => void handleNativeShare(),
1418
1467
  ariaLabel: "More sharing options",
1419
1468
  disabled: !absoluteUrl
1420
1469
  }
1421
1470
  )
1422
1471
  ] }),
1423
- 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." }),
1424
- shareError && /* @__PURE__ */ (0, import_jsx_runtime23.jsx)("p", { role: "alert", className: "text-center text-sm text-[var(--color-red-500)]", children: shareError })
1472
+ copyStatus === "failed" && /* @__PURE__ */ (0, import_jsx_runtime24.jsx)("p", { role: "alert", className: "text-center text-sm text-[var(--color-red-500)]", children: "Could not copy the link." }),
1473
+ shareError && /* @__PURE__ */ (0, import_jsx_runtime24.jsx)("p", { role: "alert", className: "text-center text-sm text-[var(--color-red-500)]", children: shareError })
1425
1474
  ] })
1426
1475
  }
1427
1476
  ),
1428
- toastId > 0 && /* @__PURE__ */ (0, import_jsx_runtime23.jsx)(Toast, { message: "Link copied", onClose: dismissToast }, toastId)
1477
+ toastId > 0 && /* @__PURE__ */ (0, import_jsx_runtime24.jsx)(Toast, { message: "Link copied", onClose: dismissToast }, toastId)
1429
1478
  ] });
1430
1479
  }
1431
1480
 
1432
1481
  // src/genre-tree/ShareActionButton.tsx
1433
- var import_jsx_runtime24 = require("react/jsx-runtime");
1482
+ var import_jsx_runtime25 = require("react/jsx-runtime");
1434
1483
  function ShareActionButton({ target, url }) {
1435
1484
  const { showPopup, hidePopup } = (0, import_popup4.usePopup)();
1436
1485
  const openSharePopup = () => {
1437
- showPopup(/* @__PURE__ */ (0, import_jsx_runtime24.jsx)(SharePopup, { target, url, onClose: hidePopup }));
1486
+ showPopup(/* @__PURE__ */ (0, import_jsx_runtime25.jsx)(SharePopup, { target, url, onClose: hidePopup }));
1438
1487
  };
1439
- return /* @__PURE__ */ (0, import_jsx_runtime24.jsxs)(
1488
+ return /* @__PURE__ */ (0, import_jsx_runtime25.jsxs)(
1440
1489
  "button",
1441
1490
  {
1442
1491
  type: "button",
@@ -1445,7 +1494,7 @@ function ShareActionButton({ target, url }) {
1445
1494
  "aria-label": `Share ${target.name}`,
1446
1495
  title: `Share ${target.name}`,
1447
1496
  children: [
1448
- /* @__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" }) }),
1497
+ /* @__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)]", children: /* @__PURE__ */ (0, import_jsx_runtime25.jsx)(import_lucide_react10.Share2, { "aria-hidden": "true", className: "w-5 h-5" }) }),
1449
1498
  "Share"
1450
1499
  ]
1451
1500
  }
@@ -1453,7 +1502,7 @@ function ShareActionButton({ target, url }) {
1453
1502
  }
1454
1503
 
1455
1504
  // src/genre-tree/GenreActionBand.tsx
1456
- var import_jsx_runtime25 = require("react/jsx-runtime");
1505
+ var import_jsx_runtime26 = require("react/jsx-runtime");
1457
1506
  function GenreActionBand({
1458
1507
  node,
1459
1508
  onPlayPause,
@@ -1475,7 +1524,7 @@ function GenreActionBand({
1475
1524
  items.push({
1476
1525
  key: "play-pause",
1477
1526
  label: isPlaying ? "Pause" : "Play",
1478
- 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" }),
1527
+ icon: isPlaying ? /* @__PURE__ */ (0, import_jsx_runtime26.jsx)(import_lucide_react11.Pause, { "aria-hidden": "true", className: "w-5 h-5" }) : /* @__PURE__ */ (0, import_jsx_runtime26.jsx)(import_lucide_react11.Play, { "aria-hidden": "true", className: "w-5 h-5" }),
1479
1528
  onClick: onPlayPause
1480
1529
  });
1481
1530
  }
@@ -1483,24 +1532,24 @@ function GenreActionBand({
1483
1532
  items.push({
1484
1533
  key: "zoom-in",
1485
1534
  label: "Focus",
1486
- icon: /* @__PURE__ */ (0, import_jsx_runtime25.jsx)(import_lucide_react11.ZoomIn, { "aria-hidden": "true", className: "w-5 h-5" }),
1535
+ icon: /* @__PURE__ */ (0, import_jsx_runtime26.jsx)(import_lucide_react11.ZoomIn, { "aria-hidden": "true", className: "w-5 h-5" }),
1487
1536
  onClick: onZoomIn
1488
1537
  });
1489
1538
  }
1490
1539
  if (onAddChild) {
1491
- 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 });
1540
+ adminItems.push({ key: "add-child", label: "Add", icon: /* @__PURE__ */ (0, import_jsx_runtime26.jsx)(import_lucide_react11.Plus, { "aria-hidden": "true", className: "w-5 h-5" }), onClick: onAddChild });
1492
1541
  }
1493
1542
  if (onRename) {
1494
- 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 });
1543
+ adminItems.push({ key: "rename", label: "Rename", icon: /* @__PURE__ */ (0, import_jsx_runtime26.jsx)(import_lucide_react11.Pencil, { "aria-hidden": "true", className: "w-5 h-5" }), onClick: onRename });
1495
1544
  }
1496
1545
  if (onChangeParent) {
1497
- 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 });
1546
+ adminItems.push({ key: "change-parent", label: "Parent", icon: /* @__PURE__ */ (0, import_jsx_runtime26.jsx)(import_lucide_react11.MoveUpRight, { "aria-hidden": "true", className: "w-5 h-5" }), onClick: onChangeParent });
1498
1547
  }
1499
1548
  if (onToggleSide) {
1500
1549
  adminItems.push({
1501
1550
  key: "toggle-side",
1502
1551
  label: side === "pop" ? "Make core" : "Make pop",
1503
- icon: /* @__PURE__ */ (0, import_jsx_runtime25.jsx)(import_lucide_react11.ArrowLeftRight, { "aria-hidden": "true", className: "w-5 h-5" }),
1552
+ icon: /* @__PURE__ */ (0, import_jsx_runtime26.jsx)(import_lucide_react11.ArrowLeftRight, { "aria-hidden": "true", className: "w-5 h-5" }),
1504
1553
  onClick: onToggleSide,
1505
1554
  disabled: !!toggleSideDisabledReason,
1506
1555
  title: toggleSideDisabledReason
@@ -1516,7 +1565,7 @@ function GenreActionBand({
1516
1565
  });
1517
1566
  }
1518
1567
  if (items.length === 0 && (!shareTarget || !shareUrl) && adminItems.length === 0) return null;
1519
- const renderItems = (list) => list.map((item) => /* @__PURE__ */ (0, import_jsx_runtime25.jsxs)(
1568
+ const renderItems = (list) => list.map((item) => /* @__PURE__ */ (0, import_jsx_runtime26.jsxs)(
1520
1569
  "button",
1521
1570
  {
1522
1571
  type: "button",
@@ -1525,25 +1574,25 @@ function GenreActionBand({
1525
1574
  onClick: item.onClick,
1526
1575
  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",
1527
1576
  children: [
1528
- /* @__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 }),
1577
+ /* @__PURE__ */ (0, import_jsx_runtime26.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 }),
1529
1578
  item.label
1530
1579
  ]
1531
1580
  },
1532
1581
  item.key
1533
1582
  ));
1534
- return /* @__PURE__ */ (0, import_jsx_runtime25.jsxs)("div", { className: "genre-action-band border-b border-[var(--color-border)]", children: [
1535
- (items.length > 0 || shareTarget && shareUrl) && /* @__PURE__ */ (0, import_jsx_runtime25.jsxs)("div", { className: "flex flex-wrap gap-1 px-3.5 py-3", children: [
1583
+ return /* @__PURE__ */ (0, import_jsx_runtime26.jsxs)("div", { className: "genre-action-band border-b border-[var(--color-border)]", children: [
1584
+ (items.length > 0 || shareTarget && shareUrl) && /* @__PURE__ */ (0, import_jsx_runtime26.jsxs)("div", { className: "flex flex-wrap gap-1 px-3.5 py-3", children: [
1536
1585
  renderItems(items),
1537
- shareTarget && shareUrl && /* @__PURE__ */ (0, import_jsx_runtime25.jsx)(ShareActionButton, { target: shareTarget, url: shareUrl })
1586
+ shareTarget && shareUrl && /* @__PURE__ */ (0, import_jsx_runtime26.jsx)(ShareActionButton, { target: shareTarget, url: shareUrl })
1538
1587
  ] }),
1539
- adminItems.length > 0 && /* @__PURE__ */ (0, import_jsx_runtime25.jsxs)(
1588
+ adminItems.length > 0 && /* @__PURE__ */ (0, import_jsx_runtime26.jsxs)(
1540
1589
  "section",
1541
1590
  {
1542
1591
  "aria-label": "Admin actions",
1543
1592
  className: `px-3.5 py-3 ${items.length > 0 ? "border-t border-[var(--color-border)]" : ""}`,
1544
1593
  children: [
1545
- /* @__PURE__ */ (0, import_jsx_runtime25.jsx)("h3", { className: "mb-2 text-xs font-semibold uppercase tracking-wide text-[var(--color-text-muted)]", children: "Admin" }),
1546
- /* @__PURE__ */ (0, import_jsx_runtime25.jsx)("div", { className: "flex flex-wrap gap-1", children: renderItems(adminItems) })
1594
+ /* @__PURE__ */ (0, import_jsx_runtime26.jsx)("h3", { className: "mb-2 text-xs font-semibold uppercase tracking-wide text-[var(--color-text-muted)]", children: "Admin" }),
1595
+ /* @__PURE__ */ (0, import_jsx_runtime26.jsx)("div", { className: "flex flex-wrap gap-1", children: renderItems(adminItems) })
1547
1596
  ]
1548
1597
  }
1549
1598
  )
@@ -1555,20 +1604,20 @@ var import_ui9 = require("@behindthemusictree/ui");
1555
1604
  var import_genre_tree17 = require("@behindthemusictree/app-kit/genre-tree");
1556
1605
 
1557
1606
  // src/genre-tree/ClampedText.tsx
1558
- var import_react10 = require("react");
1559
- var import_jsx_runtime26 = require("react/jsx-runtime");
1607
+ var import_react11 = require("react");
1608
+ var import_jsx_runtime27 = require("react/jsx-runtime");
1560
1609
  function ClampedText({ children, className: className2 = "" }) {
1561
- const ref = (0, import_react10.useRef)(null);
1562
- const [expanded, setExpanded] = (0, import_react10.useState)(false);
1563
- const [overflows, setOverflows] = (0, import_react10.useState)(false);
1564
- (0, import_react10.useLayoutEffect)(() => {
1610
+ const ref = (0, import_react11.useRef)(null);
1611
+ const [expanded, setExpanded] = (0, import_react11.useState)(false);
1612
+ const [overflows, setOverflows] = (0, import_react11.useState)(false);
1613
+ (0, import_react11.useLayoutEffect)(() => {
1565
1614
  const element = ref.current;
1566
1615
  if (!element || expanded) return;
1567
1616
  setOverflows(element.scrollHeight > element.clientHeight);
1568
1617
  }, [children, expanded]);
1569
- return /* @__PURE__ */ (0, import_jsx_runtime26.jsxs)(import_jsx_runtime26.Fragment, { children: [
1570
- /* @__PURE__ */ (0, import_jsx_runtime26.jsx)("p", { ref, className: `${className2} ${expanded ? "" : "line-clamp-3"}`, children }),
1571
- (overflows || expanded) && /* @__PURE__ */ (0, import_jsx_runtime26.jsx)(
1618
+ return /* @__PURE__ */ (0, import_jsx_runtime27.jsxs)(import_jsx_runtime27.Fragment, { children: [
1619
+ /* @__PURE__ */ (0, import_jsx_runtime27.jsx)("p", { ref, className: `${className2} ${expanded ? "" : "line-clamp-3"}`, children }),
1620
+ (overflows || expanded) && /* @__PURE__ */ (0, import_jsx_runtime27.jsx)(
1572
1621
  "button",
1573
1622
  {
1574
1623
  type: "button",
@@ -1582,7 +1631,7 @@ function ClampedText({ children, className: className2 = "" }) {
1582
1631
  }
1583
1632
 
1584
1633
  // src/genre-tree/GenrePlaylistTracks.tsx
1585
- var import_react11 = require("react");
1634
+ var import_react12 = require("react");
1586
1635
  var import_react_query = require("@tanstack/react-query");
1587
1636
  var import_zod = require("zod");
1588
1637
  var import_fa4 = require("react-icons/fa");
@@ -1603,15 +1652,15 @@ var import_genre_tree15 = require("@behindthemusictree/app-kit/genre-tree");
1603
1652
 
1604
1653
  // src/genre-tree/MusicBrainzRecordingLink.tsx
1605
1654
  var import_genre_tree10 = require("@behindthemusictree/app-kit/genre-tree");
1606
- var import_jsx_runtime27 = require("react/jsx-runtime");
1655
+ var import_jsx_runtime28 = require("react/jsx-runtime");
1607
1656
  function MusicBrainzRecordingLink({
1608
1657
  song,
1609
1658
  children,
1610
1659
  className: className2 = ""
1611
1660
  }) {
1612
1661
  const url = (0, import_genre_tree10.musicBrainzRecordingUrl)(song);
1613
- if (!url) return /* @__PURE__ */ (0, import_jsx_runtime27.jsx)(import_jsx_runtime27.Fragment, { children });
1614
- return /* @__PURE__ */ (0, import_jsx_runtime27.jsx)(
1662
+ if (!url) return /* @__PURE__ */ (0, import_jsx_runtime28.jsx)(import_jsx_runtime28.Fragment, { children });
1663
+ return /* @__PURE__ */ (0, import_jsx_runtime28.jsx)(
1615
1664
  "a",
1616
1665
  {
1617
1666
  href: url,
@@ -1628,7 +1677,7 @@ function MusicBrainzRecordingLink({
1628
1677
  // src/genre-tree/GenrePlaylistTracks.tsx
1629
1678
  var import_genre_tree16 = require("@behindthemusictree/app-kit/genre-tree");
1630
1679
  var import_track_noun2 = require("@behindthemusictree/app-kit/track-noun");
1631
- var import_jsx_runtime28 = require("react/jsx-runtime");
1680
+ var import_jsx_runtime29 = require("react/jsx-runtime");
1632
1681
  var CATALOGUE_PAGE_SIZE = 100;
1633
1682
  var SongPageSchema = (0, import_transport6.PaginatedResponseSchema)(import_genre_tree14.SongDetailedSchema).extend({
1634
1683
  overallTotal: import_zod.z.number().nullable(),
@@ -1648,6 +1697,7 @@ var nextCursor = (next) => {
1648
1697
  function GenrePlaylistTracks({
1649
1698
  genreUuid,
1650
1699
  genrePlaylist,
1700
+ genreName: selectedGenreName,
1651
1701
  selection,
1652
1702
  scope,
1653
1703
  getBackendBaseUrl,
@@ -1663,13 +1713,17 @@ function GenrePlaylistTracks({
1663
1713
  const { playerTrackObject, playState, handlePlayPauseAction } = (0, import_player4.usePlayer)();
1664
1714
  const { showPopup } = (0, import_popup5.usePopup)();
1665
1715
  const trackNoun = (0, import_track_noun2.useTrackNoun)();
1666
- const { uuid } = genrePlaylist;
1716
+ const uuid = genrePlaylist?.uuid;
1717
+ const genreName = selectedGenreName ?? genrePlaylist?.name ?? "Genre";
1667
1718
  const { fetch } = (0, import_transport5.useFetchWrapper)(getBackendBaseUrl);
1668
1719
  const playback = (0, import_transport5.usePlaybackContext)();
1669
1720
  const { data, isPending, isError, refetch, hasNextPage, isFetchingNextPage, fetchNextPage } = (0, import_react_query.useInfiniteQuery)({
1670
- queryKey: [...scope === "me" ? import_genre_tree12.genrePlaylistQueryKeys.me.tracks(uuid) : artist ? [...import_genre_tree13.libraryQueryKeys.reference.songs.byGenre(getBackendBaseUrl(), genreUuid, selection), "artist", artist.uuid] : import_genre_tree13.libraryQueryKeys.reference.songs.byGenre(getBackendBaseUrl(), genreUuid, selection), ...playback.key ? [playback.key] : []],
1721
+ queryKey: [...scope === "me" ? uuid ? import_genre_tree12.genrePlaylistQueryKeys.me.tracks(uuid) : ["missing-personal-genre-playlist", genreUuid] : artist ? [...import_genre_tree13.libraryQueryKeys.reference.songs.byGenre(getBackendBaseUrl(), genreUuid, selection), "artist", artist.uuid] : import_genre_tree13.libraryQueryKeys.reference.songs.byGenre(getBackendBaseUrl(), genreUuid, selection), ...playback.key ? [playback.key] : []],
1671
1722
  queryFn: async ({ pageParam }) => {
1672
- if (scope === "me") return fetchGenrePlaylistTracksPage(uuid, scope, Number(pageParam));
1723
+ if (scope === "me") {
1724
+ if (!uuid) throw new Error("Personal genre has no playlist");
1725
+ return fetchGenrePlaylistTracksPage(uuid, scope, Number(pageParam));
1726
+ }
1673
1727
  const response = await fetch(
1674
1728
  import_genre_tree13.libraryEndpoints.reference.songs.list(),
1675
1729
  true,
@@ -1682,10 +1736,10 @@ function GenrePlaylistTracks({
1682
1736
  },
1683
1737
  initialPageParam: scope === "me" ? 1 : "",
1684
1738
  getNextPageParam: (lastPage) => lastPage.nextPage ?? void 0,
1685
- enabled: playback.ready && (scope !== "me" || sessionRestored && !!session?.accessToken)
1739
+ enabled: playback.ready && (scope !== "me" || uuid !== void 0 && sessionRestored && !!session?.accessToken)
1686
1740
  });
1687
- const [sentinel, setSentinel] = (0, import_react11.useState)(null);
1688
- (0, import_react11.useEffect)(() => {
1741
+ const [sentinel, setSentinel] = (0, import_react12.useState)(null);
1742
+ (0, import_react12.useEffect)(() => {
1689
1743
  if (!hasNextPage || isFetchingNextPage || !sentinel) return;
1690
1744
  const observer = new IntersectionObserver((entries) => {
1691
1745
  if (entries.some((entry) => entry.isIntersecting)) {
@@ -1704,38 +1758,38 @@ function GenrePlaylistTracks({
1704
1758
  ordering: "title",
1705
1759
  genreUuid,
1706
1760
  ...selection && { selection }
1707
- }) : scope === "reference" ? playNewTrackListFromGenreCatalogue({ uuid: genreUuid, name: genrePlaylist.name }, scope, { startSongUuid, ...selection && { selection } }) : playNewTrackListFromGenrePlaylist(genrePlaylist, scope, { startSongUuid });
1761
+ }) : scope === "reference" ? playNewTrackListFromGenreCatalogue({ uuid: genreUuid, name: genreName }, scope, { startSongUuid, ...selection && { selection } }) : genrePlaylist ? playNewTrackListFromGenrePlaylist(genrePlaylist, scope, { startSongUuid }) : Promise.reject(new Error("Personal genre has no playlist"));
1708
1762
  started.catch((error) => {
1709
1763
  console.error("Failed to play genre playlist:", error);
1710
- showPopup(/* @__PURE__ */ (0, import_jsx_runtime28.jsx)(InternalErrorPopup, { errorCode: import_transport8.ErrorCode.CLIENT_UNKNOWN }));
1764
+ showPopup(/* @__PURE__ */ (0, import_jsx_runtime29.jsx)(InternalErrorPopup, { errorCode: import_transport8.ErrorCode.CLIENT_UNKNOWN }));
1711
1765
  });
1712
1766
  };
1713
- return /* @__PURE__ */ (0, import_jsx_runtime28.jsx)(import_ui8.PanelSection, { title: `${(0, import_track_noun2.capitalize)(trackNoun.plural)}${total !== void 0 ? ` (${total})` : ""}`, children: /* @__PURE__ */ (0, import_jsx_runtime28.jsxs)("div", { children: [
1767
+ return /* @__PURE__ */ (0, import_jsx_runtime29.jsx)(import_ui8.PanelSection, { title: `${(0, import_track_noun2.capitalize)(trackNoun.plural)}${total !== void 0 ? ` (${total})` : ""}`, children: /* @__PURE__ */ (0, import_jsx_runtime29.jsxs)("div", { children: [
1714
1768
  renderTracksToolbar?.(),
1715
- isPending ? /* @__PURE__ */ (0, import_jsx_runtime28.jsx)(import_ui8.Skeleton, { className: "h-4 w-3/4", "aria-busy": "true" }) : isError && tracks.length === 0 ? /* @__PURE__ */ (0, import_jsx_runtime28.jsxs)("p", { role: "alert", children: [
1769
+ scope === "me" && !genrePlaylist ? /* @__PURE__ */ (0, import_jsx_runtime29.jsx)("p", { children: "\u2014" }) : isPending ? /* @__PURE__ */ (0, import_jsx_runtime29.jsx)(import_ui8.Skeleton, { className: "h-4 w-3/4", "aria-busy": "true" }) : isError && tracks.length === 0 ? /* @__PURE__ */ (0, import_jsx_runtime29.jsxs)("p", { role: "alert", children: [
1716
1770
  "Couldn't load ",
1717
1771
  trackNoun.plural,
1718
1772
  ".",
1719
1773
  " ",
1720
- /* @__PURE__ */ (0, import_jsx_runtime28.jsx)("button", { type: "button", className: "underline", onClick: () => void refetch(), children: "Retry" })
1721
- ] }) : tracks.length === 0 ? /* @__PURE__ */ (0, import_jsx_runtime28.jsx)("p", { children: "\u2014" }) : /* @__PURE__ */ (0, import_jsx_runtime28.jsxs)("ul", { className: "mt-1 max-h-64 overflow-y-auto text-[0.8125rem] leading-[1.45] [container-type:inline-size]", "aria-label": `${genrePlaylist.name} ${trackNoun.plural}`, children: [
1774
+ /* @__PURE__ */ (0, import_jsx_runtime29.jsx)("button", { type: "button", className: "underline", onClick: () => void refetch(), children: "Retry" })
1775
+ ] }) : tracks.length === 0 ? /* @__PURE__ */ (0, import_jsx_runtime29.jsx)("p", { children: "\u2014" }) : /* @__PURE__ */ (0, import_jsx_runtime29.jsxs)("ul", { className: "mt-1 max-h-64 overflow-y-auto text-[0.8125rem] leading-[1.45] [container-type:inline-size]", "aria-label": `${genreName} ${trackNoun.plural}`, children: [
1722
1776
  tracks.map((track, index) => {
1723
1777
  const label = track.artists?.length ? `${track.title} \u2014 ${track.artists.map((artist2) => artist2.name).join(", ")}` : track.title;
1724
1778
  const isCurrent = playerTrackObject?.track.id === track.uuid;
1725
1779
  const isPlaying = isCurrent && playState === import_player5.PlayStates.PLAYING;
1726
- const text = /* @__PURE__ */ (0, import_jsx_runtime28.jsxs)("span", { className: TEXT_CELL, children: [
1727
- /* @__PURE__ */ (0, import_jsx_runtime28.jsx)("span", { className: TITLE_CELL, children: renderSongLink ? renderSongLink(track) : (0, import_genre_tree14.isSong)(track) ? /* @__PURE__ */ (0, import_jsx_runtime28.jsx)(MusicBrainzRecordingLink, { song: track, className: "hover:underline focus-visible:underline", children: track.title }) : track.title }),
1728
- track.artists?.length ? /* @__PURE__ */ (0, import_jsx_runtime28.jsx)("span", { className: SEPARATOR, children: " \u2014 " }) : null,
1729
- /* @__PURE__ */ (0, import_jsx_runtime28.jsx)("span", { className: COLUMN_CELL, children: track.artists?.length ? /* @__PURE__ */ (0, import_jsx_runtime28.jsx)(import_genre_tree16.ArtistNames, { artists: track.artists, renderArtistLink }) : null }),
1730
- track.album && /* @__PURE__ */ (0, import_jsx_runtime28.jsx)("span", { className: ALBUM_CELL, children: track.album.name })
1780
+ const text = /* @__PURE__ */ (0, import_jsx_runtime29.jsxs)("span", { className: TEXT_CELL, children: [
1781
+ /* @__PURE__ */ (0, import_jsx_runtime29.jsx)("span", { className: TITLE_CELL, children: renderSongLink ? renderSongLink(track) : (0, import_genre_tree14.isSong)(track) ? /* @__PURE__ */ (0, import_jsx_runtime29.jsx)(MusicBrainzRecordingLink, { song: track, className: "hover:underline focus-visible:underline", children: track.title }) : track.title }),
1782
+ track.artists?.length ? /* @__PURE__ */ (0, import_jsx_runtime29.jsx)("span", { className: SEPARATOR, children: " \u2014 " }) : null,
1783
+ /* @__PURE__ */ (0, import_jsx_runtime29.jsx)("span", { className: COLUMN_CELL, children: track.artists?.length ? /* @__PURE__ */ (0, import_jsx_runtime29.jsx)(import_genre_tree16.ArtistNames, { artists: track.artists, renderArtistLink }) : null }),
1784
+ track.album && /* @__PURE__ */ (0, import_jsx_runtime29.jsx)("span", { className: ALBUM_CELL, children: track.album.name })
1731
1785
  ] });
1732
1786
  const leading = renderSongLeading?.(track, index);
1733
1787
  const actions = renderSongActions?.(track);
1734
1788
  const isUnplayable = (0, import_genre_tree14.isSong)(track) && (0, import_genre_tree14.songUnplayableReason)(track) !== null;
1735
1789
  if (isUnplayable) {
1736
- return /* @__PURE__ */ (0, import_jsx_runtime28.jsxs)("li", { className: "flex min-h-8 items-center gap-1 px-1 opacity-50", children: [
1790
+ return /* @__PURE__ */ (0, import_jsx_runtime29.jsxs)("li", { className: "flex min-h-8 items-center gap-1 px-1 opacity-50", children: [
1737
1791
  leading,
1738
- /* @__PURE__ */ (0, import_jsx_runtime28.jsx)(
1792
+ /* @__PURE__ */ (0, import_jsx_runtime29.jsx)(
1739
1793
  "button",
1740
1794
  {
1741
1795
  type: "button",
@@ -1743,27 +1797,27 @@ function GenrePlaylistTracks({
1743
1797
  "aria-disabled": "true",
1744
1798
  "aria-label": label,
1745
1799
  className: "flex h-6 w-6 shrink-0 items-center justify-center rounded-full opacity-0 cursor-not-allowed",
1746
- children: /* @__PURE__ */ (0, import_jsx_runtime28.jsx)(import_fa4.FaPlay, { size: 10 })
1800
+ children: /* @__PURE__ */ (0, import_jsx_runtime29.jsx)(import_fa4.FaPlay, { size: 10 })
1747
1801
  }
1748
1802
  ),
1749
1803
  text,
1750
1804
  actions
1751
1805
  ] }, track.uuid);
1752
1806
  }
1753
- return /* @__PURE__ */ (0, import_jsx_runtime28.jsxs)(
1807
+ return /* @__PURE__ */ (0, import_jsx_runtime29.jsxs)(
1754
1808
  "li",
1755
1809
  {
1756
1810
  className: `group flex min-h-8 items-center gap-1 rounded px-1 hover:bg-[var(--color-neutral-100)] ${isCurrent ? "font-medium text-[var(--color-primary)]" : ""}`,
1757
1811
  children: [
1758
1812
  leading,
1759
- /* @__PURE__ */ (0, import_jsx_runtime28.jsx)(
1813
+ /* @__PURE__ */ (0, import_jsx_runtime29.jsx)(
1760
1814
  "button",
1761
1815
  {
1762
1816
  type: "button",
1763
1817
  "aria-label": `${isPlaying ? "Pause" : "Play"} ${label}`,
1764
1818
  className: `flex h-6 w-6 shrink-0 items-center justify-center rounded-full cursor-pointer group-hover:opacity-100 focus-visible:opacity-100 ${isCurrent && playState !== import_player5.PlayStates.STOPPED ? "opacity-100" : "opacity-0 [@media(pointer:coarse)]:opacity-100"}`,
1765
1819
  onClick: () => isCurrent ? handlePlayPauseAction() : play(track.uuid),
1766
- children: isCurrent && playState === import_player5.PlayStates.LOADING ? /* @__PURE__ */ (0, import_jsx_runtime28.jsx)(import_ui8.RingLoader, { size: 10 }) : isPlaying ? /* @__PURE__ */ (0, import_jsx_runtime28.jsx)(import_fa4.FaPause, { size: 10 }) : /* @__PURE__ */ (0, import_jsx_runtime28.jsx)(import_fa4.FaPlay, { size: 10 })
1820
+ children: isCurrent && playState === import_player5.PlayStates.LOADING ? /* @__PURE__ */ (0, import_jsx_runtime29.jsx)(import_ui8.RingLoader, { size: 10 }) : isPlaying ? /* @__PURE__ */ (0, import_jsx_runtime29.jsx)(import_fa4.FaPause, { size: 10 }) : /* @__PURE__ */ (0, import_jsx_runtime29.jsx)(import_fa4.FaPlay, { size: 10 })
1767
1821
  }
1768
1822
  ),
1769
1823
  text,
@@ -1773,7 +1827,7 @@ function GenrePlaylistTracks({
1773
1827
  track.uuid
1774
1828
  );
1775
1829
  }),
1776
- hasNextPage && /* @__PURE__ */ (0, import_jsx_runtime28.jsx)("li", { ref: setSentinel, "aria-hidden": "true", className: "h-4" })
1830
+ hasNextPage && /* @__PURE__ */ (0, import_jsx_runtime29.jsx)("li", { ref: setSentinel, "aria-hidden": "true", className: "h-4" })
1777
1831
  ] })
1778
1832
  ] }) });
1779
1833
  }
@@ -1781,10 +1835,11 @@ function GenrePlaylistTracks({
1781
1835
  // src/genre-tree/GenreDetailExtras.tsx
1782
1836
  var import_track_noun3 = require("@behindthemusictree/app-kit/track-noun");
1783
1837
  var import_si2 = require("react-icons/si");
1784
- var import_jsx_runtime29 = require("react/jsx-runtime");
1838
+ var import_jsx_runtime30 = require("react/jsx-runtime");
1785
1839
  function GenreDetailExtras({
1786
1840
  genreUuid,
1787
1841
  genrePlaylist,
1842
+ genreName,
1788
1843
  selection,
1789
1844
  wikidataId,
1790
1845
  scope,
@@ -1806,17 +1861,17 @@ function GenreDetailExtras({
1806
1861
  criteriaOverviewSchema
1807
1862
  );
1808
1863
  if (isPending) {
1809
- return /* @__PURE__ */ (0, import_jsx_runtime29.jsxs)(import_jsx_runtime29.Fragment, { children: [
1810
- /* @__PURE__ */ (0, import_jsx_runtime29.jsx)(import_ui9.PanelSection, { title: "Summary", children: /* @__PURE__ */ (0, import_jsx_runtime29.jsx)("div", { "aria-busy": "true", "data-testid": "genre-detail-extras-skeleton", children: /* @__PURE__ */ (0, import_jsx_runtime29.jsx)(import_ui9.Skeleton, { className: "h-4 w-3/4" }) }) }),
1864
+ return /* @__PURE__ */ (0, import_jsx_runtime30.jsxs)(import_jsx_runtime30.Fragment, { children: [
1865
+ /* @__PURE__ */ (0, import_jsx_runtime30.jsx)(import_ui9.PanelSection, { title: "Summary", children: /* @__PURE__ */ (0, import_jsx_runtime30.jsx)("div", { "aria-busy": "true", "data-testid": "genre-detail-extras-skeleton", children: /* @__PURE__ */ (0, import_jsx_runtime30.jsx)(import_ui9.Skeleton, { className: "h-4 w-3/4" }) }) }),
1811
1866
  family
1812
1867
  ] });
1813
1868
  }
1814
- if (!overview) return /* @__PURE__ */ (0, import_jsx_runtime29.jsx)(import_jsx_runtime29.Fragment, { children: family });
1869
+ if (!overview) return /* @__PURE__ */ (0, import_jsx_runtime30.jsx)(import_jsx_runtime30.Fragment, { children: family });
1815
1870
  const { summary, summarySourceUrl, essentialSongs } = overview;
1816
1871
  const wikidataUrl = wikidataId ? `https://www.wikidata.org/wiki/${encodeURIComponent(wikidataId)}` : null;
1817
- return /* @__PURE__ */ (0, import_jsx_runtime29.jsxs)(import_jsx_runtime29.Fragment, { children: [
1818
- (summarySourceUrl || wikidataUrl) && /* @__PURE__ */ (0, import_jsx_runtime29.jsx)(import_ui9.PanelSection, { title: "Links", children: /* @__PURE__ */ (0, import_jsx_runtime29.jsxs)("div", { className: "flex flex-wrap gap-x-3 gap-y-1 text-xs text-[var(--color-text)]", children: [
1819
- summarySourceUrl && /* @__PURE__ */ (0, import_jsx_runtime29.jsxs)(
1872
+ return /* @__PURE__ */ (0, import_jsx_runtime30.jsxs)(import_jsx_runtime30.Fragment, { children: [
1873
+ (summarySourceUrl || wikidataUrl) && /* @__PURE__ */ (0, import_jsx_runtime30.jsx)(import_ui9.PanelSection, { title: "Links", children: /* @__PURE__ */ (0, import_jsx_runtime30.jsxs)("div", { className: "flex flex-wrap gap-x-3 gap-y-1 text-xs text-[var(--color-text)]", children: [
1874
+ summarySourceUrl && /* @__PURE__ */ (0, import_jsx_runtime30.jsxs)(
1820
1875
  "a",
1821
1876
  {
1822
1877
  href: summarySourceUrl,
@@ -1824,12 +1879,12 @@ function GenreDetailExtras({
1824
1879
  rel: "noopener noreferrer",
1825
1880
  className: "inline-flex items-center gap-1.5 text-[var(--color-text-muted)] hover:text-[var(--color-text)]",
1826
1881
  children: [
1827
- /* @__PURE__ */ (0, import_jsx_runtime29.jsx)(import_si2.SiWikipedia, { "aria-hidden": "true", className: "h-3.5 w-3.5" }),
1882
+ /* @__PURE__ */ (0, import_jsx_runtime30.jsx)(import_si2.SiWikipedia, { "aria-hidden": "true", className: "h-3.5 w-3.5" }),
1828
1883
  "Wikipedia"
1829
1884
  ]
1830
1885
  }
1831
1886
  ),
1832
- wikidataUrl && /* @__PURE__ */ (0, import_jsx_runtime29.jsxs)(
1887
+ wikidataUrl && /* @__PURE__ */ (0, import_jsx_runtime30.jsxs)(
1833
1888
  "a",
1834
1889
  {
1835
1890
  href: wikidataUrl,
@@ -1837,21 +1892,22 @@ function GenreDetailExtras({
1837
1892
  rel: "noopener noreferrer",
1838
1893
  className: "inline-flex items-center gap-1.5 text-[var(--color-text-muted)] hover:text-[var(--color-text)]",
1839
1894
  children: [
1840
- /* @__PURE__ */ (0, import_jsx_runtime29.jsx)(import_si2.SiWikidata, { "aria-hidden": "true", className: "h-3.5 w-3.5" }),
1895
+ /* @__PURE__ */ (0, import_jsx_runtime30.jsx)(import_si2.SiWikidata, { "aria-hidden": "true", className: "h-3.5 w-3.5" }),
1841
1896
  "Wikidata"
1842
1897
  ]
1843
1898
  }
1844
1899
  )
1845
1900
  ] }) }),
1846
- /* @__PURE__ */ (0, import_jsx_runtime29.jsx)(import_ui9.PanelSection, { title: "Summary", children: renderSummary ? renderSummary(overview) : /* @__PURE__ */ (0, import_jsx_runtime29.jsx)(ClampedText, { className: "text-[0.8125rem] leading-[1.45] text-[var(--color-text)]", children: summary ?? "\u2014" }, genreUuid) }),
1901
+ /* @__PURE__ */ (0, import_jsx_runtime30.jsx)(import_ui9.PanelSection, { title: "Summary", children: renderSummary ? renderSummary(overview) : /* @__PURE__ */ (0, import_jsx_runtime30.jsx)(ClampedText, { className: "text-[0.8125rem] leading-[1.45] text-[var(--color-text)]", children: summary ?? "\u2014" }, genreUuid) }),
1847
1902
  family,
1848
1903
  renderGenreDetailExtras?.(overview),
1849
- (renderEssentialTracks || essentialSongs) && /* @__PURE__ */ (0, import_jsx_runtime29.jsx)(import_ui9.PanelSection, { title: `Essential ${trackNoun.plural}`, children: renderEssentialTracks ? renderEssentialTracks(overview) : essentialSongs && essentialSongs.length > 0 ? /* @__PURE__ */ (0, import_jsx_runtime29.jsx)("ul", { className: "text-[0.8125rem] leading-[1.45] text-[var(--color-text)]", children: essentialSongs.map((track) => /* @__PURE__ */ (0, import_jsx_runtime29.jsx)("li", { className: "py-1", children: track.title }, track.uuid)) }) : /* @__PURE__ */ (0, import_jsx_runtime29.jsx)("p", { className: "text-[0.8125rem] leading-[1.45] text-[var(--color-text)]", children: "\u2014" }) }),
1850
- /* @__PURE__ */ (0, import_jsx_runtime29.jsx)(
1904
+ (renderEssentialTracks || essentialSongs) && /* @__PURE__ */ (0, import_jsx_runtime30.jsx)(import_ui9.PanelSection, { title: `Essential ${trackNoun.plural}`, children: renderEssentialTracks ? renderEssentialTracks(overview) : essentialSongs && essentialSongs.length > 0 ? /* @__PURE__ */ (0, import_jsx_runtime30.jsx)("ul", { className: "text-[0.8125rem] leading-[1.45] text-[var(--color-text)]", children: essentialSongs.map((track) => /* @__PURE__ */ (0, import_jsx_runtime30.jsx)("li", { className: "py-1", children: track.title }, track.uuid)) }) : /* @__PURE__ */ (0, import_jsx_runtime30.jsx)("p", { className: "text-[0.8125rem] leading-[1.45] text-[var(--color-text)]", children: "\u2014" }) }),
1905
+ /* @__PURE__ */ (0, import_jsx_runtime30.jsx)(
1851
1906
  GenrePlaylistTracks,
1852
1907
  {
1853
1908
  genreUuid,
1854
1909
  genrePlaylist,
1910
+ genreName,
1855
1911
  selection,
1856
1912
  scope,
1857
1913
  getBackendBaseUrl,
@@ -1864,8 +1920,8 @@ function GenreDetailExtras({
1864
1920
  }
1865
1921
 
1866
1922
  // src/genre-tree/GenreInfoPanelBody.tsx
1867
- var import_jsx_runtime30 = require("react/jsx-runtime");
1868
- var import_react12 = require("react");
1923
+ var import_jsx_runtime31 = require("react/jsx-runtime");
1924
+ var import_react13 = require("react");
1869
1925
  function GenreNodeActionBand({ genrePlaylist, playback, ...bandProps }) {
1870
1926
  const { isPlaying, setIsPlaying } = (0, import_player6.usePlayer)();
1871
1927
  const isCurrentTrackList = playback.playingNodeId === genrePlaylist.uuid;
@@ -1878,7 +1934,7 @@ function GenreNodeActionBand({ genrePlaylist, playback, ...bandProps }) {
1878
1934
  console.error("Failed to load genre playlist tracks:", error);
1879
1935
  });
1880
1936
  };
1881
- return /* @__PURE__ */ (0, import_jsx_runtime30.jsx)(
1937
+ return /* @__PURE__ */ (0, import_jsx_runtime31.jsx)(
1882
1938
  GenreActionBand,
1883
1939
  {
1884
1940
  ...bandProps,
@@ -1893,6 +1949,7 @@ function GenreInfoPanelBody({
1893
1949
  genreUuid,
1894
1950
  genrePlaylist,
1895
1951
  catalogue,
1952
+ countsContext,
1896
1953
  wikidataId,
1897
1954
  scope,
1898
1955
  getBackendBaseUrl,
@@ -1901,30 +1958,32 @@ function GenreInfoPanelBody({
1901
1958
  extras
1902
1959
  }) {
1903
1960
  const noun = (0, import_track_noun4.useTrackNoun)();
1904
- const summary = scope === "reference" ? import_genre_tree18.GenreCatalogueCountsSchema.parse(genrePlaylist) : void 0;
1961
+ const summary = scope === "reference" ? import_genre_tree18.GenreCatalogueCountsSchema.parse(genrePlaylist ?? catalogue) : void 0;
1905
1962
  const selection = summary ? (0, import_genre_tree18.defaultGenreMusicSelection)(summary) : void 0;
1906
1963
  const describeCounts = (counts) => {
1907
1964
  const scopes = counts.genreAndSubgenresTracksCount;
1908
1965
  return "canonical" in scopes ? `${counts.genreOnlyTracksCount} this genre only \xB7 ${scopes.canonical} with canonical subgenres \xB7 ${scopes.regional} with regional subgenres \xB7 ${scopes.all} with all subgenres` : `${counts.genreOnlyTracksCount} this genre only \xB7 ${scopes.all} with subgenres`;
1909
1966
  };
1910
- return /* @__PURE__ */ (0, import_jsx_runtime30.jsxs)(import_jsx_runtime30.Fragment, { children: [
1967
+ return /* @__PURE__ */ (0, import_jsx_runtime31.jsxs)(import_jsx_runtime31.Fragment, { children: [
1911
1968
  toolbar,
1912
- /* @__PURE__ */ (0, import_jsx_runtime30.jsx)(GenreNodeActionBand, { ...band, genrePlaylist, playback }),
1913
- summary && catalogue && /* @__PURE__ */ (0, import_jsx_runtime30.jsxs)("div", { className: "px-3.5 py-3 text-sm text-[var(--color-text)]", children: [
1914
- /* @__PURE__ */ (0, import_jsx_runtime30.jsxs)("p", { children: [
1969
+ genrePlaylist ? /* @__PURE__ */ (0, import_jsx_runtime31.jsx)(GenreNodeActionBand, { ...band, genrePlaylist, playback }) : /* @__PURE__ */ (0, import_jsx_runtime31.jsx)(GenreActionBand, { ...band }),
1970
+ summary && catalogue && /* @__PURE__ */ (0, import_jsx_runtime31.jsxs)("div", { className: "px-3.5 py-3 text-sm text-[var(--color-text)]", children: [
1971
+ /* @__PURE__ */ (0, import_jsx_runtime31.jsxs)("p", { children: [
1915
1972
  "Catalogue ",
1916
1973
  noun.plural,
1974
+ countsContext ? ` (${countsContext})` : "",
1917
1975
  ": ",
1918
1976
  describeCounts(catalogue)
1919
1977
  ] }),
1920
- /* @__PURE__ */ (0, import_jsx_runtime30.jsxs)("p", { children: [
1978
+ genrePlaylist && /* @__PURE__ */ (0, import_jsx_runtime31.jsxs)("p", { children: [
1921
1979
  "Playlist ",
1922
1980
  noun.plural,
1981
+ countsContext ? ` (${countsContext})` : "",
1923
1982
  ": ",
1924
1983
  describeCounts(summary)
1925
1984
  ] })
1926
1985
  ] }),
1927
- /* @__PURE__ */ (0, import_react12.createElement)(
1986
+ /* @__PURE__ */ (0, import_react13.createElement)(
1928
1987
  GenreDetailExtras,
1929
1988
  {
1930
1989
  ...extras,
@@ -1932,6 +1991,7 @@ function GenreInfoPanelBody({
1932
1991
  family,
1933
1992
  genreUuid,
1934
1993
  genrePlaylist,
1994
+ genreName: band.node.name,
1935
1995
  selection,
1936
1996
  wikidataId,
1937
1997
  scope,
@@ -1941,10 +2001,160 @@ function GenreInfoPanelBody({
1941
2001
  ] });
1942
2002
  }
1943
2003
 
2004
+ // src/genre-tree/GenreDetailPanel.tsx
2005
+ var import_jsx_runtime32 = require("react/jsx-runtime");
2006
+ var RELATIVE_ITEM_COUNT = 1;
2007
+ function toNode(genre, parentId) {
2008
+ return {
2009
+ id: genre.uuid,
2010
+ parentId,
2011
+ name: genre.name,
2012
+ itemCount: RELATIVE_ITEM_COUNT,
2013
+ side: genre.side ?? void 0,
2014
+ wikidataId: genre.wikidataId
2015
+ };
2016
+ }
2017
+ function useGenreDetailContent({
2018
+ genreUuid,
2019
+ scope,
2020
+ getBackendBaseUrl,
2021
+ onSelectGenre,
2022
+ extras,
2023
+ band,
2024
+ playback: hostPlayback,
2025
+ trailing,
2026
+ genrePlaylist: hostPlaylist,
2027
+ expanded
2028
+ }) {
2029
+ const { data: detail, isPending, isError } = (0, import_genre_tree19.useFetchGenreDetail)(genreUuid, scope, getBackendBaseUrl);
2030
+ const defaultPlayback = useGenrePlaylistPlayback({ scope, genrePlaylists: [], artist: extras?.artist });
2031
+ const playback = hostPlayback ?? defaultPlayback;
2032
+ const view = (0, import_react14.useMemo)(() => {
2033
+ if (!detail) return null;
2034
+ const rootColor = (0, import_genre_tree_view4.getGenreTreeColor)(detail.root.wikidataId);
2035
+ const relative = (genre, parentId) => ({
2036
+ node: toNode(genre, parentId),
2037
+ fill: (0, import_genre_tree_view4.tintSurface)(rootColor),
2038
+ textColor: import_genre_tree_view4.TEXT_COLOR
2039
+ });
2040
+ const ancestors2 = [...detail.ascendants].sort((a, b) => b.degree - a.degree);
2041
+ return {
2042
+ node: {
2043
+ id: detail.uuid,
2044
+ parentId: detail.parent?.uuid ?? null,
2045
+ name: detail.name,
2046
+ itemCount: scope === "reference" ? (0, import_genre_tree19.genreMusicCount)(detail.criteriaPlaylist ? import_genre_tree19.GenrePlaylistSummarySchema.parse(detail.criteriaPlaylist) : import_genre_tree19.GenreCatalogueCountsSchema.parse(detail.catalogue)) : detail.tracksCount,
2047
+ wikidataId: detail.wikidataId
2048
+ },
2049
+ fill: rootColor,
2050
+ parentNode: detail.parent ? relative(detail.parent, null) : null,
2051
+ childNodes: detail.children.map((child) => relative(child, detail.uuid)),
2052
+ ancestorNodes: ancestors2.map(
2053
+ ({ ascendant }, index) => relative(ascendant, index === 0 ? null : ancestors2[index - 1].ascendant.uuid)
2054
+ )
2055
+ };
2056
+ }, [detail, scope]);
2057
+ if (isError) return {
2058
+ title: "Genre unavailable",
2059
+ content: /* @__PURE__ */ (0, import_jsx_runtime32.jsx)("p", { role: "alert", className: "p-4 text-sm text-[var(--color-text)]", children: "This genre could not be loaded." })
2060
+ };
2061
+ if (isPending || !detail || !view) return {
2062
+ title: "Loading genre\u2026",
2063
+ content: /* @__PURE__ */ (0, import_jsx_runtime32.jsxs)("div", { className: "flex flex-col gap-3 p-4", "aria-busy": "true", "data-testid": "genre-detail-panel-skeleton", children: [
2064
+ /* @__PURE__ */ (0, import_jsx_runtime32.jsx)(import_ui10.Skeleton, { className: "h-6 w-2/3" }),
2065
+ /* @__PURE__ */ (0, import_jsx_runtime32.jsx)(import_ui10.Skeleton, { className: "h-4 w-full" }),
2066
+ /* @__PURE__ */ (0, import_jsx_runtime32.jsx)(import_ui10.Skeleton, { className: "h-4 w-3/4" })
2067
+ ] })
2068
+ };
2069
+ const selectGenre = (nodeId) => onSelectGenre(import_genre_tree19.GenreUuidSchema.parse(nodeId));
2070
+ const family = /* @__PURE__ */ (0, import_jsx_runtime32.jsx)(
2071
+ import_genre_tree_view4.GenreNodeFamily,
2072
+ {
2073
+ node: view.node,
2074
+ fill: view.fill,
2075
+ textColor: import_genre_tree_view4.ACCENT_TEXT_COLOR,
2076
+ parentNode: view.parentNode,
2077
+ childNodes: view.childNodes,
2078
+ onSelectNode: selectGenre
2079
+ }
2080
+ );
2081
+ const ancestors = /* @__PURE__ */ (0, import_jsx_runtime32.jsx)(import_genre_tree_view4.GenreNodeAncestors, { ancestorNodes: view.ancestorNodes, onSelectNode: selectGenre });
2082
+ return {
2083
+ title: detail.name,
2084
+ headerAccent: { background: view.fill, color: import_genre_tree_view4.ACCENT_TEXT_COLOR },
2085
+ aside: expanded ? ancestors : void 0,
2086
+ content: /* @__PURE__ */ (0, import_jsx_runtime32.jsxs)(import_jsx_runtime32.Fragment, { children: [
2087
+ /* @__PURE__ */ (0, import_jsx_runtime32.jsx)(
2088
+ GenreInfoPanelBody,
2089
+ {
2090
+ family: /* @__PURE__ */ (0, import_jsx_runtime32.jsxs)(import_jsx_runtime32.Fragment, { children: [
2091
+ family,
2092
+ !expanded && ancestors
2093
+ ] }),
2094
+ genreUuid: detail.uuid,
2095
+ genrePlaylist: hostPlaylist ?? (detail.criteriaPlaylist === null ? null : scope === "reference" ? { ...import_genre_tree19.GenrePlaylistSummarySchema.parse(detail.criteriaPlaylist), criteria: import_genre_tree19.CriteriaMinimumSchema.parse(detail) } : { ...detail.criteriaPlaylist, criteria: import_genre_tree19.CriteriaMinimumSchema.parse(detail), tracksCount: detail.tracksCount }),
2096
+ catalogue: detail.catalogue,
2097
+ countsContext: !hostPlaylist && extras?.artist ? "all artists" : void 0,
2098
+ wikidataId: detail.wikidataId,
2099
+ scope,
2100
+ getBackendBaseUrl,
2101
+ playback,
2102
+ band: { node: view.node, ...band, shareTarget: band?.shareTarget?.kind === "genre" ? { ...band.shareTarget, name: detail.name } : band?.shareTarget },
2103
+ extras
2104
+ }
2105
+ ),
2106
+ trailing
2107
+ ] })
2108
+ };
2109
+ }
2110
+ function GenreDetailPanel({
2111
+ genreUuid,
2112
+ scope,
2113
+ getBackendBaseUrl,
2114
+ onClose,
2115
+ onSelectGenre,
2116
+ onZoomIn,
2117
+ shareUrl,
2118
+ shareTarget,
2119
+ collapsed,
2120
+ expanded,
2121
+ onExpandedChange,
2122
+ edgeTab,
2123
+ extras,
2124
+ headerClassName
2125
+ }) {
2126
+ const panel = useGenreDetailContent({
2127
+ genreUuid,
2128
+ scope,
2129
+ getBackendBaseUrl,
2130
+ onSelectGenre,
2131
+ extras,
2132
+ expanded,
2133
+ band: { onZoomIn, shareUrl, shareTarget: shareUrl ? shareTarget : void 0 }
2134
+ });
2135
+ return /* @__PURE__ */ (0, import_jsx_runtime32.jsx)(
2136
+ import_ui10.DetailPanel,
2137
+ {
2138
+ side: "left",
2139
+ title: panel.title,
2140
+ headerAccent: panel.headerAccent,
2141
+ onClose,
2142
+ collapsed,
2143
+ expanded,
2144
+ onExpandedChange,
2145
+ edgeTab,
2146
+ aside: panel.aside,
2147
+ headerClassName,
2148
+ headerActions: onExpandedChange ? /* @__PURE__ */ (0, import_jsx_runtime32.jsx)(import_ui10.IconButton, { "aria-label": expanded ? "Shrink panel" : "Extend panel", onClick: () => onExpandedChange(!expanded), children: expanded ? /* @__PURE__ */ (0, import_jsx_runtime32.jsx)(import_lucide_react12.Minimize2, { "aria-hidden": "true", className: "h-4 w-4" }) : /* @__PURE__ */ (0, import_jsx_runtime32.jsx)(import_lucide_react12.Maximize2, { "aria-hidden": "true", className: "h-4 w-4" }) }) : void 0,
2149
+ children: panel.content
2150
+ }
2151
+ );
2152
+ }
2153
+
1944
2154
  // src/genre-tree/ChangeParentPopup.tsx
1945
- var import_react13 = require("react");
1946
- var import_genre_tree19 = require("@behindthemusictree/app-kit/genre-tree");
1947
- var import_jsx_runtime31 = require("react/jsx-runtime");
2155
+ var import_react15 = require("react");
2156
+ var import_genre_tree20 = require("@behindthemusictree/app-kit/genre-tree");
2157
+ var import_jsx_runtime33 = require("react/jsx-runtime");
1948
2158
  function ChangeParentPopup({
1949
2159
  genrePlaylist,
1950
2160
  genrePlaylists,
@@ -1953,9 +2163,9 @@ function ChangeParentPopup({
1953
2163
  onClose,
1954
2164
  onParentChanged
1955
2165
  }) {
1956
- const { mutate, isPending, formErrors } = (0, import_genre_tree19.useUpdateGenre)(scope, getBackendBaseUrl);
1957
- const candidates = (0, import_react13.useMemo)(
1958
- () => (0, import_genre_tree19.getReparentCandidates)(genrePlaylists, genrePlaylist.uuid),
2166
+ const { mutate, isPending, formErrors } = (0, import_genre_tree20.useUpdateGenre)(scope, getBackendBaseUrl);
2167
+ const candidates = (0, import_react15.useMemo)(
2168
+ () => (0, import_genre_tree20.getReparentCandidates)(genrePlaylists, genrePlaylist.uuid),
1959
2169
  [genrePlaylists, genrePlaylist.uuid]
1960
2170
  );
1961
2171
  const handleSelect = (newParent) => {
@@ -1972,19 +2182,19 @@ function ChangeParentPopup({
1972
2182
  }
1973
2183
  );
1974
2184
  };
1975
- (0, import_react13.useEffect)(() => {
2185
+ (0, import_react15.useEffect)(() => {
1976
2186
  const handleKeyDown = (event) => {
1977
2187
  if (event.key === "Escape") onClose();
1978
2188
  };
1979
2189
  window.addEventListener("keydown", handleKeyDown);
1980
2190
  return () => window.removeEventListener("keydown", handleKeyDown);
1981
2191
  }, [onClose]);
1982
- return /* @__PURE__ */ (0, import_jsx_runtime31.jsxs)(BasePopup_default, { title: `Change parent of ${genrePlaylist.name}`, onClose, showCancelButton: true, fitContent: true, children: [
1983
- /* @__PURE__ */ (0, import_jsx_runtime31.jsxs)("p", { className: "mb-3 text-sm text-[var(--color-text-muted)]", children: [
2192
+ return /* @__PURE__ */ (0, import_jsx_runtime33.jsxs)(BasePopup_default, { title: `Change parent of ${genrePlaylist.name}`, onClose, showCancelButton: true, fitContent: true, children: [
2193
+ /* @__PURE__ */ (0, import_jsx_runtime33.jsxs)("p", { className: "mb-3 text-sm text-[var(--color-text-muted)]", children: [
1984
2194
  "Current parent: ",
1985
2195
  genrePlaylist.parent?.name ?? "none"
1986
2196
  ] }),
1987
- /* @__PURE__ */ (0, import_jsx_runtime31.jsx)("div", { "aria-busy": isPending, className: isPending ? "pointer-events-none opacity-60" : void 0, children: /* @__PURE__ */ (0, import_jsx_runtime31.jsx)(
2197
+ /* @__PURE__ */ (0, import_jsx_runtime33.jsx)("div", { "aria-busy": isPending, className: isPending ? "pointer-events-none opacity-60" : void 0, children: /* @__PURE__ */ (0, import_jsx_runtime33.jsx)(
1988
2198
  GenreSearch,
1989
2199
  {
1990
2200
  genrePlaylists: candidates,
@@ -1992,24 +2202,24 @@ function ChangeParentPopup({
1992
2202
  placeholder: "Search a new parent\u2026"
1993
2203
  }
1994
2204
  ) }),
1995
- formErrors.length > 0 && /* @__PURE__ */ (0, import_jsx_runtime31.jsx)("p", { role: "alert", className: "mt-3 text-sm text-[var(--color-red-500)]", children: formErrors.map(({ message }) => message).join(" ") })
2205
+ formErrors.length > 0 && /* @__PURE__ */ (0, import_jsx_runtime33.jsx)("p", { role: "alert", className: "mt-3 text-sm text-[var(--color-red-500)]", children: formErrors.map(({ message }) => message).join(" ") })
1996
2206
  ] });
1997
2207
  }
1998
2208
 
1999
2209
  // src/genre-tree/artist/useArtistPanel.tsx
2000
- var import_ui15 = require("@behindthemusictree/ui");
2001
- var import_genre_tree24 = require("@behindthemusictree/app-kit/genre-tree");
2210
+ var import_ui16 = require("@behindthemusictree/ui");
2211
+ var import_genre_tree25 = require("@behindthemusictree/app-kit/genre-tree");
2002
2212
 
2003
2213
  // src/genre-tree/artist/ArtistHeader.tsx
2004
- var import_ui11 = require("@behindthemusictree/ui");
2214
+ var import_ui12 = require("@behindthemusictree/ui");
2005
2215
 
2006
2216
  // src/genre-tree/artist/ArtistLinks.tsx
2007
- var import_lucide_react12 = require("lucide-react");
2217
+ var import_lucide_react13 = require("lucide-react");
2008
2218
  var import_si3 = require("react-icons/si");
2009
- var import_ui10 = require("@behindthemusictree/ui");
2010
- var import_jsx_runtime32 = require("react/jsx-runtime");
2219
+ var import_ui11 = require("@behindthemusictree/ui");
2220
+ var import_jsx_runtime34 = require("react/jsx-runtime");
2011
2221
  var LINK_KINDS = {
2012
- official: { label: "Official website", Icon: import_lucide_react12.Globe },
2222
+ official: { label: "Official website", Icon: import_lucide_react13.Globe },
2013
2223
  wikipedia: { label: "Wikipedia", Icon: import_si3.SiWikipedia },
2014
2224
  wikidata: { label: "Wikidata", Icon: import_si3.SiWikidata },
2015
2225
  spotify: { label: "Spotify", Icon: import_si3.SiSpotify },
@@ -2017,7 +2227,7 @@ var LINK_KINDS = {
2017
2227
  bandcamp: { label: "Bandcamp", Icon: import_si3.SiBandcamp },
2018
2228
  soundcloud: { label: "SoundCloud", Icon: import_si3.SiSoundcloud },
2019
2229
  discogs: { label: "Discogs", Icon: import_si3.SiDiscogs },
2020
- social: { label: "Social", Icon: import_lucide_react12.Share2 }
2230
+ social: { label: "Social", Icon: import_lucide_react13.Share2 }
2021
2231
  };
2022
2232
  function socialLabel(url) {
2023
2233
  try {
@@ -2044,7 +2254,7 @@ function artistLinks(artist) {
2044
2254
  return links;
2045
2255
  }
2046
2256
  function ArtistLinks({ links }) {
2047
- return /* @__PURE__ */ (0, import_jsx_runtime32.jsx)("ul", { className: "flex flex-wrap gap-1.5", children: links.map(({ key, url, Icon, label }) => /* @__PURE__ */ (0, import_jsx_runtime32.jsx)("li", { children: /* @__PURE__ */ (0, import_jsx_runtime32.jsx)(import_ui10.Chip, { href: url, icon: /* @__PURE__ */ (0, import_jsx_runtime32.jsx)(Icon, { className: "h-3.5 w-3.5 shrink-0", "aria-hidden": "true" }), children: label }) }, key)) });
2257
+ return /* @__PURE__ */ (0, import_jsx_runtime34.jsx)("ul", { className: "flex flex-wrap gap-1.5", children: links.map(({ key, url, Icon, label }) => /* @__PURE__ */ (0, import_jsx_runtime34.jsx)("li", { children: /* @__PURE__ */ (0, import_jsx_runtime34.jsx)(import_ui11.Chip, { href: url, icon: /* @__PURE__ */ (0, import_jsx_runtime34.jsx)(Icon, { className: "h-3.5 w-3.5 shrink-0", "aria-hidden": "true" }), children: label }) }, key)) });
2048
2258
  }
2049
2259
 
2050
2260
  // src/genre-tree/artist/formatYears.ts
@@ -2055,7 +2265,7 @@ function formatYears(beginYear, endYear, ended) {
2055
2265
  }
2056
2266
 
2057
2267
  // src/genre-tree/artist/ArtistHeader.tsx
2058
- var import_jsx_runtime33 = require("react/jsx-runtime");
2268
+ var import_jsx_runtime35 = require("react/jsx-runtime");
2059
2269
  var countryNames = new Intl.DisplayNames(["en"], { type: "region" });
2060
2270
  function ArtistHeader({ artist }) {
2061
2271
  const fields = [];
@@ -2065,35 +2275,35 @@ function ArtistHeader({ artist }) {
2065
2275
  const years = formatYears(artist.beginYear, artist.endYear, artist.ended);
2066
2276
  if (years) fields.push({ label: "Active", value: years });
2067
2277
  const links = artistLinks(artist);
2068
- if (links.length > 0) fields.push({ label: "Links", value: /* @__PURE__ */ (0, import_jsx_runtime33.jsx)(ArtistLinks, { links }) });
2278
+ if (links.length > 0) fields.push({ label: "Links", value: /* @__PURE__ */ (0, import_jsx_runtime35.jsx)(ArtistLinks, { links }) });
2069
2279
  if (fields.length === 0) return null;
2070
- return /* @__PURE__ */ (0, import_jsx_runtime33.jsx)(import_ui11.FieldList, { items: fields });
2280
+ return /* @__PURE__ */ (0, import_jsx_runtime35.jsx)(import_ui12.FieldList, { items: fields });
2071
2281
  }
2072
2282
 
2073
2283
  // src/genre-tree/artist/ArtistPhoto.tsx
2074
- var import_jsx_runtime34 = require("react/jsx-runtime");
2284
+ var import_jsx_runtime36 = require("react/jsx-runtime");
2075
2285
  function ExternalLink({ href, children }) {
2076
- return /* @__PURE__ */ (0, import_jsx_runtime34.jsx)("a", { href, target: "_blank", rel: "noopener noreferrer", className: "underline-offset-2 hover:underline", children });
2286
+ return /* @__PURE__ */ (0, import_jsx_runtime36.jsx)("a", { href, target: "_blank", rel: "noopener noreferrer", className: "underline-offset-2 hover:underline", children });
2077
2287
  }
2078
2288
  function ArtistPhoto({ image, name }) {
2079
- return /* @__PURE__ */ (0, import_jsx_runtime34.jsxs)("figure", { className: "flex flex-col gap-1.5 px-6 py-2.5", children: [
2080
- /* @__PURE__ */ (0, import_jsx_runtime34.jsx)("img", { src: image.url, alt: name, className: "h-64 w-full object-contain bg-[var(--color-surface-muted)] rounded" }),
2081
- /* @__PURE__ */ (0, import_jsx_runtime34.jsxs)("figcaption", { className: "text-xs text-[var(--color-text-muted)]", children: [
2289
+ return /* @__PURE__ */ (0, import_jsx_runtime36.jsxs)("figure", { className: "flex flex-col gap-1.5 px-6 py-2.5", children: [
2290
+ /* @__PURE__ */ (0, import_jsx_runtime36.jsx)("img", { src: image.url, alt: name, className: "h-64 w-full object-contain bg-[var(--color-surface-muted)] rounded" }),
2291
+ /* @__PURE__ */ (0, import_jsx_runtime36.jsxs)("figcaption", { className: "text-xs text-[var(--color-text-muted)]", children: [
2082
2292
  "Photo: ",
2083
- /* @__PURE__ */ (0, import_jsx_runtime34.jsx)(ExternalLink, { href: image.pageUrl, children: image.author ?? "Wikimedia Commons" }),
2293
+ /* @__PURE__ */ (0, import_jsx_runtime36.jsx)(ExternalLink, { href: image.pageUrl, children: image.author ?? "Wikimedia Commons" }),
2084
2294
  ",",
2085
2295
  " ",
2086
- image.licenseUrl ? /* @__PURE__ */ (0, import_jsx_runtime34.jsx)(ExternalLink, { href: image.licenseUrl, children: image.license }) : image.license
2296
+ image.licenseUrl ? /* @__PURE__ */ (0, import_jsx_runtime36.jsx)(ExternalLink, { href: image.licenseUrl, children: image.license }) : image.license
2087
2297
  ] })
2088
2298
  ] });
2089
2299
  }
2090
2300
 
2091
2301
  // src/genre-tree/artist/ArtistMembers.tsx
2092
- var import_ui12 = require("@behindthemusictree/ui");
2093
- var import_jsx_runtime35 = require("react/jsx-runtime");
2302
+ var import_ui13 = require("@behindthemusictree/ui");
2303
+ var import_jsx_runtime37 = require("react/jsx-runtime");
2094
2304
  function MembershipList({ title, memberships, renderArtistLink }) {
2095
2305
  if (memberships.length === 0) return null;
2096
- return /* @__PURE__ */ (0, import_jsx_runtime35.jsx)(import_ui12.PanelGroup, { title, children: /* @__PURE__ */ (0, import_jsx_runtime35.jsx)("ul", { children: memberships.map((membership, index) => {
2306
+ return /* @__PURE__ */ (0, import_jsx_runtime37.jsx)(import_ui13.PanelGroup, { title, children: /* @__PURE__ */ (0, import_jsx_runtime37.jsx)("ul", { children: memberships.map((membership, index) => {
2097
2307
  const details = [
2098
2308
  formatYears(membership.beginYear, membership.endYear, membership.ended),
2099
2309
  ...membership.roles
@@ -2101,9 +2311,9 @@ function MembershipList({ title, memberships, renderArtistLink }) {
2101
2311
  const detailsText = details.filter(Boolean).join(" \xB7 ");
2102
2312
  return (
2103
2313
  // A member can have several periods in the same group.
2104
- /* @__PURE__ */ (0, import_jsx_runtime35.jsxs)("li", { className: "truncate py-0.5", title: detailsText || void 0, children: [
2105
- /* @__PURE__ */ (0, import_jsx_runtime35.jsx)("span", { className: "font-medium text-[var(--color-text)]", children: renderArtistLink(membership.artist) }),
2106
- detailsText && /* @__PURE__ */ (0, import_jsx_runtime35.jsxs)("span", { className: "text-[var(--color-text-muted)]", children: [
2314
+ /* @__PURE__ */ (0, import_jsx_runtime37.jsxs)("li", { className: "truncate py-0.5", title: detailsText || void 0, children: [
2315
+ /* @__PURE__ */ (0, import_jsx_runtime37.jsx)("span", { className: "font-medium text-[var(--color-text)]", children: renderArtistLink(membership.artist) }),
2316
+ detailsText && /* @__PURE__ */ (0, import_jsx_runtime37.jsxs)("span", { className: "text-[var(--color-text-muted)]", children: [
2107
2317
  " \u2014 ",
2108
2318
  detailsText
2109
2319
  ] })
@@ -2114,9 +2324,9 @@ function MembershipList({ title, memberships, renderArtistLink }) {
2114
2324
  function ArtistMembers({ title, memberships, renderArtistLink }) {
2115
2325
  if (memberships.length === 0) return null;
2116
2326
  const isCurrent = (membership) => !membership.ended && membership.endYear === null;
2117
- return /* @__PURE__ */ (0, import_jsx_runtime35.jsxs)(import_ui12.PanelSection, { title, children: [
2118
- /* @__PURE__ */ (0, import_jsx_runtime35.jsx)(MembershipList, { title: "Current", memberships: memberships.filter(isCurrent), renderArtistLink }),
2119
- /* @__PURE__ */ (0, import_jsx_runtime35.jsx)(
2327
+ return /* @__PURE__ */ (0, import_jsx_runtime37.jsxs)(import_ui13.PanelSection, { title, children: [
2328
+ /* @__PURE__ */ (0, import_jsx_runtime37.jsx)(MembershipList, { title: "Current", memberships: memberships.filter(isCurrent), renderArtistLink }),
2329
+ /* @__PURE__ */ (0, import_jsx_runtime37.jsx)(
2120
2330
  MembershipList,
2121
2331
  {
2122
2332
  title: "Past",
@@ -2128,8 +2338,8 @@ function ArtistMembers({ title, memberships, renderArtistLink }) {
2128
2338
  }
2129
2339
 
2130
2340
  // src/genre-tree/artist/ArtistDiscography.tsx
2131
- var import_ui13 = require("@behindthemusictree/ui");
2132
- var import_jsx_runtime36 = require("react/jsx-runtime");
2341
+ var import_ui14 = require("@behindthemusictree/ui");
2342
+ var import_jsx_runtime38 = require("react/jsx-runtime");
2133
2343
  var GROUPS = ["Albums", "EPs", "Singles", "Live", "Compilations", "Other"];
2134
2344
  var PRIMARY_GROUPS = { album: "Albums", ep: "EPs", single: "Singles" };
2135
2345
  function discographyGroup({ primaryType, secondaryTypes }) {
@@ -2149,34 +2359,34 @@ function byYear(a, b) {
2149
2359
  function ArtistDiscography({ albums }) {
2150
2360
  if (albums.length === 0) return null;
2151
2361
  const sorted = [...albums].sort(byYear);
2152
- return /* @__PURE__ */ (0, import_jsx_runtime36.jsx)(import_ui13.PanelSection, { title: "Discography", children: GROUPS.map((group) => {
2362
+ return /* @__PURE__ */ (0, import_jsx_runtime38.jsx)(import_ui14.PanelSection, { title: "Discography", children: GROUPS.map((group) => {
2153
2363
  const groupAlbums = sorted.filter((album) => discographyGroup(album) === group);
2154
2364
  if (groupAlbums.length === 0) return null;
2155
- return /* @__PURE__ */ (0, import_jsx_runtime36.jsx)(import_ui13.PanelGroup, { title: group, children: /* @__PURE__ */ (0, import_jsx_runtime36.jsx)("ul", { children: groupAlbums.map((album) => /* @__PURE__ */ (0, import_jsx_runtime36.jsxs)("li", { className: "grid grid-cols-[2.5rem_minmax(0,1fr)] gap-2 py-0.5", children: [
2156
- /* @__PURE__ */ (0, import_jsx_runtime36.jsx)("span", { className: "tabular-nums text-[var(--color-text-muted)]", children: album.year ?? "\u2014" }),
2157
- /* @__PURE__ */ (0, import_jsx_runtime36.jsx)("span", { className: "truncate text-[var(--color-text)]", title: album.name, children: album.name })
2365
+ return /* @__PURE__ */ (0, import_jsx_runtime38.jsx)(import_ui14.PanelGroup, { title: group, children: /* @__PURE__ */ (0, import_jsx_runtime38.jsx)("ul", { children: groupAlbums.map((album) => /* @__PURE__ */ (0, import_jsx_runtime38.jsxs)("li", { className: "grid grid-cols-[2.5rem_minmax(0,1fr)] gap-2 py-0.5", children: [
2366
+ /* @__PURE__ */ (0, import_jsx_runtime38.jsx)("span", { className: "tabular-nums text-[var(--color-text-muted)]", children: album.year ?? "\u2014" }),
2367
+ /* @__PURE__ */ (0, import_jsx_runtime38.jsx)("span", { className: "truncate text-[var(--color-text)]", title: album.name, children: album.name })
2158
2368
  ] }, album.uuid)) }) }, group);
2159
2369
  }) });
2160
2370
  }
2161
2371
 
2162
2372
  // src/genre-tree/artist/ArtistSongs.tsx
2163
- var import_react14 = require("react");
2373
+ var import_react16 = require("react");
2164
2374
  var import_react_query2 = require("@tanstack/react-query");
2165
- var import_lucide_react13 = require("lucide-react");
2166
- var import_ui14 = require("@behindthemusictree/ui");
2375
+ var import_lucide_react14 = require("lucide-react");
2376
+ var import_ui15 = require("@behindthemusictree/ui");
2167
2377
  var import_player10 = require("@behindthemusictree/app-kit/player");
2168
- var import_genre_tree23 = require("@behindthemusictree/app-kit/genre-tree");
2378
+ var import_genre_tree24 = require("@behindthemusictree/app-kit/genre-tree");
2169
2379
  var import_transport9 = require("@behindthemusictree/app-kit/transport");
2170
2380
 
2171
2381
  // src/genre-tree/track-list-sidebar/TrackItem.tsx
2172
2382
  var import_player9 = require("@behindthemusictree/app-kit/player");
2173
- var import_genre_tree20 = require("@behindthemusictree/app-kit/genre-tree");
2383
+ var import_genre_tree21 = require("@behindthemusictree/app-kit/genre-tree");
2174
2384
 
2175
2385
  // src/genre-tree/TrackPositionPlayPause.tsx
2176
2386
  var import_fa5 = require("react-icons/fa");
2177
2387
  var import_player7 = require("@behindthemusictree/app-kit/player");
2178
2388
  var import_player8 = require("@behindthemusictree/app-kit/player");
2179
- var import_jsx_runtime37 = require("react/jsx-runtime");
2389
+ var import_jsx_runtime39 = require("react/jsx-runtime");
2180
2390
  function TrackPositionPlayPause({
2181
2391
  position,
2182
2392
  uuid,
@@ -2190,14 +2400,14 @@ function TrackPositionPlayPause({
2190
2400
  const isPlaying = isThisTrack && isConfirmedPlayable && playState === import_player8.PlayStates.PLAYING;
2191
2401
  const isPaused = isThisTrack && isConfirmedPlayable && playState === import_player8.PlayStates.PAUSED;
2192
2402
  const isCurrentTrackActive = isPlaying || isPaused;
2193
- return /* @__PURE__ */ (0, import_jsx_runtime37.jsxs)(
2403
+ return /* @__PURE__ */ (0, import_jsx_runtime39.jsxs)(
2194
2404
  "div",
2195
2405
  {
2196
2406
  className: "track-position-play-pause relative flex items-center justify-center gap-2 text-lg w-16 cursor-pointer",
2197
2407
  style: { minWidth: "4rem", maxWidth: "4rem" },
2198
2408
  onClick: handlePlayPauseClick,
2199
2409
  children: [
2200
- /* @__PURE__ */ (0, import_jsx_runtime37.jsx)("div", { className: "track-position-play-pause__status group-hover:hidden flex items-center justify-center", children: isAwaitingPlayback ? /* @__PURE__ */ (0, import_jsx_runtime37.jsx)("span", { className: "track-position-play-pause__spinner", role: "status", "aria-label": "Loading track" }) : isCurrentTrackActive ? /* @__PURE__ */ (0, import_jsx_runtime37.jsxs)(
2410
+ /* @__PURE__ */ (0, import_jsx_runtime39.jsx)("div", { className: "track-position-play-pause__status group-hover:hidden flex items-center justify-center", children: isAwaitingPlayback ? /* @__PURE__ */ (0, import_jsx_runtime39.jsx)("span", { className: "track-position-play-pause__spinner", role: "status", "aria-label": "Loading track" }) : isCurrentTrackActive ? /* @__PURE__ */ (0, import_jsx_runtime39.jsxs)(
2201
2411
  "span",
2202
2412
  {
2203
2413
  className: "track-position-play-pause__bars",
@@ -2205,18 +2415,18 @@ function TrackPositionPlayPause({
2205
2415
  role: "img",
2206
2416
  "aria-label": isPlaying ? "Playing" : "Paused",
2207
2417
  children: [
2208
- /* @__PURE__ */ (0, import_jsx_runtime37.jsx)("span", { className: "track-position-play-pause__bar" }),
2209
- /* @__PURE__ */ (0, import_jsx_runtime37.jsx)("span", { className: "track-position-play-pause__bar" }),
2210
- /* @__PURE__ */ (0, import_jsx_runtime37.jsx)("span", { className: "track-position-play-pause__bar" })
2418
+ /* @__PURE__ */ (0, import_jsx_runtime39.jsx)("span", { className: "track-position-play-pause__bar" }),
2419
+ /* @__PURE__ */ (0, import_jsx_runtime39.jsx)("span", { className: "track-position-play-pause__bar" }),
2420
+ /* @__PURE__ */ (0, import_jsx_runtime39.jsx)("span", { className: "track-position-play-pause__bar" })
2211
2421
  ]
2212
2422
  }
2213
2423
  ) : position }),
2214
- /* @__PURE__ */ (0, import_jsx_runtime37.jsx)(
2424
+ /* @__PURE__ */ (0, import_jsx_runtime39.jsx)(
2215
2425
  "div",
2216
2426
  {
2217
2427
  className: `hidden group-hover:flex items-center justify-center ${isCurrentTrackActive ? "" : "absolute inset-0"}`,
2218
2428
  "aria-hidden": "true",
2219
- children: isAwaitingPlayback ? /* @__PURE__ */ (0, import_jsx_runtime37.jsx)("span", { className: "track-position-play-pause__spinner" }) : isPlaying ? /* @__PURE__ */ (0, import_jsx_runtime37.jsx)(import_fa5.FaPause, {}) : /* @__PURE__ */ (0, import_jsx_runtime37.jsx)(import_fa5.FaPlay, {})
2429
+ children: isAwaitingPlayback ? /* @__PURE__ */ (0, import_jsx_runtime39.jsx)("span", { className: "track-position-play-pause__spinner" }) : isPlaying ? /* @__PURE__ */ (0, import_jsx_runtime39.jsx)(import_fa5.FaPause, {}) : /* @__PURE__ */ (0, import_jsx_runtime39.jsx)(import_fa5.FaPlay, {})
2220
2430
  }
2221
2431
  )
2222
2432
  ]
@@ -2225,9 +2435,9 @@ function TrackPositionPlayPause({
2225
2435
  }
2226
2436
 
2227
2437
  // src/genre-tree/track-list-sidebar/TrackItem.tsx
2228
- var import_genre_tree21 = require("@behindthemusictree/app-kit/genre-tree");
2229
2438
  var import_genre_tree22 = require("@behindthemusictree/app-kit/genre-tree");
2230
- var import_jsx_runtime38 = require("react/jsx-runtime");
2439
+ var import_genre_tree23 = require("@behindthemusictree/app-kit/genre-tree");
2440
+ var import_jsx_runtime40 = require("react/jsx-runtime");
2231
2441
  function TrackItem({
2232
2442
  track,
2233
2443
  position,
@@ -2237,11 +2447,11 @@ function TrackItem({
2237
2447
  renderSongLink
2238
2448
  }) {
2239
2449
  const { handlePlayPauseAction, playerTrackObject, loadTrackForPlayer } = (0, import_player9.usePlayer)();
2240
- const { trackList, selectedTrack, toTrackAtPosition, runtimeUnplayableTrackIds } = (0, import_genre_tree20.useTrackList)();
2450
+ const { trackList, selectedTrack, toTrackAtPosition, runtimeUnplayableTrackIds } = (0, import_genre_tree21.useTrackList)();
2241
2451
  const scope = trackList?.origin?.scope ?? null;
2242
2452
  const isCurrentTrack = playerTrackObject?.track.id === track.uuid;
2243
2453
  const hasPlaybackError = isCurrentTrack && !!playerTrackObject?.loadError;
2244
- const isUnplayable = hasPlaybackError || runtimeUnplayableTrackIds?.has(track.uuid) === true || (0, import_genre_tree21.isSong)(track) && (0, import_genre_tree21.songUnplayableReason)(track) !== null;
2454
+ const isUnplayable = hasPlaybackError || runtimeUnplayableTrackIds?.has(track.uuid) === true || (0, import_genre_tree22.isSong)(track) && (0, import_genre_tree22.songUnplayableReason)(track) !== null;
2245
2455
  const handlePlayPauseClick = (event) => {
2246
2456
  event.stopPropagation();
2247
2457
  if (isCurrentTrack) {
@@ -2251,12 +2461,12 @@ function TrackItem({
2251
2461
  loadTrackForPlayer(track.uuid);
2252
2462
  }
2253
2463
  };
2254
- return /* @__PURE__ */ (0, import_jsx_runtime38.jsxs)(
2464
+ return /* @__PURE__ */ (0, import_jsx_runtime40.jsxs)(
2255
2465
  "div",
2256
2466
  {
2257
2467
  className: `track-item flex h-14 text-[var(--color-text)] ${isUnplayable ? "opacity-50" : "hover:bg-[var(--color-surface-muted)] group"}`,
2258
2468
  children: [
2259
- isUnplayable ? /* @__PURE__ */ (0, import_jsx_runtime38.jsx)(
2469
+ isUnplayable ? /* @__PURE__ */ (0, import_jsx_runtime40.jsx)(
2260
2470
  "div",
2261
2471
  {
2262
2472
  className: "flex items-center justify-center text-lg w-16",
@@ -2264,7 +2474,7 @@ function TrackItem({
2264
2474
  "aria-disabled": "true",
2265
2475
  children: position
2266
2476
  }
2267
- ) : /* @__PURE__ */ (0, import_jsx_runtime38.jsx)(
2477
+ ) : /* @__PURE__ */ (0, import_jsx_runtime40.jsx)(
2268
2478
  TrackPositionPlayPause,
2269
2479
  {
2270
2480
  position,
@@ -2273,12 +2483,12 @@ function TrackItem({
2273
2483
  isSelected: selectedTrack?.uuid === track.uuid
2274
2484
  }
2275
2485
  ),
2276
- /* @__PURE__ */ (0, import_jsx_runtime38.jsxs)("div", { className: "title-artist-container flex flex-col items-start justify-center min-w-0 flex-1", children: [
2277
- /* @__PURE__ */ (0, import_jsx_runtime38.jsx)("div", { className: "flex max-w-full items-center gap-2", children: /* @__PURE__ */ (0, import_jsx_runtime38.jsx)("div", { className: "title text-lg font-bold text-[var(--color-text)] max-w-full truncate", children: renderSongLink ? renderSongLink(track) : (0, import_genre_tree21.isSong)(track) ? /* @__PURE__ */ (0, import_jsx_runtime38.jsx)(MusicBrainzRecordingLink, { song: track, className: "hover:underline focus-visible:underline", children: track.title }) : track.title }) }),
2278
- track.artists && track.artists.length > 0 ? /* @__PURE__ */ (0, import_jsx_runtime38.jsx)("div", { className: "artist text-base max-w-full truncate text-[var(--color-text-muted)]", children: /* @__PURE__ */ (0, import_jsx_runtime38.jsx)(import_genre_tree22.ArtistNames, { artists: track.artists, renderArtistLink }) }) : ""
2486
+ /* @__PURE__ */ (0, import_jsx_runtime40.jsxs)("div", { className: "title-artist-container flex flex-col items-start justify-center min-w-0 flex-1", children: [
2487
+ /* @__PURE__ */ (0, import_jsx_runtime40.jsx)("div", { className: "flex max-w-full items-center gap-2", children: /* @__PURE__ */ (0, import_jsx_runtime40.jsx)("div", { className: "title text-lg font-bold text-[var(--color-text)] max-w-full truncate", children: renderSongLink ? renderSongLink(track) : (0, import_genre_tree22.isSong)(track) ? /* @__PURE__ */ (0, import_jsx_runtime40.jsx)(MusicBrainzRecordingLink, { song: track, className: "hover:underline focus-visible:underline", children: track.title }) : track.title }) }),
2488
+ track.artists && track.artists.length > 0 ? /* @__PURE__ */ (0, import_jsx_runtime40.jsx)("div", { className: "artist text-base max-w-full truncate text-[var(--color-text-muted)]", children: /* @__PURE__ */ (0, import_jsx_runtime40.jsx)(import_genre_tree23.ArtistNames, { artists: track.artists, renderArtistLink }) }) : ""
2279
2489
  ] }),
2280
- /* @__PURE__ */ (0, import_jsx_runtime38.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_runtime38.jsx)("div", { className: "genre-name max-w-full whitespace-normal break-words p-1 text-xs font-bold", children: track.genre.name }) : "" }),
2281
- renderDuration && /* @__PURE__ */ (0, import_jsx_runtime38.jsx)(
2490
+ /* @__PURE__ */ (0, import_jsx_runtime40.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_runtime40.jsx)("div", { className: "genre-name max-w-full whitespace-normal break-words p-1 text-xs font-bold", children: track.genre.name }) : "" }),
2491
+ renderDuration && /* @__PURE__ */ (0, import_jsx_runtime40.jsx)(
2282
2492
  "div",
2283
2493
  {
2284
2494
  className: "duration flex text-base w-16 items-center justify-center",
@@ -2286,28 +2496,28 @@ function TrackItem({
2286
2496
  children: renderDuration(track)
2287
2497
  }
2288
2498
  ),
2289
- renderActions && /* @__PURE__ */ (0, import_jsx_runtime38.jsx)("div", { className: "edit flex text-base w-6 items-center justify-center mr-2 cursor-pointer", children: renderActions(track) })
2499
+ renderActions && /* @__PURE__ */ (0, import_jsx_runtime40.jsx)("div", { className: "edit flex text-base w-6 items-center justify-center mr-2 cursor-pointer", children: renderActions(track) })
2290
2500
  ]
2291
2501
  }
2292
2502
  );
2293
2503
  }
2294
2504
 
2295
2505
  // src/genre-tree/artist/ArtistSongs.tsx
2296
- var import_jsx_runtime39 = require("react/jsx-runtime");
2297
- var SongPageSchema2 = (0, import_transport9.PaginatedResponseSchema)(import_genre_tree23.SongDetailedSchema);
2506
+ var import_jsx_runtime41 = require("react/jsx-runtime");
2507
+ var SongPageSchema2 = (0, import_transport9.PaginatedResponseSchema)(import_genre_tree24.SongDetailedSchema);
2298
2508
  var SORTS = [
2299
2509
  { ordering: "title", label: "Title" },
2300
2510
  { ordering: "-albumYear", label: "Year" }
2301
2511
  ];
2302
2512
  function ArtistSongs({ artist, getBackendBaseUrl, renderArtistLink, renderSongLink }) {
2303
- const [page, setPage] = (0, import_react14.useState)(1);
2304
- const [ordering, setOrdering] = (0, import_react14.useState)("title");
2513
+ const [page, setPage] = (0, import_react16.useState)(1);
2514
+ const [ordering, setOrdering] = (0, import_react16.useState)("title");
2305
2515
  const { fetch } = (0, import_transport9.useFetchWrapper)(getBackendBaseUrl);
2306
- const { playNewTrackListFromArtist } = (0, import_genre_tree23.useTrackList)();
2516
+ const { playNewTrackListFromArtist } = (0, import_genre_tree24.useTrackList)();
2307
2517
  const { playerTrackObject } = (0, import_player10.usePlayer)();
2308
2518
  const { data, isPending, isError } = (0, import_transport9.useQueryWithParse)({
2309
2519
  queryKey: ["referenceSongs", "artist", artist.uuid, ordering, page],
2310
- queryFn: () => fetch(import_genre_tree23.libraryEndpoints.reference.songs.list(), true, false, void 0, {
2520
+ queryFn: () => fetch(import_genre_tree24.libraryEndpoints.reference.songs.list(), true, false, void 0, {
2311
2521
  artist: artist.uuid,
2312
2522
  ordering,
2313
2523
  page
@@ -2322,25 +2532,25 @@ function ArtistSongs({ artist, getBackendBaseUrl, renderArtistLink, renderSongLi
2322
2532
  });
2323
2533
  const handleRowClickCapture = (event, song) => {
2324
2534
  if (!(event.target instanceof Element) || !event.target.closest(".track-position-play-pause")) return;
2325
- if (playerTrackObject?.track.id === song.uuid || (0, import_genre_tree23.songUnplayableReason)(song) !== null) return;
2535
+ if (playerTrackObject?.track.id === song.uuid || (0, import_genre_tree24.songUnplayableReason)(song) !== null) return;
2326
2536
  event.stopPropagation();
2327
2537
  void play(song.uuid);
2328
2538
  };
2329
- return /* @__PURE__ */ (0, import_jsx_runtime39.jsx)(import_ui14.PanelSection, { title: "Songs", children: isPending ? /* @__PURE__ */ (0, import_jsx_runtime39.jsx)(import_ui14.Skeleton, { className: "h-14", "aria-busy": "true" }) : isError ? /* @__PURE__ */ (0, import_jsx_runtime39.jsx)("p", { className: "text-red-600", children: "Could not load songs." }) : data.results.length === 0 ? /* @__PURE__ */ (0, import_jsx_runtime39.jsx)("p", { className: "text-[var(--color-text-muted)]", children: "No songs yet." }) : /* @__PURE__ */ (0, import_jsx_runtime39.jsxs)(import_jsx_runtime39.Fragment, { children: [
2330
- /* @__PURE__ */ (0, import_jsx_runtime39.jsxs)("div", { className: "flex flex-wrap items-center justify-between gap-2 mb-2", children: [
2331
- /* @__PURE__ */ (0, import_jsx_runtime39.jsxs)(
2539
+ return /* @__PURE__ */ (0, import_jsx_runtime41.jsx)(import_ui15.PanelSection, { title: "Songs", children: isPending ? /* @__PURE__ */ (0, import_jsx_runtime41.jsx)(import_ui15.Skeleton, { className: "h-14", "aria-busy": "true" }) : isError ? /* @__PURE__ */ (0, import_jsx_runtime41.jsx)("p", { className: "text-red-600", children: "Could not load songs." }) : data.results.length === 0 ? /* @__PURE__ */ (0, import_jsx_runtime41.jsx)("p", { className: "text-[var(--color-text-muted)]", children: "No songs yet." }) : /* @__PURE__ */ (0, import_jsx_runtime41.jsxs)(import_jsx_runtime41.Fragment, { children: [
2540
+ /* @__PURE__ */ (0, import_jsx_runtime41.jsxs)("div", { className: "flex flex-wrap items-center justify-between gap-2 mb-2", children: [
2541
+ /* @__PURE__ */ (0, import_jsx_runtime41.jsxs)(
2332
2542
  "button",
2333
2543
  {
2334
2544
  type: "button",
2335
2545
  onClick: () => void play(),
2336
2546
  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",
2337
2547
  children: [
2338
- /* @__PURE__ */ (0, import_jsx_runtime39.jsx)(import_lucide_react13.Play, { className: "h-3.5 w-3.5", "aria-hidden": "true" }),
2548
+ /* @__PURE__ */ (0, import_jsx_runtime41.jsx)(import_lucide_react14.Play, { className: "h-3.5 w-3.5", "aria-hidden": "true" }),
2339
2549
  "Play all"
2340
2550
  ]
2341
2551
  }
2342
2552
  ),
2343
- /* @__PURE__ */ (0, import_jsx_runtime39.jsx)("div", { role: "group", "aria-label": "Sort songs by", className: "flex gap-1 text-xs", children: SORTS.map((sort) => /* @__PURE__ */ (0, import_jsx_runtime39.jsx)(
2553
+ /* @__PURE__ */ (0, import_jsx_runtime41.jsx)("div", { role: "group", "aria-label": "Sort songs by", className: "flex gap-1 text-xs", children: SORTS.map((sort) => /* @__PURE__ */ (0, import_jsx_runtime41.jsx)(
2344
2554
  "button",
2345
2555
  {
2346
2556
  type: "button",
@@ -2355,7 +2565,7 @@ function ArtistSongs({ artist, getBackendBaseUrl, renderArtistLink, renderSongLi
2355
2565
  sort.ordering
2356
2566
  )) })
2357
2567
  ] }),
2358
- /* @__PURE__ */ (0, import_jsx_runtime39.jsx)("ul", { className: "overflow-hidden mb-2 bg-zinc-950 rounded-lg", children: data.results.map((song, index) => /* @__PURE__ */ (0, import_jsx_runtime39.jsx)("li", { onClickCapture: (event) => handleRowClickCapture(event, song), children: /* @__PURE__ */ (0, import_jsx_runtime39.jsx)(
2568
+ /* @__PURE__ */ (0, import_jsx_runtime41.jsx)("ul", { className: "overflow-hidden mb-2 bg-zinc-950 rounded-lg", children: data.results.map((song, index) => /* @__PURE__ */ (0, import_jsx_runtime41.jsx)("li", { onClickCapture: (event) => handleRowClickCapture(event, song), children: /* @__PURE__ */ (0, import_jsx_runtime41.jsx)(
2359
2569
  TrackItem,
2360
2570
  {
2361
2571
  track: song,
@@ -2364,12 +2574,12 @@ function ArtistSongs({ artist, getBackendBaseUrl, renderArtistLink, renderSongLi
2364
2574
  renderSongLink
2365
2575
  }
2366
2576
  ) }, song.uuid)) }),
2367
- /* @__PURE__ */ (0, import_jsx_runtime39.jsx)(import_ui14.Pagination, { currentPage: page, totalPages: data.totalPages, onPageChange: setPage })
2577
+ /* @__PURE__ */ (0, import_jsx_runtime41.jsx)(import_ui15.Pagination, { currentPage: page, totalPages: data.totalPages, onPageChange: setPage })
2368
2578
  ] }) });
2369
2579
  }
2370
2580
 
2371
2581
  // src/genre-tree/artist/useArtistPanel.tsx
2372
- var import_jsx_runtime40 = require("react/jsx-runtime");
2582
+ var import_jsx_runtime42 = require("react/jsx-runtime");
2373
2583
  function useArtistPanel({
2374
2584
  uuid,
2375
2585
  scope,
@@ -2381,7 +2591,7 @@ function useArtistPanel({
2381
2591
  onExpandedChange,
2382
2592
  onClose
2383
2593
  }) {
2384
- const result = (0, import_genre_tree24.useArtist)(uuid, getBackendBaseUrl);
2594
+ const result = (0, import_genre_tree25.useArtist)(uuid, getBackendBaseUrl);
2385
2595
  const panel = { expanded, onExpandedChange, onClose };
2386
2596
  switch (result.status) {
2387
2597
  case "idle":
@@ -2390,26 +2600,26 @@ function useArtistPanel({
2390
2600
  return {
2391
2601
  ...panel,
2392
2602
  title: "Artist",
2393
- content: /* @__PURE__ */ (0, import_jsx_runtime40.jsxs)("div", { "aria-busy": "true", className: "flex flex-col gap-2.5 px-6 py-2.5", children: [
2394
- /* @__PURE__ */ (0, import_jsx_runtime40.jsx)(import_ui15.Skeleton, { className: "h-3 w-16" }),
2395
- /* @__PURE__ */ (0, import_jsx_runtime40.jsx)(import_ui15.Skeleton, { className: "h-4 w-32" }),
2396
- /* @__PURE__ */ (0, import_jsx_runtime40.jsx)(import_ui15.Skeleton, { className: "h-3 w-16" }),
2397
- /* @__PURE__ */ (0, import_jsx_runtime40.jsx)(import_ui15.Skeleton, { className: "h-4 w-24" }),
2398
- /* @__PURE__ */ (0, import_jsx_runtime40.jsx)(import_ui15.Skeleton, { className: "h-16 mt-2.5" }),
2399
- /* @__PURE__ */ (0, import_jsx_runtime40.jsx)(import_ui15.Skeleton, { className: "h-16" })
2603
+ content: /* @__PURE__ */ (0, import_jsx_runtime42.jsxs)("div", { "aria-busy": "true", className: "flex flex-col gap-2.5 px-6 py-2.5", children: [
2604
+ /* @__PURE__ */ (0, import_jsx_runtime42.jsx)(import_ui16.Skeleton, { className: "h-3 w-16" }),
2605
+ /* @__PURE__ */ (0, import_jsx_runtime42.jsx)(import_ui16.Skeleton, { className: "h-4 w-32" }),
2606
+ /* @__PURE__ */ (0, import_jsx_runtime42.jsx)(import_ui16.Skeleton, { className: "h-3 w-16" }),
2607
+ /* @__PURE__ */ (0, import_jsx_runtime42.jsx)(import_ui16.Skeleton, { className: "h-4 w-24" }),
2608
+ /* @__PURE__ */ (0, import_jsx_runtime42.jsx)(import_ui16.Skeleton, { className: "h-16 mt-2.5" }),
2609
+ /* @__PURE__ */ (0, import_jsx_runtime42.jsx)(import_ui16.Skeleton, { className: "h-16" })
2400
2610
  ] })
2401
2611
  };
2402
2612
  case "not-found":
2403
2613
  return {
2404
2614
  ...panel,
2405
2615
  title: "Artist not found",
2406
- content: /* @__PURE__ */ (0, import_jsx_runtime40.jsx)("p", { className: "px-6 py-2.5 text-[var(--color-text-muted)]", children: "This artist does not exist." })
2616
+ content: /* @__PURE__ */ (0, import_jsx_runtime42.jsx)("p", { className: "px-6 py-2.5 text-[var(--color-text-muted)]", children: "This artist does not exist." })
2407
2617
  };
2408
2618
  case "error":
2409
2619
  return {
2410
2620
  ...panel,
2411
2621
  title: "Artist",
2412
- content: /* @__PURE__ */ (0, import_jsx_runtime40.jsx)("p", { className: "px-6 py-2.5 text-red-600", children: "Could not load this artist." })
2622
+ content: /* @__PURE__ */ (0, import_jsx_runtime42.jsx)("p", { className: "px-6 py-2.5 text-red-600", children: "Could not load this artist." })
2413
2623
  };
2414
2624
  case "success": {
2415
2625
  const { artist } = result;
@@ -2418,13 +2628,13 @@ function useArtistPanel({
2418
2628
  return {
2419
2629
  ...panel,
2420
2630
  title: artist.name,
2421
- content: /* @__PURE__ */ (0, import_jsx_runtime40.jsxs)(import_jsx_runtime40.Fragment, { children: [
2422
- shareUrl && /* @__PURE__ */ (0, import_jsx_runtime40.jsx)("div", { className: "genre-action-band border-b border-[var(--color-border)]", children: /* @__PURE__ */ (0, import_jsx_runtime40.jsx)("div", { className: "flex flex-wrap gap-1 px-3.5 py-3", children: shareTarget && /* @__PURE__ */ (0, import_jsx_runtime40.jsx)(ShareActionButton, { target: shareTarget, url: shareUrl }) }) }),
2423
- artist.image && /* @__PURE__ */ (0, import_jsx_runtime40.jsx)(ArtistPhoto, { image: artist.image, name: artist.name }),
2424
- /* @__PURE__ */ (0, import_jsx_runtime40.jsx)(ArtistHeader, { artist }),
2425
- /* @__PURE__ */ (0, import_jsx_runtime40.jsx)(ArtistMembers, { title: "Members", memberships: artist.members, renderArtistLink }),
2426
- /* @__PURE__ */ (0, import_jsx_runtime40.jsx)(ArtistMembers, { title: "Member of", memberships: artist.memberOf, renderArtistLink }),
2427
- artist.tracksCount > 0 && /* @__PURE__ */ (0, import_jsx_runtime40.jsx)(
2631
+ content: /* @__PURE__ */ (0, import_jsx_runtime42.jsxs)(import_jsx_runtime42.Fragment, { children: [
2632
+ shareUrl && /* @__PURE__ */ (0, import_jsx_runtime42.jsx)("div", { className: "genre-action-band border-b border-[var(--color-border)]", children: /* @__PURE__ */ (0, import_jsx_runtime42.jsx)("div", { className: "flex flex-wrap gap-1 px-3.5 py-3", children: shareTarget && /* @__PURE__ */ (0, import_jsx_runtime42.jsx)(ShareActionButton, { target: shareTarget, url: shareUrl }) }) }),
2633
+ artist.image && /* @__PURE__ */ (0, import_jsx_runtime42.jsx)(ArtistPhoto, { image: artist.image, name: artist.name }),
2634
+ /* @__PURE__ */ (0, import_jsx_runtime42.jsx)(ArtistHeader, { artist }),
2635
+ /* @__PURE__ */ (0, import_jsx_runtime42.jsx)(ArtistMembers, { title: "Members", memberships: artist.members, renderArtistLink }),
2636
+ /* @__PURE__ */ (0, import_jsx_runtime42.jsx)(ArtistMembers, { title: "Member of", memberships: artist.memberOf, renderArtistLink }),
2637
+ artist.tracksCount > 0 && /* @__PURE__ */ (0, import_jsx_runtime42.jsx)(
2428
2638
  ArtistSongs,
2429
2639
  {
2430
2640
  artist: { uuid: artist.uuid, name: artist.name },
@@ -2434,56 +2644,56 @@ function useArtistPanel({
2434
2644
  }
2435
2645
  )
2436
2646
  ] }),
2437
- extraContent: artist.albums.length > 0 ? /* @__PURE__ */ (0, import_jsx_runtime40.jsx)(ArtistDiscography, { albums: artist.albums }) : void 0
2647
+ extraContent: artist.albums.length > 0 ? /* @__PURE__ */ (0, import_jsx_runtime42.jsx)(ArtistDiscography, { albums: artist.albums }) : void 0
2438
2648
  };
2439
2649
  }
2440
2650
  }
2441
2651
  }
2442
2652
 
2443
2653
  // src/genre-tree/song/useSongPanel.tsx
2444
- var import_ui17 = require("@behindthemusictree/ui");
2445
- var import_genre_tree26 = require("@behindthemusictree/app-kit/genre-tree");
2654
+ var import_ui18 = require("@behindthemusictree/ui");
2655
+ var import_genre_tree27 = require("@behindthemusictree/app-kit/genre-tree");
2446
2656
 
2447
2657
  // src/genre-tree/song/SongPanelContent.tsx
2448
- var import_react15 = require("react");
2449
- var import_lucide_react14 = require("lucide-react");
2450
- var import_ui16 = require("@behindthemusictree/ui");
2658
+ var import_react17 = require("react");
2659
+ var import_lucide_react15 = require("lucide-react");
2660
+ var import_ui17 = require("@behindthemusictree/ui");
2451
2661
  var import_player11 = require("@behindthemusictree/app-kit/player");
2452
- var import_genre_tree25 = require("@behindthemusictree/app-kit/genre-tree");
2453
- var import_jsx_runtime41 = require("react/jsx-runtime");
2662
+ var import_genre_tree26 = require("@behindthemusictree/app-kit/genre-tree");
2663
+ var import_jsx_runtime43 = require("react/jsx-runtime");
2454
2664
  var numberFormat = new Intl.NumberFormat("en");
2455
2665
  var dateFormat = new Intl.DateTimeFormat("en", { dateStyle: "medium" });
2456
2666
  var externalLink = "text-[var(--color-primary)] hover:underline focus-visible:underline";
2457
2667
  function SongHistory({ uuid, getBackendBaseUrl }) {
2458
- const [open, setOpen] = (0, import_react15.useState)(false);
2459
- const history = (0, import_genre_tree25.useSongHistory)(uuid, getBackendBaseUrl, open);
2668
+ const [open, setOpen] = (0, import_react17.useState)(false);
2669
+ const history = (0, import_genre_tree26.useSongHistory)(uuid, getBackendBaseUrl, open);
2460
2670
  let body = null;
2461
- if (history.status === "loading") body = /* @__PURE__ */ (0, import_jsx_runtime41.jsx)(import_ui16.Skeleton, { className: "h-10", "aria-busy": "true" });
2462
- else if (history.status === "error") body = /* @__PURE__ */ (0, import_jsx_runtime41.jsx)("p", { className: "text-red-600", children: "Could not load the history." });
2671
+ if (history.status === "loading") body = /* @__PURE__ */ (0, import_jsx_runtime43.jsx)(import_ui17.Skeleton, { className: "h-10", "aria-busy": "true" });
2672
+ else if (history.status === "error") body = /* @__PURE__ */ (0, import_jsx_runtime43.jsx)("p", { className: "text-red-600", children: "Could not load the history." });
2463
2673
  else if (history.status === "success")
2464
- body = history.entries.length === 0 ? /* @__PURE__ */ (0, import_jsx_runtime41.jsx)("p", { className: "text-[var(--color-text-muted)]", children: "No changes yet." }) : /* @__PURE__ */ (0, import_jsx_runtime41.jsx)("ul", { className: "divide-y divide-[var(--color-border)]", children: history.entries.map((entry) => /* @__PURE__ */ (0, import_jsx_runtime41.jsxs)("li", { className: "py-1.5 text-sm", children: [
2465
- /* @__PURE__ */ (0, import_jsx_runtime41.jsx)("span", { className: "font-medium", children: entry.action }),
2466
- entry.oldValue !== null || entry.newValue !== null ? /* @__PURE__ */ (0, import_jsx_runtime41.jsxs)("span", { className: "text-[var(--color-text-muted)]", children: [
2674
+ body = history.entries.length === 0 ? /* @__PURE__ */ (0, import_jsx_runtime43.jsx)("p", { className: "text-[var(--color-text-muted)]", children: "No changes yet." }) : /* @__PURE__ */ (0, import_jsx_runtime43.jsx)("ul", { className: "divide-y divide-[var(--color-border)]", children: history.entries.map((entry) => /* @__PURE__ */ (0, import_jsx_runtime43.jsxs)("li", { className: "py-1.5 text-sm", children: [
2675
+ /* @__PURE__ */ (0, import_jsx_runtime43.jsx)("span", { className: "font-medium", children: entry.action }),
2676
+ entry.oldValue !== null || entry.newValue !== null ? /* @__PURE__ */ (0, import_jsx_runtime43.jsxs)("span", { className: "text-[var(--color-text-muted)]", children: [
2467
2677
  " ",
2468
2678
  "\u2014 ",
2469
2679
  entry.oldValue ?? "none",
2470
2680
  " \u2192 ",
2471
2681
  entry.newValue ?? "none"
2472
2682
  ] }) : null,
2473
- /* @__PURE__ */ (0, import_jsx_runtime41.jsxs)("span", { className: "block text-xs text-[var(--color-text-muted)]", children: [
2683
+ /* @__PURE__ */ (0, import_jsx_runtime43.jsxs)("span", { className: "block text-xs text-[var(--color-text-muted)]", children: [
2474
2684
  entry.actorPseudo ?? "Anonymous",
2475
2685
  " \xB7 ",
2476
2686
  dateFormat.format(new Date(entry.createdOn))
2477
2687
  ] })
2478
2688
  ] }, entry.uuid)) });
2479
- return /* @__PURE__ */ (0, import_jsx_runtime41.jsxs)(
2689
+ return /* @__PURE__ */ (0, import_jsx_runtime43.jsxs)(
2480
2690
  "details",
2481
2691
  {
2482
2692
  onToggle: (event) => setOpen(event.currentTarget.open),
2483
2693
  className: "px-6 pb-2.5 mt-2.5 border-t border-[var(--color-border)]",
2484
2694
  children: [
2485
- /* @__PURE__ */ (0, import_jsx_runtime41.jsx)("summary", { className: "mt-2.5 cursor-pointer text-[0.6875rem] font-semibold uppercase text-[var(--color-text-muted)]", children: "History" }),
2486
- /* @__PURE__ */ (0, import_jsx_runtime41.jsx)("div", { className: "mt-1.5", children: body })
2695
+ /* @__PURE__ */ (0, import_jsx_runtime43.jsx)("summary", { className: "mt-2.5 cursor-pointer text-[0.6875rem] font-semibold uppercase text-[var(--color-text-muted)]", children: "History" }),
2696
+ /* @__PURE__ */ (0, import_jsx_runtime43.jsx)("div", { className: "mt-1.5", children: body })
2487
2697
  ]
2488
2698
  }
2489
2699
  );
@@ -2497,25 +2707,25 @@ function SongPanelContent({
2497
2707
  genrePath,
2498
2708
  onSelectGenre
2499
2709
  }) {
2500
- const { playNewTrackListFromTrackUuid } = (0, import_genre_tree25.useTrackList)();
2710
+ const { playNewTrackListFromTrackUuid } = (0, import_genre_tree26.useTrackList)();
2501
2711
  const { playerTrackObject, playState, handlePlayPauseAction } = (0, import_player11.usePlayer)();
2502
- const unplayableReason = (0, import_genre_tree25.songUnplayableReason)(song);
2712
+ const unplayableReason = (0, import_genre_tree26.songUnplayableReason)(song);
2503
2713
  const isCurrent = playerTrackObject?.track.id === song.uuid;
2504
2714
  const isPlaying = isCurrent && playState === import_player11.PlayStates.PLAYING;
2505
2715
  const shareTarget = { kind: "song", uuid: song.uuid, name: song.title, scope };
2506
- const musicBrainzUrl = (0, import_genre_tree25.musicBrainzRecordingUrl)(song);
2716
+ const musicBrainzUrl = (0, import_genre_tree26.musicBrainzRecordingUrl)(song);
2507
2717
  const fields = [];
2508
2718
  if (song.genre) {
2509
2719
  const path = genrePath.length > 0 ? genrePath : null;
2510
2720
  fields.push({
2511
2721
  label: "Genre",
2512
- value: path ? /* @__PURE__ */ (0, import_jsx_runtime41.jsx)("span", { className: "flex flex-wrap items-center gap-x-1", children: path.map((genrePlaylist, index) => /* @__PURE__ */ (0, import_jsx_runtime41.jsxs)("span", { children: [
2513
- index > 0 && /* @__PURE__ */ (0, import_jsx_runtime41.jsx)("span", { "aria-hidden": "true", children: "\u203A " }),
2514
- /* @__PURE__ */ (0, import_jsx_runtime41.jsx)("button", { type: "button", onClick: () => onSelectGenre(genrePlaylist), className: "hover:underline", children: genrePlaylist.name })
2722
+ value: path ? /* @__PURE__ */ (0, import_jsx_runtime43.jsx)("span", { className: "flex flex-wrap items-center gap-x-1", children: path.map((genrePlaylist, index) => /* @__PURE__ */ (0, import_jsx_runtime43.jsxs)("span", { children: [
2723
+ index > 0 && /* @__PURE__ */ (0, import_jsx_runtime43.jsx)("span", { "aria-hidden": "true", children: "\u203A " }),
2724
+ /* @__PURE__ */ (0, import_jsx_runtime43.jsx)("button", { type: "button", onClick: () => onSelectGenre(genrePlaylist), className: "hover:underline", children: genrePlaylist.name })
2515
2725
  ] }, genrePlaylist.uuid)) }) : song.genre.name
2516
2726
  });
2517
2727
  } else {
2518
- fields.push({ label: "Genre", value: /* @__PURE__ */ (0, import_jsx_runtime41.jsx)("span", { className: "text-[var(--color-text-muted)]", children: "No genre" }) });
2728
+ fields.push({ label: "Genre", value: /* @__PURE__ */ (0, import_jsx_runtime43.jsx)("span", { className: "text-[var(--color-text-muted)]", children: "No genre" }) });
2519
2729
  }
2520
2730
  if (song.firstReleaseYear) fields.push({ label: "First released", value: song.firstReleaseYear });
2521
2731
  const viewCount = song.youtubeVideo?.viewCount;
@@ -2525,11 +2735,11 @@ function SongPanelContent({
2525
2735
  const links = [];
2526
2736
  if (musicBrainzUrl)
2527
2737
  links.push(
2528
- /* @__PURE__ */ (0, import_jsx_runtime41.jsx)("a", { href: musicBrainzUrl, target: "_blank", rel: "noopener noreferrer", className: externalLink, children: "MusicBrainz" }, "mb")
2738
+ /* @__PURE__ */ (0, import_jsx_runtime43.jsx)("a", { href: musicBrainzUrl, target: "_blank", rel: "noopener noreferrer", className: externalLink, children: "MusicBrainz" }, "mb")
2529
2739
  );
2530
2740
  if (song.youtubeVideo)
2531
2741
  links.push(
2532
- /* @__PURE__ */ (0, import_jsx_runtime41.jsx)(
2742
+ /* @__PURE__ */ (0, import_jsx_runtime43.jsx)(
2533
2743
  "a",
2534
2744
  {
2535
2745
  href: `https://www.youtube.com/watch?v=${encodeURIComponent(song.youtubeVideo.id)}`,
@@ -2544,33 +2754,33 @@ function SongPanelContent({
2544
2754
  if (links.length > 0)
2545
2755
  fields.push({
2546
2756
  label: "Links",
2547
- value: /* @__PURE__ */ (0, import_jsx_runtime41.jsx)("span", { className: "flex gap-3", children: links })
2757
+ value: /* @__PURE__ */ (0, import_jsx_runtime43.jsx)("span", { className: "flex gap-3", children: links })
2548
2758
  });
2549
- return /* @__PURE__ */ (0, import_jsx_runtime41.jsxs)(import_jsx_runtime41.Fragment, { children: [
2550
- /* @__PURE__ */ (0, import_jsx_runtime41.jsx)("div", { className: "genre-action-band border-b border-[var(--color-border)]", children: /* @__PURE__ */ (0, import_jsx_runtime41.jsxs)("div", { className: "flex flex-wrap gap-1 px-3.5 py-3", children: [
2551
- unplayableReason ? /* @__PURE__ */ (0, import_jsx_runtime41.jsx)("p", { className: "flex items-center px-2.5 text-sm text-[var(--color-text-muted)]", children: unplayableReason }) : /* @__PURE__ */ (0, import_jsx_runtime41.jsxs)(
2759
+ return /* @__PURE__ */ (0, import_jsx_runtime43.jsxs)(import_jsx_runtime43.Fragment, { children: [
2760
+ /* @__PURE__ */ (0, import_jsx_runtime43.jsx)("div", { className: "genre-action-band border-b border-[var(--color-border)]", children: /* @__PURE__ */ (0, import_jsx_runtime43.jsxs)("div", { className: "flex flex-wrap gap-1 px-3.5 py-3", children: [
2761
+ unplayableReason ? /* @__PURE__ */ (0, import_jsx_runtime43.jsx)("p", { className: "flex items-center px-2.5 text-sm text-[var(--color-text-muted)]", children: unplayableReason }) : /* @__PURE__ */ (0, import_jsx_runtime43.jsxs)(
2552
2762
  "button",
2553
2763
  {
2554
2764
  type: "button",
2555
2765
  onClick: () => isCurrent ? handlePlayPauseAction() : playNewTrackListFromTrackUuid(song, scope),
2556
2766
  className: "group flex flex-col items-center gap-1 w-16 text-xs text-center text-[var(--color-text)] focus-visible:outline-none",
2557
2767
  children: [
2558
- /* @__PURE__ */ (0, import_jsx_runtime41.jsx)("span", { className: "flex items-center justify-center w-10 h-10 rounded-full group-focus-visible:ring-2 group-focus-visible:ring-[var(--color-focus-ring)] bg-[var(--color-surface-muted)] transition-colors group-hover:bg-[var(--color-border)]", children: isPlaying ? /* @__PURE__ */ (0, import_jsx_runtime41.jsx)(import_lucide_react14.Pause, { "aria-hidden": "true", className: "w-5 h-5" }) : /* @__PURE__ */ (0, import_jsx_runtime41.jsx)(import_lucide_react14.Play, { "aria-hidden": "true", className: "w-5 h-5" }) }),
2768
+ /* @__PURE__ */ (0, import_jsx_runtime43.jsx)("span", { className: "flex items-center justify-center w-10 h-10 rounded-full group-focus-visible:ring-2 group-focus-visible:ring-[var(--color-focus-ring)] bg-[var(--color-surface-muted)] transition-colors group-hover:bg-[var(--color-border)]", children: isPlaying ? /* @__PURE__ */ (0, import_jsx_runtime43.jsx)(import_lucide_react15.Pause, { "aria-hidden": "true", className: "w-5 h-5" }) : /* @__PURE__ */ (0, import_jsx_runtime43.jsx)(import_lucide_react15.Play, { "aria-hidden": "true", className: "w-5 h-5" }) }),
2559
2769
  isPlaying ? "Pause" : "Play"
2560
2770
  ]
2561
2771
  }
2562
2772
  ),
2563
- shareUrl && /* @__PURE__ */ (0, import_jsx_runtime41.jsx)(ShareActionButton, { target: shareTarget, url: shareUrl })
2773
+ shareUrl && /* @__PURE__ */ (0, import_jsx_runtime43.jsx)(ShareActionButton, { target: shareTarget, url: shareUrl })
2564
2774
  ] }) }),
2565
- song.artists && song.artists.length > 0 && /* @__PURE__ */ (0, import_jsx_runtime41.jsx)("p", { className: "px-6 pt-2.5 text-[var(--color-text-muted)]", children: /* @__PURE__ */ (0, import_jsx_runtime41.jsx)(import_genre_tree25.ArtistNames, { artists: song.artists, renderArtistLink }) }),
2566
- /* @__PURE__ */ (0, import_jsx_runtime41.jsx)(import_ui16.FieldList, { items: fields }),
2567
- song.essentialGenres && song.essentialGenres.length > 0 && /* @__PURE__ */ (0, import_jsx_runtime41.jsx)(import_ui16.PanelSection, { title: "Essential in", children: /* @__PURE__ */ (0, import_jsx_runtime41.jsx)("ul", { children: song.essentialGenres.map((genre) => /* @__PURE__ */ (0, import_jsx_runtime41.jsx)("li", { className: "py-0.5", children: genre.name }, genre.uuid)) }) }),
2568
- /* @__PURE__ */ (0, import_jsx_runtime41.jsx)(SongHistory, { uuid: song.uuid, getBackendBaseUrl })
2775
+ song.artists && song.artists.length > 0 && /* @__PURE__ */ (0, import_jsx_runtime43.jsx)("p", { className: "px-6 pt-2.5 text-[var(--color-text-muted)]", children: /* @__PURE__ */ (0, import_jsx_runtime43.jsx)(import_genre_tree26.ArtistNames, { artists: song.artists, renderArtistLink }) }),
2776
+ /* @__PURE__ */ (0, import_jsx_runtime43.jsx)(import_ui17.FieldList, { items: fields }),
2777
+ song.essentialGenres && song.essentialGenres.length > 0 && /* @__PURE__ */ (0, import_jsx_runtime43.jsx)(import_ui17.PanelSection, { title: "Essential in", children: /* @__PURE__ */ (0, import_jsx_runtime43.jsx)("ul", { children: song.essentialGenres.map((genre) => /* @__PURE__ */ (0, import_jsx_runtime43.jsx)("li", { className: "py-0.5", children: genre.name }, genre.uuid)) }) }),
2778
+ /* @__PURE__ */ (0, import_jsx_runtime43.jsx)(SongHistory, { uuid: song.uuid, getBackendBaseUrl })
2569
2779
  ] });
2570
2780
  }
2571
2781
 
2572
2782
  // src/genre-tree/song/useSongPanel.tsx
2573
- var import_jsx_runtime42 = require("react/jsx-runtime");
2783
+ var import_jsx_runtime44 = require("react/jsx-runtime");
2574
2784
  function useSongPanel({
2575
2785
  uuid,
2576
2786
  scope,
@@ -2583,7 +2793,7 @@ function useSongPanel({
2583
2793
  onExpandedChange,
2584
2794
  onClose
2585
2795
  }) {
2586
- const result = (0, import_genre_tree26.useSong)(uuid, getBackendBaseUrl);
2796
+ const result = (0, import_genre_tree27.useSong)(uuid, getBackendBaseUrl);
2587
2797
  const panel = { expanded, onExpandedChange, onClose };
2588
2798
  switch (result.status) {
2589
2799
  case "idle":
@@ -2592,24 +2802,24 @@ function useSongPanel({
2592
2802
  return {
2593
2803
  ...panel,
2594
2804
  title: "Song",
2595
- content: /* @__PURE__ */ (0, import_jsx_runtime42.jsxs)("div", { "aria-busy": "true", className: "flex flex-col gap-2.5 px-6 py-2.5", children: [
2596
- /* @__PURE__ */ (0, import_jsx_runtime42.jsx)(import_ui17.Skeleton, { className: "h-3 w-16" }),
2597
- /* @__PURE__ */ (0, import_jsx_runtime42.jsx)(import_ui17.Skeleton, { className: "h-4 w-32" }),
2598
- /* @__PURE__ */ (0, import_jsx_runtime42.jsx)(import_ui17.Skeleton, { className: "h-3 w-16" }),
2599
- /* @__PURE__ */ (0, import_jsx_runtime42.jsx)(import_ui17.Skeleton, { className: "h-4 w-24" })
2805
+ content: /* @__PURE__ */ (0, import_jsx_runtime44.jsxs)("div", { "aria-busy": "true", className: "flex flex-col gap-2.5 px-6 py-2.5", children: [
2806
+ /* @__PURE__ */ (0, import_jsx_runtime44.jsx)(import_ui18.Skeleton, { className: "h-3 w-16" }),
2807
+ /* @__PURE__ */ (0, import_jsx_runtime44.jsx)(import_ui18.Skeleton, { className: "h-4 w-32" }),
2808
+ /* @__PURE__ */ (0, import_jsx_runtime44.jsx)(import_ui18.Skeleton, { className: "h-3 w-16" }),
2809
+ /* @__PURE__ */ (0, import_jsx_runtime44.jsx)(import_ui18.Skeleton, { className: "h-4 w-24" })
2600
2810
  ] })
2601
2811
  };
2602
2812
  case "not-found":
2603
2813
  return {
2604
2814
  ...panel,
2605
2815
  title: "Song not found",
2606
- content: /* @__PURE__ */ (0, import_jsx_runtime42.jsx)("p", { className: "px-6 py-2.5 text-[var(--color-text-muted)]", children: "This song does not exist." })
2816
+ content: /* @__PURE__ */ (0, import_jsx_runtime44.jsx)("p", { className: "px-6 py-2.5 text-[var(--color-text-muted)]", children: "This song does not exist." })
2607
2817
  };
2608
2818
  case "error":
2609
2819
  return {
2610
2820
  ...panel,
2611
2821
  title: "Song",
2612
- content: /* @__PURE__ */ (0, import_jsx_runtime42.jsx)("p", { className: "px-6 py-2.5 text-red-600", children: "Could not load this song." })
2822
+ content: /* @__PURE__ */ (0, import_jsx_runtime44.jsx)("p", { className: "px-6 py-2.5 text-red-600", children: "Could not load this song." })
2613
2823
  };
2614
2824
  case "success": {
2615
2825
  const { song } = result;
@@ -2617,7 +2827,7 @@ function useSongPanel({
2617
2827
  return {
2618
2828
  ...panel,
2619
2829
  title: song.title,
2620
- content: /* @__PURE__ */ (0, import_jsx_runtime42.jsx)(
2830
+ content: /* @__PURE__ */ (0, import_jsx_runtime44.jsx)(
2621
2831
  SongPanelContent,
2622
2832
  {
2623
2833
  song,
@@ -2636,29 +2846,29 @@ function useSongPanel({
2636
2846
 
2637
2847
  // src/genre-tree/SizeMetricToggle.tsx
2638
2848
  var import_md2 = require("react-icons/md");
2639
- var import_ui18 = require("@behindthemusictree/ui");
2640
- var import_jsx_runtime43 = require("react/jsx-runtime");
2849
+ var import_ui19 = require("@behindthemusictree/ui");
2850
+ var import_jsx_runtime45 = require("react/jsx-runtime");
2641
2851
  function SizeMetricToggle({
2642
2852
  value,
2643
2853
  onChange
2644
2854
  }) {
2645
- return /* @__PURE__ */ (0, import_jsx_runtime43.jsx)(
2646
- import_ui18.SegmentedControl,
2855
+ return /* @__PURE__ */ (0, import_jsx_runtime45.jsx)(
2856
+ import_ui19.SegmentedControl,
2647
2857
  {
2648
2858
  variant: "floating",
2649
2859
  "aria-label": "Size nodes by",
2650
2860
  value,
2651
2861
  onChange,
2652
2862
  options: [
2653
- { value: "songs", label: /* @__PURE__ */ (0, import_jsx_runtime43.jsx)(import_md2.MdMusicNote, { size: 18, "aria-hidden": true }), "aria-label": "Size by songs", title: "Node size by song count" },
2654
- { value: "views", label: /* @__PURE__ */ (0, import_jsx_runtime43.jsx)(import_md2.MdVisibility, { size: 18, "aria-hidden": true }), "aria-label": "Size by views", title: "Node size by song views" }
2863
+ { value: "songs", label: /* @__PURE__ */ (0, import_jsx_runtime45.jsx)(import_md2.MdMusicNote, { size: 18, "aria-hidden": true }), "aria-label": "Size by songs", title: "Node size by song count" },
2864
+ { value: "views", label: /* @__PURE__ */ (0, import_jsx_runtime45.jsx)(import_md2.MdVisibility, { size: 18, "aria-hidden": true }), "aria-label": "Size by views", title: "Node size by song views" }
2655
2865
  ]
2656
2866
  }
2657
2867
  );
2658
2868
  }
2659
2869
 
2660
2870
  // src/genre-tree/GenreTreeView.tsx
2661
- var import_jsx_runtime44 = require("react/jsx-runtime");
2871
+ var import_jsx_runtime46 = require("react/jsx-runtime");
2662
2872
  var NO_POP_ROOT_TITLE = "This genre tree has no 'Mainstream Pop' root yet";
2663
2873
  var REGIONS_CLASS = "[--gtv-center-left:var(--layout-center-left)] [--gtv-center-right:var(--layout-center-right)] [--gtv-center-top:var(--layout-band-top)]";
2664
2874
  var noop = () => {
@@ -2671,8 +2881,8 @@ function GenreTreeView({
2671
2881
  getBackendBaseUrl,
2672
2882
  additionalActions,
2673
2883
  viewMode: controlledViewMode,
2674
- selectedNodeId: controlledSelectedNodeId,
2675
- onSelectedNodeIdChange,
2884
+ selectedGenreUuid: controlledSelectedGenreUuid,
2885
+ onSelectedGenreUuidChange,
2676
2886
  defaultViewport,
2677
2887
  viewportRestore,
2678
2888
  onViewportChange,
@@ -2693,6 +2903,7 @@ function GenreTreeView({
2693
2903
  onArtistExpandedChange,
2694
2904
  getShareUrl,
2695
2905
  extraSearchGroups = [],
2906
+ SearchGroups,
2696
2907
  onSearchQueryChange,
2697
2908
  onSearchFocus,
2698
2909
  artistSearch = true,
@@ -2719,21 +2930,21 @@ function GenreTreeView({
2719
2930
  placement = "default",
2720
2931
  ...focus
2721
2932
  }) {
2722
- const getShareUrlRef = (0, import_react16.useRef)(getShareUrl);
2933
+ const getShareUrlRef = (0, import_react18.useRef)(getShareUrl);
2723
2934
  getShareUrlRef.current = getShareUrl;
2724
- const [internalArtistUuid, setInternalArtistUuid] = (0, import_react16.useState)(null);
2935
+ const [internalArtistUuid, setInternalArtistUuid] = (0, import_react18.useState)(null);
2725
2936
  const isArtistControlled = controlledArtistUuid !== void 0;
2726
2937
  const artistUuid = isArtistControlled ? controlledArtistUuid : internalArtistUuid;
2727
- const [internalSongUuid, setInternalSongUuid] = (0, import_react16.useState)(null);
2938
+ const [internalSongUuid, setInternalSongUuid] = (0, import_react18.useState)(null);
2728
2939
  const isSongControlled = controlledSongUuid !== void 0;
2729
2940
  const songUuid = isSongControlled ? controlledSongUuid : internalSongUuid;
2730
2941
  const changeSong = (uuid) => {
2731
2942
  if (!isSongControlled) setInternalSongUuid(uuid);
2732
2943
  onSongUuidChange?.(uuid);
2733
2944
  };
2734
- const [internalSizeMetric, setInternalSizeMetric] = (0, import_react16.useState)("songs");
2945
+ const [internalSizeMetric, setInternalSizeMetric] = (0, import_react18.useState)("songs");
2735
2946
  const sizeMetric = controlledSizeMetric ?? internalSizeMetric;
2736
- const deferredSizeMetric = (0, import_react16.useDeferredValue)(sizeMetric);
2947
+ const deferredSizeMetric = (0, import_react18.useDeferredValue)(sizeMetric);
2737
2948
  const changeSizeMetric = (metric) => {
2738
2949
  if (controlledSizeMetric === void 0) setInternalSizeMetric(metric);
2739
2950
  onSizeMetricChange?.(metric);
@@ -2748,6 +2959,8 @@ function GenreTreeView({
2748
2959
  genrePlaylistByNodeId,
2749
2960
  visibleGenrePlaylists,
2750
2961
  groupedGenrePlaylistsByRoot,
2962
+ selectedGenreUuid,
2963
+ selectGenre,
2751
2964
  selectedNodeId,
2752
2965
  selectedGenrePlaylist,
2753
2966
  selectNode,
@@ -2760,17 +2973,17 @@ function GenreTreeView({
2760
2973
  allowWheelRotation: internalAllowWheelRotation,
2761
2974
  toggleWheelRotation,
2762
2975
  onNodeHover: handleNodeHover
2763
- } = (0, import_genre_tree27.useGenreTreeViewState)({
2976
+ } = (0, import_genre_tree28.useGenreTreeViewState)({
2764
2977
  scope,
2765
2978
  getBackendBaseUrl,
2766
2979
  viewMode: controlledViewMode,
2767
- selectedNodeId: controlledSelectedNodeId,
2768
- onSelectedNodeIdChange,
2980
+ selectedGenreUuid: controlledSelectedGenreUuid,
2981
+ onSelectedGenreUuidChange,
2769
2982
  criteriaOverviewSchema,
2770
2983
  artistUuid
2771
2984
  });
2772
2985
  const restoreRevision = viewportRestore?.revision;
2773
- const [stackedRestoreOwner, setStackedRestoreOwner] = (0, import_react16.useState)({ revision: restoreRevision, rootUuid: selectedGenrePlaylist?.root.uuid });
2986
+ const [stackedRestoreOwner, setStackedRestoreOwner] = (0, import_react18.useState)({ revision: restoreRevision, rootUuid: selectedGenrePlaylist?.root.uuid });
2774
2987
  let restoreRootUuid = stackedRestoreOwner.rootUuid;
2775
2988
  if (stackedRestoreOwner.revision !== restoreRevision) {
2776
2989
  restoreRootUuid = selectedGenrePlaylist?.root.uuid;
@@ -2779,36 +2992,36 @@ function GenreTreeView({
2779
2992
  restoreRootUuid = selectedGenrePlaylist.root.uuid;
2780
2993
  setStackedRestoreOwner({ revision: restoreRevision, rootUuid: restoreRootUuid });
2781
2994
  }
2782
- const isGenreUpdating = (0, import_react_query3.useIsMutating)({ mutationKey: import_genre_tree27.genreUpdateMutationKey }) > 0;
2783
- const { mutate: updateGenre } = (0, import_genre_tree27.useUpdateGenre)(scope, getBackendBaseUrl);
2995
+ const isGenreUpdating = (0, import_react_query3.useIsMutating)({ mutationKey: import_genre_tree28.genreUpdateMutationKey }) > 0;
2996
+ const { mutate: updateGenre } = (0, import_genre_tree28.useUpdateGenre)(scope, getBackendBaseUrl);
2784
2997
  const busy = deferredSizeMetric !== sizeMetric || isTreeFetching || isGenreUpdating;
2785
2998
  const isArtistFiltered = artistUuid !== null;
2786
2999
  const canSizeByViews = !isArtistFiltered && hasViewsCount(visibleGenrePlaylists);
2787
- const artistResult = (0, import_genre_tree27.useArtist)(artistUuid, artistSearchBaseUrl ?? getBackendBaseUrl);
3000
+ const artistResult = (0, import_genre_tree28.useArtist)(artistUuid, artistSearchBaseUrl ?? getBackendBaseUrl);
2788
3001
  const filterArtistName = artistResult.status === "success" ? artistResult.artist.name : null;
2789
- const filterArtist = (0, import_react16.useMemo)(
3002
+ const filterArtist = (0, import_react18.useMemo)(
2790
3003
  () => artistUuid !== null && filterArtistName !== null ? { uuid: artistUuid, name: filterArtistName } : null,
2791
3004
  [artistUuid, filterArtistName]
2792
3005
  );
2793
3006
  const hasSelectedGenreNode = selectedGenrePlaylist !== void 0 && visibleGenrePlaylists.some(({ uuid }) => uuid === selectedGenrePlaylist.uuid);
2794
3007
  const playback = useGenrePlaylistPlayback({ scope, genrePlaylists: visibleGenrePlaylists, artist: filterArtist });
2795
3008
  const openArtist = (uuid) => {
2796
- if (selectedNodeId !== null) clearSelection();
3009
+ if (selectedGenreUuid !== null) clearSelection();
2797
3010
  if (songUuid !== null) changeSong(null);
2798
3011
  changeArtist(uuid);
2799
3012
  };
2800
3013
  const openSong = (uuid) => {
2801
- if (selectedNodeId !== null) clearSelection();
3014
+ if (selectedGenreUuid !== null) clearSelection();
2802
3015
  changeSong(uuid);
2803
3016
  };
2804
- const openSongRef = (0, import_react16.useRef)(openSong);
3017
+ const openSongRef = (0, import_react18.useRef)(openSong);
2805
3018
  openSongRef.current = openSong;
2806
- const resolvedRenderSongLink = (0, import_react16.useCallback)(
2807
- (song) => renderSongLink ? renderSongLink(song) : /* @__PURE__ */ (0, import_jsx_runtime44.jsx)("button", { type: "button", onClick: () => openSongRef.current(song.uuid), className: "hover:underline", children: song.title }),
3019
+ const resolvedRenderSongLink = (0, import_react18.useCallback)(
3020
+ (song) => renderSongLink ? renderSongLink(song) : /* @__PURE__ */ (0, import_jsx_runtime46.jsx)("button", { type: "button", onClick: () => openSongRef.current(song.uuid), className: "hover:underline", children: song.title }),
2808
3021
  [renderSongLink]
2809
3022
  );
2810
- const [expandedSongUuid, setExpandedSongUuid] = (0, import_react16.useState)(null);
2811
- const [expandedArtistUuid, setExpandedArtistUuid] = (0, import_react16.useState)(null);
3023
+ const [expandedSongUuid, setExpandedSongUuid] = (0, import_react18.useState)(null);
3024
+ const [expandedArtistUuid, setExpandedArtistUuid] = (0, import_react18.useState)(null);
2812
3025
  const artistBaseUrl = artistSearchBaseUrl ?? getBackendBaseUrl;
2813
3026
  const songBaseUrl = getBackendBaseUrl;
2814
3027
  const artistPanel = useArtistPanel({
@@ -2816,7 +3029,7 @@ function GenreTreeView({
2816
3029
  scope,
2817
3030
  getBackendBaseUrl: artistBaseUrl,
2818
3031
  getShareUrl: getShareUrlRef.current,
2819
- renderArtistLink: renderArtistLink ?? ((artist) => artist.isSpecialPurpose ? artist.name : /* @__PURE__ */ (0, import_jsx_runtime44.jsx)("button", { type: "button", onClick: () => openArtist(artist.uuid), className: "hover:underline", children: artist.name })),
3032
+ renderArtistLink: renderArtistLink ?? ((artist) => artist.isSpecialPurpose ? artist.name : /* @__PURE__ */ (0, import_jsx_runtime46.jsx)("button", { type: "button", onClick: () => openArtist(artist.uuid), className: "hover:underline", children: artist.name })),
2820
3033
  expanded: controlledArtistExpanded ?? (artistUuid !== null && expandedArtistUuid === artistUuid),
2821
3034
  onExpandedChange: (on) => {
2822
3035
  if (controlledArtistExpanded === void 0) setExpandedArtistUuid(on ? artistUuid : null);
@@ -2825,7 +3038,7 @@ function GenreTreeView({
2825
3038
  renderSongLink: resolvedRenderSongLink,
2826
3039
  onClose: () => changeArtist(null)
2827
3040
  });
2828
- const resolvedRenderArtistLink = renderArtistLink ?? ((artist) => artist.isSpecialPurpose ? artist.name : /* @__PURE__ */ (0, import_jsx_runtime44.jsx)("button", { type: "button", onClick: () => openArtist(artist.uuid), className: "hover:underline", children: artist.name }));
3041
+ const resolvedRenderArtistLink = renderArtistLink ?? ((artist) => artist.isSpecialPurpose ? artist.name : /* @__PURE__ */ (0, import_jsx_runtime46.jsx)("button", { type: "button", onClick: () => openArtist(artist.uuid), className: "hover:underline", children: artist.name }));
2829
3042
  const getGenrePath = (genre) => {
2830
3043
  const path = [];
2831
3044
  let current = visibleGenrePlaylists.find((playlist) => playlist.criteria?.uuid === genre.uuid);
@@ -2843,9 +3056,9 @@ function GenreTreeView({
2843
3056
  getShareUrl: getShareUrlRef.current,
2844
3057
  renderArtistLink: resolvedRenderArtistLink,
2845
3058
  getGenrePath,
2846
- onSelectGenre: (genrePlaylist) => {
3059
+ onSelectGenre: (genrePlaylist2) => {
2847
3060
  changeSong(null);
2848
- selectGenrePlaylist(genrePlaylist);
3061
+ selectGenrePlaylist(genrePlaylist2);
2849
3062
  },
2850
3063
  expanded: controlledSongExpanded ?? (songUuid !== null && expandedSongUuid === songUuid),
2851
3064
  onExpandedChange: (on) => {
@@ -2854,11 +3067,11 @@ function GenreTreeView({
2854
3067
  },
2855
3068
  onClose: () => changeSong(null)
2856
3069
  });
2857
- const builtInSongPanel = overlayPanel || hasSelectedGenreNode ? void 0 : songPanel;
2858
- const builtInArtistPanel = overlayPanel || hasSelectedGenreNode || builtInSongPanel ? void 0 : artistPanel;
3070
+ const builtInSongPanel = overlayPanel || selectedGenreUuid !== null ? void 0 : songPanel;
3071
+ const builtInArtistPanel = overlayPanel || selectedGenreUuid !== null || builtInSongPanel ? void 0 : artistPanel;
2859
3072
  const builtInPanel = builtInSongPanel ?? builtInArtistPanel;
2860
- const [uncontrolledCollapsed, setUncontrolledCollapsed] = (0, import_react16.useState)(false);
2861
- const [uncontrolledExpanded, setUncontrolledExpanded] = (0, import_react16.useState)(false);
3073
+ const [uncontrolledCollapsed, setUncontrolledCollapsed] = (0, import_react18.useState)(false);
3074
+ const [uncontrolledExpanded, setUncontrolledExpanded] = (0, import_react18.useState)(false);
2862
3075
  const isCollapsed = collapsed ?? uncontrolledCollapsed;
2863
3076
  const isExpanded = expanded ?? uncontrolledExpanded;
2864
3077
  const changeCollapsed = (next) => {
@@ -2869,177 +3082,114 @@ function GenreTreeView({
2869
3082
  setUncontrolledExpanded(next);
2870
3083
  onExpandedChange?.(next);
2871
3084
  };
2872
- (0, import_react16.useEffect)(() => {
3085
+ (0, import_react18.useEffect)(() => {
2873
3086
  setUncontrolledCollapsed(false);
2874
- }, [selectedNodeId, artistUuid, songUuid]);
2875
- const [isWheelRevealed, setIsWheelRevealed] = (0, import_react16.useState)(false);
3087
+ }, [selectedGenreUuid, artistUuid, songUuid]);
3088
+ const [isWheelRevealed, setIsWheelRevealed] = (0, import_react18.useState)(false);
2876
3089
  const usesHandoff = viewMode === "wheel" || viewMode === "pop-core";
2877
3090
  const isTreeReady = !isLoading && (!usesHandoff || isWheelRevealed);
2878
- const [resolveStyle, setResolveStyle] = (0, import_react16.useState)(null);
2879
- const handleNodeStyleResolverChange = (0, import_react16.useCallback)(
3091
+ const [resolveStyle, setResolveStyle] = (0, import_react18.useState)(null);
3092
+ const handleNodeStyleResolverChange = (0, import_react18.useCallback)(
2880
3093
  (resolver) => setResolveStyle(() => resolver),
2881
3094
  []
2882
3095
  );
2883
- const panelRef = (0, import_react16.useRef)(null);
2884
- const obscuredInset = (0, import_ui19.useObscuredInset)(panelRef);
3096
+ const panelRef = (0, import_react18.useRef)(null);
3097
+ const obscuredInset = (0, import_ui20.useObscuredInset)(panelRef);
2885
3098
  const hostPanel = { obscuredInset, onNodeStyleResolverChange: handleNodeStyleResolverChange };
2886
3099
  const nodeMetric = isArtistFiltered || !canSizeByViews ? "songs" : deferredSizeMetric;
2887
- const treeNodes = (0, import_react16.useMemo)(() => toTreeNodes(visibleGenrePlaylists, nodeMetric), [visibleGenrePlaylists, nodeMetric]);
2888
- const relations = (0, import_genre_tree_view4.useGenreNodeRelations)(
3100
+ const treeNodes = (0, import_react18.useMemo)(() => toTreeNodes(visibleGenrePlaylists, nodeMetric), [visibleGenrePlaylists, nodeMetric]);
3101
+ const relations = (0, import_genre_tree_view5.useGenreNodeRelations)(
2889
3102
  treeNodes,
2890
3103
  !overlayPanel && hasSelectedGenreNode ? selectedNodeId : null,
2891
3104
  resolveStyle
2892
3105
  );
2893
- const [selectionHistory, setSelectionHistory] = (0, import_react16.useState)({
2894
- current: selectedNodeId,
2895
- previous: null
2896
- });
2897
- if (selectionHistory.current !== selectedNodeId) {
2898
- setSelectionHistory({
2899
- current: selectedNodeId,
2900
- previous: selectionHistory.current ?? selectionHistory.previous
2901
- });
3106
+ const [selectionHistory, setSelectionHistory] = (0, import_react18.useState)({ current: selectedGenreUuid, previous: null });
3107
+ if (selectionHistory.current !== selectedGenreUuid) {
3108
+ setSelectionHistory({ current: selectedGenreUuid, previous: selectionHistory.current ?? selectionHistory.previous });
2902
3109
  }
2903
- const previousPlaylist = selectionHistory.previous !== null && selectionHistory.previous !== selectedNodeId ? genrePlaylistByNodeId.get(selectionHistory.previous) : void 0;
2904
- const handlePrevious = previousPlaylist && visibleGenrePlaylists.includes(previousPlaylist) ? () => handleGenreSearchSelect(previousPlaylist) : void 0;
3110
+ const handlePrevious = selectionHistory.previous ? () => selectGenre(selectionHistory.previous) : void 0;
2905
3111
  const { showPopup, hidePopup } = (0, import_popup6.usePopup)();
2906
- const [searchText, setSearchText] = (0, import_react16.useState)("");
2907
- const artistGroup = useArtistSearchGroup(
2908
- artistBaseUrl,
2909
- searchText,
2910
- (artist) => {
2911
- openArtist(artist.uuid);
2912
- onArtistSelect?.(artist);
2913
- },
2914
- Boolean(artistSearch)
2915
- );
2916
- const songGroup = useSongSearchGroup(
2917
- songBaseUrl,
2918
- searchText,
2919
- (song) => {
2920
- openSong(song.uuid);
2921
- onSongSelect?.(song);
2922
- },
2923
- Boolean(songSearch)
2924
- );
2925
- const extraGroups = [...extraSearchGroups, artistGroup, songGroup].filter(
2926
- (group) => group !== void 0
2927
- );
3112
+ const reopenPanel = () => {
3113
+ setUncontrolledCollapsed(false);
3114
+ onCollapsedChange?.(false);
3115
+ };
2928
3116
  const handleSelectedNodeChange = (node) => {
3117
+ reopenPanel();
2929
3118
  if (node !== null && songUuid !== null) changeSong(null);
2930
3119
  selectNode(node);
2931
3120
  };
2932
3121
  const handleGenreSearchClear = clearSelection;
2933
- const handleGenreSearchSelect = (genrePlaylist) => {
3122
+ const handleGenreSearchSelect = (genrePlaylist2) => {
3123
+ reopenPanel();
2934
3124
  if (songUuid !== null) changeSong(null);
2935
- selectGenrePlaylist(genrePlaylist);
3125
+ selectGenrePlaylist(genrePlaylist2);
2936
3126
  };
2937
3127
  const selectedUuid = selectedGenrePlaylist?.uuid;
2938
- const selectedChildren = selectedUuid ? visibleGenrePlaylists.filter((genrePlaylist) => genrePlaylist.parent?.uuid === selectedUuid) : [];
3128
+ const selectedChildren = selectedUuid ? visibleGenrePlaylists.filter((genrePlaylist2) => genrePlaylist2.parent?.uuid === selectedUuid) : [];
2939
3129
  const canZoomIn = viewMode === "pop-core" && focus.onFocusGenreChange !== void 0 && selectedUuid !== void 0 && focus.focusGenreUuid !== selectedUuid && selectedChildren.length > 0 && selectedChildren.filter((child) => child.criteria?.side === "pop").length <= 1;
2940
3130
  const handleGenreSearchZoomIn = () => {
2941
3131
  if (selectedUuid) focus.onFocusGenreChange?.(selectedUuid, "zoom-in");
2942
3132
  };
2943
- const zoomInRef = (0, import_react16.useRef)(void 0);
3133
+ const zoomInRef = (0, import_react18.useRef)(void 0);
2944
3134
  zoomInRef.current = canZoomIn ? handleGenreSearchZoomIn : void 0;
2945
- const selectGenreFromPanel = (nodeId) => {
2946
- const genrePlaylist = genrePlaylistByNodeId.get(nodeId);
2947
- if (genrePlaylist) handleGenreSearchSelect(genrePlaylist);
2948
- };
2949
- const renderGenreBody = (relation) => {
2950
- const { node } = relation;
2951
- const family = /* @__PURE__ */ (0, import_jsx_runtime44.jsx)(
2952
- import_genre_tree_view4.GenreNodeFamily,
2953
- {
2954
- node,
2955
- fill: relation.fill,
2956
- textColor: relation.textColor,
2957
- parentNode: relation.parentNode,
2958
- childNodes: relation.childNodes,
2959
- onSelectNode: selectGenreFromPanel
2960
- }
2961
- );
2962
- const ancestors = /* @__PURE__ */ (0, import_jsx_runtime44.jsx)(import_genre_tree_view4.GenreNodeAncestors, { ancestorNodes: relation.ancestorNodes, onSelectNode: selectGenreFromPanel });
2963
- const genrePlaylist = genrePlaylistByNodeId.get(node.id);
2964
- const criteria = genrePlaylist?.criteria;
2965
- if (!genrePlaylist || !criteria) return { primary: /* @__PURE__ */ (0, import_jsx_runtime44.jsxs)(import_jsx_runtime44.Fragment, { children: [
2966
- family,
2967
- ancestors
2968
- ] }) };
2969
- const genreUuid = criteria.uuid;
2970
- const parentPlaylist = visibleGenrePlaylists.find(({ uuid }) => uuid === genrePlaylist.parent?.uuid);
2971
- const canToggleSide = !readOnly && parentPlaylist !== void 0 && parentPlaylist.parent === null;
2972
- const side = criteria.side ?? "core";
2973
- const shareTarget = {
2974
- kind: "genre",
2975
- genrePlaylistUuid: genrePlaylist.uuid,
2976
- name: criteria.name,
2977
- scope,
2978
- artistUuid
2979
- };
2980
- const shareUrl = getShareUrlRef.current?.(shareTarget) ?? void 0;
2981
- const popSibling = canToggleSide && side === "core" ? visibleGenrePlaylists.find(
2982
- (sibling) => sibling.parent?.uuid === parentPlaylist.uuid && sibling.uuid !== genrePlaylist.uuid && sibling.criteria?.side === "pop"
2983
- ) : void 0;
2984
- return {
2985
- primary: /* @__PURE__ */ (0, import_jsx_runtime44.jsxs)(import_jsx_runtime44.Fragment, { children: [
2986
- /* @__PURE__ */ (0, import_jsx_runtime44.jsx)(
2987
- GenreInfoPanelBody,
3135
+ const genrePlaylist = selectedGenrePlaylist;
3136
+ const criteria = genrePlaylist?.criteria;
3137
+ const parentPlaylist = visibleGenrePlaylists.find(({ uuid }) => uuid === genrePlaylist?.parent?.uuid);
3138
+ const canToggleSide = !readOnly && criteria !== void 0 && criteria !== null && parentPlaylist !== void 0 && parentPlaylist.parent === null;
3139
+ const side = criteria?.side ?? "core";
3140
+ const popSibling = canToggleSide && side === "core" ? visibleGenrePlaylists.find((sibling) => sibling.parent?.uuid === parentPlaylist.uuid && sibling.uuid !== genrePlaylist?.uuid && sibling.criteria?.side === "pop") : void 0;
3141
+ const shareTarget = selectedGenreUuid ? { kind: "genre", genreUuid: selectedGenreUuid, name: criteria?.name ?? "Genre", scope, artistUuid } : void 0;
3142
+ const shareUrl = shareTarget ? getShareUrlRef.current?.(shareTarget) ?? void 0 : void 0;
3143
+ const genreContent = useGenreDetailContent({
3144
+ genreUuid: selectedGenreUuid,
3145
+ scope,
3146
+ getBackendBaseUrl,
3147
+ onSelectGenre: (uuid) => {
3148
+ reopenPanel();
3149
+ selectGenre(uuid);
3150
+ },
3151
+ expanded: isExpanded,
3152
+ playback,
3153
+ genrePlaylist: selectedGenrePlaylist,
3154
+ band: {
3155
+ ...relations ? { node: relations.node } : {},
3156
+ shareTarget: shareUrl ? shareTarget : void 0,
3157
+ shareUrl,
3158
+ onZoomIn: zoomInRef.current,
3159
+ onAddChild: readOnly || !criteria ? void 0 : () => handleGenreCreationAction(criteria),
3160
+ onRename: readOnly || !criteria ? void 0 : () => handleGenreRenameAction(criteria),
3161
+ onChangeParent: readOnly || !genrePlaylist || !criteria ? void 0 : () => showPopup(
3162
+ /* @__PURE__ */ (0, import_jsx_runtime46.jsx)(
3163
+ ChangeParentPopup,
2988
3164
  {
2989
- family: /* @__PURE__ */ (0, import_jsx_runtime44.jsxs)(import_jsx_runtime44.Fragment, { children: [
2990
- family,
2991
- ancestors
2992
- ] }),
2993
- genreUuid,
2994
- genrePlaylist,
2995
- catalogue: genrePlaylist.catalogue,
2996
- wikidataId: criteria.wikidataId,
3165
+ genrePlaylist: { ...genrePlaylist, criteria },
3166
+ genrePlaylists: visibleGenrePlaylists,
2997
3167
  scope,
2998
3168
  getBackendBaseUrl,
2999
- playback,
3000
- band: {
3001
- node,
3002
- shareTarget: shareUrl ? shareTarget : void 0,
3003
- shareUrl,
3004
- onZoomIn: zoomInRef.current,
3005
- onAddChild: readOnly ? void 0 : () => handleGenreCreationAction(criteria),
3006
- onRename: readOnly ? void 0 : () => handleGenreRenameAction(criteria),
3007
- onChangeParent: readOnly ? void 0 : () => showPopup(
3008
- /* @__PURE__ */ (0, import_jsx_runtime44.jsx)(
3009
- ChangeParentPopup,
3010
- {
3011
- genrePlaylist: { ...genrePlaylist, criteria },
3012
- genrePlaylists: visibleGenrePlaylists,
3013
- scope,
3014
- getBackendBaseUrl,
3015
- onClose: hidePopup,
3016
- onParentChanged
3017
- }
3018
- )
3019
- ),
3020
- side,
3021
- onToggleSide: canToggleSide ? () => updateGenre({ uuid: genreUuid, data: { side: side === "pop" ? "core" : "pop" } }) : void 0,
3022
- toggleSideDisabledReason: popSibling ? `${popSibling.name} is already the pop side of ${parentPlaylist?.name}` : void 0,
3023
- additionalActions
3024
- },
3025
- extras: {
3026
- criteriaOverviewSchema,
3027
- renderGenreDetailExtras,
3028
- renderArtistLink,
3029
- renderSongLink: resolvedRenderSongLink,
3030
- artist: filterArtist,
3031
- renderSummary,
3032
- renderEssentialTracks,
3033
- renderSongLeading,
3034
- renderSongActions,
3035
- renderTracksToolbar
3036
- }
3169
+ onClose: hidePopup,
3170
+ onParentChanged
3037
3171
  }
3038
- ),
3039
- renderDangerZone?.(node)
3040
- ] })
3041
- };
3042
- };
3172
+ )
3173
+ ),
3174
+ side,
3175
+ onToggleSide: canToggleSide && criteria ? () => updateGenre({ uuid: criteria.uuid, data: { side: side === "pop" ? "core" : "pop" } }) : void 0,
3176
+ toggleSideDisabledReason: popSibling ? `${popSibling.name} is already the pop side of ${parentPlaylist?.name}` : void 0,
3177
+ additionalActions
3178
+ },
3179
+ extras: {
3180
+ criteriaOverviewSchema,
3181
+ renderGenreDetailExtras,
3182
+ renderArtistLink,
3183
+ renderSongLink: resolvedRenderSongLink,
3184
+ artist: filterArtist,
3185
+ renderSummary,
3186
+ renderEssentialTracks,
3187
+ renderSongLeading,
3188
+ renderSongActions,
3189
+ renderTracksToolbar
3190
+ },
3191
+ trailing: relations ? renderDangerZone?.(relations.node) : void 0
3192
+ });
3043
3193
  const isRotationControlled = controlledAllowWheelRotation !== void 0;
3044
3194
  const allowWheelRotation = controlledAllowWheelRotation ?? internalAllowWheelRotation;
3045
3195
  let activePanel = null;
@@ -3053,16 +3203,16 @@ function GenreTreeView({
3053
3203
  onClose: overlayPanel.onClose,
3054
3204
  compact: false
3055
3205
  };
3056
- } else if (relations) {
3057
- const { primary } = renderGenreBody(relations);
3206
+ } else if (selectedGenreUuid) {
3058
3207
  activePanel = {
3059
- title: relations.node.name,
3060
- content: primary,
3208
+ title: genreContent.title,
3209
+ content: genreContent.content,
3210
+ aside: genreContent.aside,
3061
3211
  expanded: isExpanded,
3062
3212
  onExpandedChange: changeExpanded,
3063
3213
  onClose: clearSelection,
3064
- compact: true,
3065
- headerAccent: { background: relations.fill, color: relations.textColor }
3214
+ compact: false,
3215
+ headerAccent: relations ? { background: relations.fill, color: relations.textColor } : genreContent.headerAccent
3066
3216
  };
3067
3217
  } else if (builtInPanel) {
3068
3218
  activePanel = {
@@ -3075,36 +3225,36 @@ function GenreTreeView({
3075
3225
  compact: true
3076
3226
  };
3077
3227
  }
3078
- const isPanelShown = isTreeReady && activePanel !== null;
3228
+ const isPanelShown = activePanel !== null && (selectedGenreUuid !== null || isTreeReady);
3079
3229
  const detailTab = placement === "regions" && activePanel?.onExpandedChange ? {
3080
3230
  edge: activePanel.expanded ? { label: "Shrink panel", onClick: () => activePanel.onExpandedChange?.(false) } : isCollapsed ? null : { label: "Hide panel", onClick: () => changeCollapsed(true) },
3081
3231
  center: activePanel.expanded ? null : isCollapsed ? { label: "Show panel", onClick: () => changeCollapsed(false) } : activePanel.onExpandedChange ? { label: "Extend panel", onClick: () => activePanel.onExpandedChange?.(true) } : null
3082
3232
  } : void 0;
3083
- const expandButton = placement === "default" && activePanel?.onExpandedChange ? /* @__PURE__ */ (0, import_jsx_runtime44.jsx)(
3084
- import_ui19.IconButton,
3233
+ const expandButton = placement === "default" && activePanel?.onExpandedChange ? /* @__PURE__ */ (0, import_jsx_runtime46.jsx)(
3234
+ import_ui20.IconButton,
3085
3235
  {
3086
3236
  "aria-label": activePanel.expanded ? "Shrink panel" : "Extend panel",
3087
3237
  style: activePanel.headerAccent && { color: "inherit" },
3088
3238
  onClick: () => activePanel.onExpandedChange?.(!activePanel.expanded),
3089
- children: activePanel.expanded ? /* @__PURE__ */ (0, import_jsx_runtime44.jsx)(import_lucide_react16.Minimize2, { "aria-hidden": "true", className: "h-4 w-4" }) : /* @__PURE__ */ (0, import_jsx_runtime44.jsx)(import_lucide_react16.Maximize2, { "aria-hidden": "true", className: "h-4 w-4" })
3239
+ children: activePanel.expanded ? /* @__PURE__ */ (0, import_jsx_runtime46.jsx)(import_lucide_react17.Minimize2, { "aria-hidden": "true", className: "h-4 w-4" }) : /* @__PURE__ */ (0, import_jsx_runtime46.jsx)(import_lucide_react17.Maximize2, { "aria-hidden": "true", className: "h-4 w-4" })
3090
3240
  }
3091
3241
  ) : null;
3092
3242
  const renderArtistFilter = () => {
3093
3243
  if (isLoading || !filterArtist) return null;
3094
- return /* @__PURE__ */ (0, import_jsx_runtime44.jsxs)("span", { className: "inline-flex items-center overflow-hidden rounded-full bg-[var(--color-neutral-100)] text-sm", children: [
3095
- /* @__PURE__ */ (0, import_jsx_runtime44.jsx)(
3244
+ return /* @__PURE__ */ (0, import_jsx_runtime46.jsxs)("span", { className: "inline-flex items-center overflow-hidden rounded-full bg-[var(--color-neutral-100)] text-sm", children: [
3245
+ /* @__PURE__ */ (0, import_jsx_runtime46.jsx)(
3096
3246
  "button",
3097
3247
  {
3098
3248
  type: "button",
3099
3249
  className: "py-1 pl-3 pr-2 [@media(pointer:coarse)]:min-h-[var(--layout-touch-target)] hover:underline",
3100
3250
  "aria-label": `Show artist ${filterArtist.name}`,
3101
3251
  onClick: () => {
3102
- if (selectedNodeId !== null) clearSelection();
3252
+ if (selectedGenreUuid !== null) clearSelection();
3103
3253
  },
3104
3254
  children: filterArtist.name
3105
3255
  }
3106
3256
  ),
3107
- /* @__PURE__ */ (0, import_jsx_runtime44.jsx)(
3257
+ /* @__PURE__ */ (0, import_jsx_runtime46.jsx)(
3108
3258
  "button",
3109
3259
  {
3110
3260
  type: "button",
@@ -3116,27 +3266,42 @@ function GenreTreeView({
3116
3266
  )
3117
3267
  ] });
3118
3268
  };
3119
- const actions = /* @__PURE__ */ (0, import_jsx_runtime44.jsxs)(import_jsx_runtime44.Fragment, { children: [
3120
- !isLoading && /* @__PURE__ */ (0, import_jsx_runtime44.jsx)(
3121
- GenreSearch,
3269
+ const actions = /* @__PURE__ */ (0, import_jsx_runtime46.jsxs)(import_jsx_runtime46.Fragment, { children: [
3270
+ !isLoading && /* @__PURE__ */ (0, import_jsx_runtime46.jsx)(
3271
+ GenreTreeSearch,
3122
3272
  {
3273
+ artistBaseUrl,
3274
+ songBaseUrl,
3275
+ artistSearch: Boolean(artistSearch),
3276
+ songSearch: Boolean(songSearch),
3277
+ onArtistSelect: (artist) => {
3278
+ openArtist(artist.uuid);
3279
+ onArtistSelect?.(artist);
3280
+ },
3281
+ onSongSelect: (song) => {
3282
+ openSong(song.uuid);
3283
+ onSongSelect?.(song);
3284
+ },
3285
+ SearchGroups,
3123
3286
  genrePlaylists: visibleGenrePlaylists,
3124
3287
  onSelect: handleGenreSearchSelect,
3125
- selectedName: selectedGenrePlaylist?.name ?? (typeof builtInPanel?.title === "string" ? builtInPanel.title : null),
3288
+ onSelectGenre: (uuid) => {
3289
+ reopenPanel();
3290
+ if (songUuid) changeSong(null);
3291
+ selectGenre(uuid);
3292
+ },
3293
+ selectedName: (selectedGenreUuid ? genreContent.title : null) ?? (typeof builtInPanel?.title === "string" ? builtInPanel.title : null),
3126
3294
  onClear: builtInSongPanel ? () => changeSong(null) : builtInArtistPanel ? () => changeArtist(null) : handleGenreSearchClear,
3127
3295
  onPrevious: handlePrevious,
3128
- onQueryChange: (text) => {
3129
- setSearchText(text);
3130
- onSearchQueryChange?.(text);
3131
- },
3296
+ onQueryChange: onSearchQueryChange,
3132
3297
  onFocus: onSearchFocus,
3133
- extraGroups
3298
+ extraGroups: extraSearchGroups
3134
3299
  }
3135
3300
  ),
3136
- !isLoading && filterArtist && /* @__PURE__ */ (0, import_jsx_runtime44.jsx)("div", { className: "hidden w-full justify-center max-md:flex", children: renderArtistFilter() }),
3137
- /* @__PURE__ */ (0, import_jsx_runtime44.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: [
3138
- !isLoading && !isControlled && /* @__PURE__ */ (0, import_jsx_runtime44.jsx)(
3139
- import_ui19.SegmentedControl,
3301
+ !isLoading && filterArtist && /* @__PURE__ */ (0, import_jsx_runtime46.jsx)("div", { className: "hidden w-full justify-center max-md:flex", children: renderArtistFilter() }),
3302
+ /* @__PURE__ */ (0, import_jsx_runtime46.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: [
3303
+ !isLoading && !isControlled && /* @__PURE__ */ (0, import_jsx_runtime46.jsx)(
3304
+ import_ui20.SegmentedControl,
3140
3305
  {
3141
3306
  "aria-label": "Tree view mode",
3142
3307
  value: viewMode,
@@ -3149,14 +3314,14 @@ function GenreTreeView({
3149
3314
  ]
3150
3315
  }
3151
3316
  ),
3152
- !isLoading && showDisplayOptions && /* @__PURE__ */ (0, import_jsx_runtime44.jsx)(
3317
+ !isLoading && showDisplayOptions && /* @__PURE__ */ (0, import_jsx_runtime46.jsx)(
3153
3318
  "div",
3154
3319
  {
3155
3320
  className: "flex items-center gap-1",
3156
3321
  role: "group",
3157
3322
  "aria-label": "Tree display options",
3158
- children: !isRotationControlled && viewMode !== "stacked" && viewMode !== "outline" && /* @__PURE__ */ (0, import_jsx_runtime44.jsx)(
3159
- import_ui19.Button,
3323
+ children: !isRotationControlled && viewMode !== "stacked" && viewMode !== "outline" && /* @__PURE__ */ (0, import_jsx_runtime46.jsx)(
3324
+ import_ui20.Button,
3160
3325
  {
3161
3326
  variant: allowWheelRotation ? "default" : "outline",
3162
3327
  size: "sm",
@@ -3166,44 +3331,44 @@ function GenreTreeView({
3166
3331
  )
3167
3332
  }
3168
3333
  ),
3169
- !isLoading && !readOnly && showAddRoot && /* @__PURE__ */ (0, import_jsx_runtime44.jsx)(
3170
- import_ui19.IconTextButton,
3334
+ !isLoading && !readOnly && showAddRoot && /* @__PURE__ */ (0, import_jsx_runtime46.jsx)(
3335
+ import_ui20.IconTextButton,
3171
3336
  {
3172
- icon: import_lucide_react15.Plus,
3337
+ icon: import_lucide_react16.Plus,
3173
3338
  text: "Add root",
3174
3339
  onClick: () => handleGenreCreationAction(null)
3175
3340
  }
3176
3341
  )
3177
3342
  ] })
3178
3343
  ] });
3179
- return /* @__PURE__ */ (0, import_jsx_runtime44.jsxs)(
3344
+ return /* @__PURE__ */ (0, import_jsx_runtime46.jsxs)(
3180
3345
  "div",
3181
3346
  {
3182
3347
  "data-left-panel": isPanelShown && !isCollapsed ? void 0 : "hidden",
3183
3348
  className: ["genre-tree-view relative flex flex-col h-full", placement === "regions" && REGIONS_CLASS, className2].filter(Boolean).join(" "),
3184
3349
  children: [
3185
- /* @__PURE__ */ (0, import_jsx_runtime44.jsx)(
3350
+ /* @__PURE__ */ (0, import_jsx_runtime46.jsx)(
3186
3351
  "div",
3187
3352
  {
3188
3353
  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"}`,
3189
3354
  children: actions
3190
3355
  }
3191
3356
  ),
3192
- !isLoading && filterArtist && /* @__PURE__ */ (0, import_jsx_runtime44.jsx)(
3357
+ !isLoading && filterArtist && /* @__PURE__ */ (0, import_jsx_runtime46.jsx)(
3193
3358
  "div",
3194
3359
  {
3195
3360
  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"}`,
3196
3361
  children: renderArtistFilter()
3197
3362
  }
3198
3363
  ),
3199
- /* @__PURE__ */ (0, import_jsx_runtime44.jsx)("div", { className: "content-container flex-1 min-h-0 flex flex-row gap-4", children: /* @__PURE__ */ (0, import_jsx_runtime44.jsxs)("div", { className: "tree-view-container relative flex-1 min-w-0 flex flex-col h-full", children: [
3200
- !isLoading && viewMode === "outline" && floatingControlsLeading && /* @__PURE__ */ (0, import_jsx_runtime44.jsx)("div", { className: "absolute bottom-3 right-3 z-10", children: floatingControlsLeading }),
3201
- isLoading ? /* @__PURE__ */ (0, import_jsx_runtime44.jsx)(import_genre_tree_view4.GenreTreeViewSkeleton, { viewMode }) : viewMode === "wheel" ? (
3364
+ /* @__PURE__ */ (0, import_jsx_runtime46.jsx)("div", { className: "content-container flex-1 min-h-0 flex flex-row gap-4", children: /* @__PURE__ */ (0, import_jsx_runtime46.jsxs)("div", { className: "tree-view-container relative flex-1 min-w-0 flex flex-col h-full", children: [
3365
+ !isLoading && viewMode === "outline" && floatingControlsLeading && /* @__PURE__ */ (0, import_jsx_runtime46.jsx)("div", { className: "absolute bottom-3 right-3 z-10", children: floatingControlsLeading }),
3366
+ isLoading ? /* @__PURE__ */ (0, import_jsx_runtime46.jsx)(import_genre_tree_view5.GenreTreeViewSkeleton, { viewMode }) : viewMode === "wheel" ? (
3202
3367
  // The info panel's top edge aligns with the tree's; the search bar floats `--layout-panel-inset` below it,
3203
3368
  // and the header inset (the bar's height plus that inset) extends the header's fill behind the bar,
3204
3369
  // Google Maps style; the 8px bottom padding mirrors the inset above the bar, and the hidden title's height is zeroed.
3205
3370
  // The panel's own Zoom in button is hidden: the search bar carries it.
3206
- /* @__PURE__ */ (0, import_jsx_runtime44.jsx)("div", { className: "tree-container search-overlay relative flex-1 w-full min-h-0 ", children: /* @__PURE__ */ (0, import_jsx_runtime44.jsx)(GenreTreeWheelHandoff, { skeleton: /* @__PURE__ */ (0, import_jsx_runtime44.jsx)(import_genre_tree_view4.GenreTreeWheelSkeleton, {}), onRevealedChange: setIsWheelRevealed, children: /* @__PURE__ */ (0, import_jsx_runtime44.jsx)(
3371
+ /* @__PURE__ */ (0, import_jsx_runtime46.jsx)("div", { className: "tree-container search-overlay relative flex-1 w-full min-h-0 ", children: /* @__PURE__ */ (0, import_jsx_runtime46.jsx)(GenreTreeWheelHandoff, { skeleton: /* @__PURE__ */ (0, import_jsx_runtime46.jsx)(import_genre_tree_view5.GenreTreeWheelSkeleton, {}), onRevealedChange: setIsWheelRevealed, children: /* @__PURE__ */ (0, import_jsx_runtime46.jsx)(
3207
3372
  GenrePlaylistTreeWheel,
3208
3373
  {
3209
3374
  scope,
@@ -3220,9 +3385,9 @@ function GenreTreeView({
3220
3385
  allowWheelRotation,
3221
3386
  sizeMetric: isArtistFiltered || !canSizeByViews ? "songs" : deferredSizeMetric,
3222
3387
  busy,
3223
- floatingControlsLeading: floatingControlsLeading || canSizeByViews ? /* @__PURE__ */ (0, import_jsx_runtime44.jsxs)("div", { className: "flex items-center gap-2", children: [
3388
+ floatingControlsLeading: floatingControlsLeading || canSizeByViews ? /* @__PURE__ */ (0, import_jsx_runtime46.jsxs)("div", { className: "flex items-center gap-2", children: [
3224
3389
  floatingControlsLeading,
3225
- canSizeByViews && /* @__PURE__ */ (0, import_jsx_runtime44.jsx)(SizeMetricToggle, { value: sizeMetric, onChange: changeSizeMetric })
3390
+ canSizeByViews && /* @__PURE__ */ (0, import_jsx_runtime46.jsx)(SizeMetricToggle, { value: sizeMetric, onChange: changeSizeMetric })
3226
3391
  ] }) : void 0,
3227
3392
  defaultViewport,
3228
3393
  viewportRestore,
@@ -3231,7 +3396,7 @@ function GenreTreeView({
3231
3396
  ) }) })
3232
3397
  ) : viewMode === "pop-core" ? (
3233
3398
  // Same as the wheel: the header inset extends its fill behind the floating search bar, Google Maps style.
3234
- /* @__PURE__ */ (0, import_jsx_runtime44.jsx)("div", { className: "tree-container search-overlay relative flex-1 w-full min-h-0 ", children: /* @__PURE__ */ (0, import_jsx_runtime44.jsx)(GenreTreeWheelHandoff, { skeleton: /* @__PURE__ */ (0, import_jsx_runtime44.jsx)(import_genre_tree_view4.GenreTreeWheelSkeleton, {}), onRevealedChange: setIsWheelRevealed, children: /* @__PURE__ */ (0, import_jsx_runtime44.jsx)(
3399
+ /* @__PURE__ */ (0, import_jsx_runtime46.jsx)("div", { className: "tree-container search-overlay relative flex-1 w-full min-h-0 ", children: /* @__PURE__ */ (0, import_jsx_runtime46.jsx)(GenreTreeWheelHandoff, { skeleton: /* @__PURE__ */ (0, import_jsx_runtime46.jsx)(import_genre_tree_view5.GenreTreeWheelSkeleton, {}), onRevealedChange: setIsWheelRevealed, children: /* @__PURE__ */ (0, import_jsx_runtime46.jsx)(
3235
3400
  GenrePlaylistTreeWheelRadialPopCore,
3236
3401
  {
3237
3402
  scope,
@@ -3248,9 +3413,9 @@ function GenreTreeView({
3248
3413
  allowWheelRotation,
3249
3414
  sizeMetric: isArtistFiltered || !canSizeByViews ? "songs" : deferredSizeMetric,
3250
3415
  busy,
3251
- floatingControlsLeading: floatingControlsLeading || canSizeByViews ? /* @__PURE__ */ (0, import_jsx_runtime44.jsxs)("div", { className: "flex items-center gap-2", children: [
3416
+ floatingControlsLeading: floatingControlsLeading || canSizeByViews ? /* @__PURE__ */ (0, import_jsx_runtime46.jsxs)("div", { className: "flex items-center gap-2", children: [
3252
3417
  floatingControlsLeading,
3253
- canSizeByViews && /* @__PURE__ */ (0, import_jsx_runtime44.jsx)(SizeMetricToggle, { value: sizeMetric, onChange: changeSizeMetric })
3418
+ canSizeByViews && /* @__PURE__ */ (0, import_jsx_runtime46.jsx)(SizeMetricToggle, { value: sizeMetric, onChange: changeSizeMetric })
3254
3419
  ] }) : void 0,
3255
3420
  defaultViewport,
3256
3421
  viewportRestore,
@@ -3258,7 +3423,7 @@ function GenreTreeView({
3258
3423
  ...focus
3259
3424
  }
3260
3425
  ) }) })
3261
- ) : viewMode === "outline" ? /* @__PURE__ */ (0, import_jsx_runtime44.jsx)("div", { className: "tree-container flex-1 min-h-0 w-full overflow-y-auto relative", children: /* @__PURE__ */ (0, import_jsx_runtime44.jsx)(
3426
+ ) : viewMode === "outline" ? /* @__PURE__ */ (0, import_jsx_runtime46.jsx)("div", { className: "tree-container flex-1 min-h-0 w-full overflow-y-auto relative", children: /* @__PURE__ */ (0, import_jsx_runtime46.jsx)(
3262
3427
  GenrePlaylistTreeWheelRadialPopCore,
3263
3428
  {
3264
3429
  outline: true,
@@ -3274,13 +3439,13 @@ function GenreTreeView({
3274
3439
  selectedNodeId,
3275
3440
  readOnly
3276
3441
  }
3277
- ) }) : /* @__PURE__ */ (0, import_jsx_runtime44.jsx)("div", { className: "tree-container relative flex flex-col gap-4 w-full pt-14 overflow-x-auto overflow-y-auto text-[var(--color-text)]", children: Object.entries(groupedGenrePlaylistsByRoot).map(
3442
+ ) }) : /* @__PURE__ */ (0, import_jsx_runtime46.jsx)("div", { className: "tree-container relative flex flex-col gap-4 w-full pt-14 overflow-x-auto overflow-y-auto text-[var(--color-text)]", children: Object.entries(groupedGenrePlaylistsByRoot).map(
3278
3443
  ([uuid, genrePlaylistTreePerRoot]) => {
3279
- return /* @__PURE__ */ (0, import_jsx_runtime44.jsx)(
3444
+ return /* @__PURE__ */ (0, import_jsx_runtime46.jsx)(
3280
3445
  "div",
3281
3446
  {
3282
3447
  className: "tree-per-root-container relative shrink-0 h-[28rem] mt-2 mr-16 p-2 bg-[var(--color-neutral-100)] rounded-lg",
3283
- children: /* @__PURE__ */ (0, import_jsx_runtime44.jsx)("div", { className: "graph-container relative z-10 h-full", children: /* @__PURE__ */ (0, import_jsx_runtime44.jsx)(
3448
+ children: /* @__PURE__ */ (0, import_jsx_runtime46.jsx)("div", { className: "graph-container relative z-10 h-full", children: /* @__PURE__ */ (0, import_jsx_runtime46.jsx)(
3284
3449
  GenrePlaylistTreePerRoot,
3285
3450
  {
3286
3451
  scope,
@@ -3306,8 +3471,8 @@ function GenreTreeView({
3306
3471
  );
3307
3472
  }
3308
3473
  ) }),
3309
- /* @__PURE__ */ (0, import_jsx_runtime44.jsx)(
3310
- import_ui19.DetailPanel,
3474
+ /* @__PURE__ */ (0, import_jsx_runtime46.jsx)(
3475
+ import_ui20.DetailPanel,
3311
3476
  {
3312
3477
  ref: panelRef,
3313
3478
  side: "left",
@@ -3317,13 +3482,13 @@ function GenreTreeView({
3317
3482
  headerAccent: activePanel?.headerAccent,
3318
3483
  headerActions: expandButton,
3319
3484
  onClose: activePanel?.onClose ?? noop,
3320
- collapsed: isCollapsed || !isTreeReady || !activePanel,
3485
+ collapsed: isCollapsed || !isPanelShown,
3321
3486
  expanded: activePanel?.expanded,
3322
3487
  onExpandedChange: activePanel?.onExpandedChange,
3323
3488
  aside: activePanel?.aside,
3324
3489
  edgeTab: detailTab,
3325
- headerClassName: activePanel?.compact ? "pt-[calc(var(--layout-search-h)+var(--layout-panel-inset)+0.5rem)]" : void 0,
3326
- children: activePanel?.aside && !activePanel.expanded ? /* @__PURE__ */ (0, import_jsx_runtime44.jsxs)(import_jsx_runtime44.Fragment, { children: [
3490
+ headerClassName: "pt-[calc(var(--layout-search-h)+var(--layout-panel-inset)+0.5rem)]",
3491
+ children: activePanel?.aside && !activePanel.expanded ? /* @__PURE__ */ (0, import_jsx_runtime46.jsxs)(import_jsx_runtime46.Fragment, { children: [
3327
3492
  activePanel.content,
3328
3493
  activePanel.aside
3329
3494
  ] }) : activePanel?.content
@@ -3335,145 +3500,17 @@ function GenreTreeView({
3335
3500
  );
3336
3501
  }
3337
3502
 
3338
- // src/genre-tree/GenreDetailPanel.tsx
3339
- var import_react17 = require("react");
3340
- var import_genre_tree_view5 = require("@behindthemusictree/genre-tree-view");
3341
- var import_ui20 = require("@behindthemusictree/ui");
3342
- var import_lucide_react17 = require("lucide-react");
3343
- var import_genre_tree28 = require("@behindthemusictree/app-kit/genre-tree");
3344
- var import_jsx_runtime45 = require("react/jsx-runtime");
3345
- var RELATIVE_ITEM_COUNT = 1;
3346
- function toNode(genre, parentId) {
3347
- return {
3348
- id: genre.uuid,
3349
- parentId,
3350
- name: genre.name,
3351
- itemCount: RELATIVE_ITEM_COUNT,
3352
- side: genre.side ?? void 0,
3353
- wikidataId: genre.wikidataId
3354
- };
3355
- }
3356
- function GenreDetailPanel({
3357
- genreUuid,
3358
- scope,
3359
- getBackendBaseUrl,
3360
- onClose,
3361
- onSelectGenre,
3362
- onZoomIn,
3363
- shareUrl,
3364
- shareTarget,
3365
- collapsed,
3366
- expanded,
3367
- onExpandedChange,
3368
- edgeTab,
3369
- extras
3370
- }) {
3371
- const { data: detail, isPending, isError } = (0, import_genre_tree28.useFetchGenreDetail)(genreUuid, scope, getBackendBaseUrl);
3372
- const playback = useGenrePlaylistPlayback({ scope, genrePlaylists: [] });
3373
- const view = (0, import_react17.useMemo)(() => {
3374
- if (!detail) return null;
3375
- const rootColor = (0, import_genre_tree_view5.getGenreTreeColor)(detail.root.wikidataId);
3376
- const relative = (genre, parentId) => ({
3377
- node: toNode(genre, parentId),
3378
- fill: (0, import_genre_tree_view5.tintSurface)(rootColor),
3379
- textColor: import_genre_tree_view5.TEXT_COLOR
3380
- });
3381
- const ancestors2 = [...detail.ascendants].sort((a, b) => b.degree - a.degree);
3382
- return {
3383
- node: {
3384
- id: detail.uuid,
3385
- parentId: detail.parent?.uuid ?? null,
3386
- name: detail.name,
3387
- itemCount: scope === "reference" ? (0, import_genre_tree28.genreMusicCount)(import_genre_tree28.GenrePlaylistSummarySchema.parse(detail.criteriaPlaylist)) : detail.tracksCount,
3388
- wikidataId: detail.wikidataId
3389
- },
3390
- fill: rootColor,
3391
- parentNode: detail.parent ? relative(detail.parent, null) : null,
3392
- childNodes: detail.children.map((child) => relative(child, detail.uuid)),
3393
- ancestorNodes: ancestors2.map(
3394
- ({ ascendant }, index) => relative(ascendant, index === 0 ? null : ancestors2[index - 1].ascendant.uuid)
3395
- )
3396
- };
3397
- }, [detail, scope]);
3398
- if (isError) {
3399
- return /* @__PURE__ */ (0, import_jsx_runtime45.jsx)("div", { role: "alert", className: "p-4 text-sm text-[var(--color-text)]", children: "This genre could not be loaded." });
3400
- }
3401
- if (isPending || !detail || !view) {
3402
- return /* @__PURE__ */ (0, import_jsx_runtime45.jsxs)("div", { className: "flex flex-col gap-3 p-4", "aria-busy": "true", "data-testid": "genre-detail-panel-skeleton", children: [
3403
- /* @__PURE__ */ (0, import_jsx_runtime45.jsx)(import_ui20.Skeleton, { className: "h-6 w-2/3" }),
3404
- /* @__PURE__ */ (0, import_jsx_runtime45.jsx)(import_ui20.Skeleton, { className: "h-4 w-full" }),
3405
- /* @__PURE__ */ (0, import_jsx_runtime45.jsx)(import_ui20.Skeleton, { className: "h-4 w-3/4" })
3406
- ] });
3407
- }
3408
- const selectGenre = (nodeId) => onSelectGenre(nodeId);
3409
- const family = /* @__PURE__ */ (0, import_jsx_runtime45.jsx)(
3410
- import_genre_tree_view5.GenreNodeFamily,
3411
- {
3412
- node: view.node,
3413
- fill: view.fill,
3414
- textColor: import_genre_tree_view5.ACCENT_TEXT_COLOR,
3415
- parentNode: view.parentNode,
3416
- childNodes: view.childNodes,
3417
- onSelectNode: selectGenre
3418
- }
3419
- );
3420
- const ancestors = /* @__PURE__ */ (0, import_jsx_runtime45.jsx)(import_genre_tree_view5.GenreNodeAncestors, { ancestorNodes: view.ancestorNodes, onSelectNode: selectGenre });
3421
- return /* @__PURE__ */ (0, import_jsx_runtime45.jsxs)(
3422
- import_ui20.DetailPanel,
3423
- {
3424
- side: "left",
3425
- title: view.node.name,
3426
- headerAccent: { background: view.fill, color: import_genre_tree_view5.ACCENT_TEXT_COLOR },
3427
- headerActions: onExpandedChange ? /* @__PURE__ */ (0, import_jsx_runtime45.jsx)(
3428
- import_ui20.IconButton,
3429
- {
3430
- "aria-label": expanded ? "Shrink panel" : "Extend panel",
3431
- style: { color: "inherit" },
3432
- onClick: () => onExpandedChange(!expanded),
3433
- children: expanded ? /* @__PURE__ */ (0, import_jsx_runtime45.jsx)(import_lucide_react17.Minimize2, { "aria-hidden": "true", className: "h-4 w-4" }) : /* @__PURE__ */ (0, import_jsx_runtime45.jsx)(import_lucide_react17.Maximize2, { "aria-hidden": "true", className: "h-4 w-4" })
3434
- }
3435
- ) : void 0,
3436
- onClose,
3437
- collapsed,
3438
- expanded,
3439
- onExpandedChange,
3440
- edgeTab,
3441
- children: [
3442
- /* @__PURE__ */ (0, import_jsx_runtime45.jsx)(
3443
- GenreInfoPanelBody,
3444
- {
3445
- family: /* @__PURE__ */ (0, import_jsx_runtime45.jsxs)(import_jsx_runtime45.Fragment, { children: [
3446
- family,
3447
- ancestors
3448
- ] }),
3449
- genreUuid: detail.uuid,
3450
- genrePlaylist: scope === "reference" ? import_genre_tree28.GenrePlaylistSummarySchema.parse(detail.criteriaPlaylist) : { ...detail.criteriaPlaylist, tracksCount: detail.tracksCount },
3451
- catalogue: detail.catalogue,
3452
- wikidataId: detail.wikidataId,
3453
- scope,
3454
- getBackendBaseUrl,
3455
- playback,
3456
- band: { node: view.node, onZoomIn, shareUrl, shareTarget: shareUrl ? shareTarget : void 0 },
3457
- extras
3458
- }
3459
- ),
3460
- !expanded && ancestors
3461
- ]
3462
- }
3463
- );
3464
- }
3465
-
3466
3503
  // src/genre-tree/index.ts
3467
3504
  var import_genre_tree_view6 = require("@behindthemusictree/genre-tree-view");
3468
3505
 
3469
3506
  // src/genre-tree/track-list-sidebar/TrackListSidebar.tsx
3470
- var import_react18 = require("react");
3507
+ var import_react19 = require("react");
3471
3508
  var import_lucide_react18 = require("lucide-react");
3472
3509
  var import_genre_tree29 = require("@behindthemusictree/app-kit/genre-tree");
3473
3510
  var import_genre_tree30 = require("@behindthemusictree/app-kit/genre-tree");
3474
3511
  var import_track_noun5 = require("@behindthemusictree/app-kit/track-noun");
3475
- var import_jsx_runtime46 = require("react/jsx-runtime");
3476
- var useBrowserLayoutEffect = typeof window === "undefined" ? import_react18.useEffect : import_react18.useLayoutEffect;
3512
+ var import_jsx_runtime47 = require("react/jsx-runtime");
3513
+ var useBrowserLayoutEffect = typeof window === "undefined" ? import_react19.useEffect : import_react19.useLayoutEffect;
3477
3514
  function TrackListSidebar({
3478
3515
  className: className2 = "",
3479
3516
  renderDuration,
@@ -3487,16 +3524,16 @@ function TrackListSidebar({
3487
3524
  }) {
3488
3525
  const { trackList, loadMore, autoAdvanceScrollRequest } = (0, import_genre_tree29.useTrackList)();
3489
3526
  const trackNoun = (0, import_track_noun5.useTrackNoun)();
3490
- const trackListRef = (0, import_react18.useRef)(null);
3491
- const previousOriginRef = (0, import_react18.useRef)(trackList?.origin ?? null);
3492
- const handledAutoAdvanceRequestIdRef = (0, import_react18.useRef)(null);
3493
- const shouldFollowPlaybackRef = (0, import_react18.useRef)(true);
3494
- const programmaticScrollTopRef = (0, import_react18.useRef)(null);
3495
- const userScrollIntentRef = (0, import_react18.useRef)(false);
3496
- const userScrollIntentTimerRef = (0, import_react18.useRef)(null);
3497
- const loadMoreSentinelRef = (0, import_react18.useRef)(null);
3527
+ const trackListRef = (0, import_react19.useRef)(null);
3528
+ const previousOriginRef = (0, import_react19.useRef)(trackList?.origin ?? null);
3529
+ const handledAutoAdvanceRequestIdRef = (0, import_react19.useRef)(null);
3530
+ const shouldFollowPlaybackRef = (0, import_react19.useRef)(true);
3531
+ const programmaticScrollTopRef = (0, import_react19.useRef)(null);
3532
+ const userScrollIntentRef = (0, import_react19.useRef)(false);
3533
+ const userScrollIntentTimerRef = (0, import_react19.useRef)(null);
3534
+ const loadMoreSentinelRef = (0, import_react19.useRef)(null);
3498
3535
  const hasMore = !!trackList && trackList.nextPage !== null;
3499
- (0, import_react18.useEffect)(() => {
3536
+ (0, import_react19.useEffect)(() => {
3500
3537
  const sentinel = loadMoreSentinelRef.current;
3501
3538
  if (!hasMore || !sentinel) return;
3502
3539
  const observer = new IntersectionObserver((entries) => {
@@ -3516,11 +3553,11 @@ function TrackListSidebar({
3516
3553
  programmaticScrollTopRef.current = null;
3517
3554
  if (trackListRef.current) trackListRef.current.scrollTop = 0;
3518
3555
  }, [trackList?.origin]);
3519
- (0, import_react18.useEffect)(() => {
3556
+ (0, import_react19.useEffect)(() => {
3520
3557
  const list = trackListRef.current;
3521
3558
  if (!isCollapsed && list?.scrollTop === 0) shouldFollowPlaybackRef.current = true;
3522
3559
  }, [isCollapsed]);
3523
- (0, import_react18.useEffect)(
3560
+ (0, import_react19.useEffect)(
3524
3561
  () => () => {
3525
3562
  if (userScrollIntentTimerRef.current !== null) clearTimeout(userScrollIntentTimerRef.current);
3526
3563
  },
@@ -3574,25 +3611,25 @@ function TrackListSidebar({
3574
3611
  }
3575
3612
  };
3576
3613
  const positionClasses = layout === "inline" ? "relative w-full flex h-full flex-col" : "fixed right-0 w-[50rem] flex flex-col";
3577
- return trackList ? /* @__PURE__ */ (0, import_jsx_runtime46.jsxs)(
3614
+ return trackList ? /* @__PURE__ */ (0, import_jsx_runtime47.jsxs)(
3578
3615
  "div",
3579
3616
  {
3580
3617
  "data-theme": "dark",
3581
3618
  className: `track-list-sidebar ${positionClasses} rounded-2xl bg-[var(--color-surface)] pb-1 ${className2}`,
3582
3619
  style: layout === "fixed" ? { top: "63.5px", maxHeight: "calc(100vh - 63.5px - 79px)" } : void 0,
3583
3620
  children: [
3584
- /* @__PURE__ */ (0, import_jsx_runtime46.jsxs)("div", { className: "header flex h-16 items-center px-4 py-2 text-[var(--color-text)]", children: [
3585
- /* @__PURE__ */ (0, import_jsx_runtime46.jsxs)("div", { className: "min-w-0 flex flex-1 flex-col justify-center", children: [
3586
- /* @__PURE__ */ (0, import_jsx_runtime46.jsxs)("div", { className: "origin flex text-xl", children: [
3587
- /* @__PURE__ */ (0, import_jsx_runtime46.jsx)("div", { className: "from h-auto flex flex-col justify-center items-center mr-2", children: "From" }),
3588
- /* @__PURE__ */ (0, import_jsx_runtime46.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_runtime46.jsx)("div", { className: "name max-w-full truncate", children: trackList.origin.label }) })
3621
+ /* @__PURE__ */ (0, import_jsx_runtime47.jsxs)("div", { className: "header flex h-16 items-center px-4 py-2 text-[var(--color-text)]", children: [
3622
+ /* @__PURE__ */ (0, import_jsx_runtime47.jsxs)("div", { className: "min-w-0 flex flex-1 flex-col justify-center", children: [
3623
+ /* @__PURE__ */ (0, import_jsx_runtime47.jsxs)("div", { className: "origin flex text-xl", children: [
3624
+ /* @__PURE__ */ (0, import_jsx_runtime47.jsx)("div", { className: "from h-auto flex flex-col justify-center items-center mr-2", children: "From" }),
3625
+ /* @__PURE__ */ (0, import_jsx_runtime47.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_runtime47.jsx)("div", { className: "name max-w-full truncate", children: trackList.origin.label }) })
3589
3626
  ] }),
3590
- /* @__PURE__ */ (0, import_jsx_runtime46.jsxs)("div", { className: "info flex items-center text-sm text-[var(--color-text-muted)]", children: [
3627
+ /* @__PURE__ */ (0, import_jsx_runtime47.jsxs)("div", { className: "info flex items-center text-sm text-[var(--color-text-muted)]", children: [
3591
3628
  trackList.origin.type === import_genre_tree30.TrackListOriginType.GENRE_PLAYLIST ? "Genre \u2022 " : trackList.origin.type === import_genre_tree30.TrackListOriginType.ARTIST ? "Artist \u2022 " : `${trackNoun.singular} playlist \u2022 `,
3592
3629
  `${trackList.total} ${trackList.total > 1 ? trackNoun.plural : trackNoun.singular}`
3593
3630
  ] })
3594
3631
  ] }),
3595
- (isCollapsed && onExpand || !isCollapsed && onMinimize) && /* @__PURE__ */ (0, import_jsx_runtime46.jsx)(
3632
+ (isCollapsed && onExpand || !isCollapsed && onMinimize) && /* @__PURE__ */ (0, import_jsx_runtime47.jsx)(
3596
3633
  "button",
3597
3634
  {
3598
3635
  type: "button",
@@ -3601,11 +3638,11 @@ function TrackListSidebar({
3601
3638
  "aria-expanded": !isCollapsed,
3602
3639
  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"}`,
3603
3640
  onClick: isCollapsed ? onExpand : onMinimize,
3604
- children: /* @__PURE__ */ (0, import_jsx_runtime46.jsx)(import_lucide_react18.ListMusic, { "aria-hidden": "true", className: "h-5 w-5" })
3641
+ children: /* @__PURE__ */ (0, import_jsx_runtime47.jsx)(import_lucide_react18.ListMusic, { "aria-hidden": "true", className: "h-5 w-5" })
3605
3642
  }
3606
3643
  )
3607
3644
  ] }),
3608
- !isCollapsed && /* @__PURE__ */ (0, import_jsx_runtime46.jsxs)(
3645
+ !isCollapsed && /* @__PURE__ */ (0, import_jsx_runtime47.jsxs)(
3609
3646
  "ul",
3610
3647
  {
3611
3648
  ref: trackListRef,
@@ -3615,7 +3652,7 @@ function TrackListSidebar({
3615
3652
  onPointerDown: markUserScrollIntent,
3616
3653
  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",
3617
3654
  children: [
3618
- trackList.tracks.map((track, index) => /* @__PURE__ */ (0, import_jsx_runtime46.jsx)("li", { "data-track-uuid": track.uuid, children: /* @__PURE__ */ (0, import_jsx_runtime46.jsx)(
3655
+ trackList.tracks.map((track, index) => /* @__PURE__ */ (0, import_jsx_runtime47.jsx)("li", { "data-track-uuid": track.uuid, children: /* @__PURE__ */ (0, import_jsx_runtime47.jsx)(
3619
3656
  TrackItem,
3620
3657
  {
3621
3658
  track,
@@ -3626,7 +3663,7 @@ function TrackListSidebar({
3626
3663
  renderSongLink
3627
3664
  }
3628
3665
  ) }, track.uuid)),
3629
- hasMore && /* @__PURE__ */ (0, import_jsx_runtime46.jsx)("li", { ref: loadMoreSentinelRef, "aria-hidden": "true", className: "h-px" })
3666
+ hasMore && /* @__PURE__ */ (0, import_jsx_runtime47.jsx)("li", { ref: loadMoreSentinelRef, "aria-hidden": "true", className: "h-px" })
3630
3667
  ]
3631
3668
  }
3632
3669
  )
@@ -3636,7 +3673,7 @@ function TrackListSidebar({
3636
3673
  }
3637
3674
 
3638
3675
  // src/genre-tree/PlayerPanel.tsx
3639
- var import_react19 = require("react");
3676
+ var import_react20 = require("react");
3640
3677
  var import_tailwind_merge = require("tailwind-merge");
3641
3678
  var import_player13 = require("@behindthemusictree/app-kit/player");
3642
3679
  var import_genre_tree32 = require("@behindthemusictree/app-kit/genre-tree");
@@ -3646,7 +3683,7 @@ var import_ui21 = require("@behindthemusictree/ui");
3646
3683
  var import_lucide_react19 = require("lucide-react");
3647
3684
  var import_genre_tree31 = require("@behindthemusictree/app-kit/genre-tree");
3648
3685
  var import_player12 = require("@behindthemusictree/app-kit/player");
3649
- var import_jsx_runtime47 = require("react/jsx-runtime");
3686
+ var import_jsx_runtime48 = require("react/jsx-runtime");
3650
3687
  function PlayerControls() {
3651
3688
  const {
3652
3689
  duration,
@@ -3665,15 +3702,15 @@ function PlayerControls() {
3665
3702
  const safeDuration = Number.isFinite(duration) && duration > 0 ? duration : 0;
3666
3703
  const safeCurrentTime = Number.isFinite(currentTime) ? Math.max(0, Math.min(currentTime, safeDuration)) : 0;
3667
3704
  const isPlaying = playState === import_player12.PlayStates.PLAYING;
3668
- return /* @__PURE__ */ (0, import_jsx_runtime47.jsxs)(
3705
+ return /* @__PURE__ */ (0, import_jsx_runtime48.jsxs)(
3669
3706
  "section",
3670
3707
  {
3671
3708
  "aria-label": "Player controls",
3672
3709
  className: "player-controls flex shrink-0 flex-col gap-2 border-b border-zinc-800 px-3 py-2 text-zinc-200",
3673
3710
  children: [
3674
- /* @__PURE__ */ (0, import_jsx_runtime47.jsxs)("div", { className: "flex min-w-0 items-center gap-2 text-xs tabular-nums text-zinc-400", children: [
3675
- /* @__PURE__ */ (0, import_jsx_runtime47.jsx)("span", { "aria-label": "Elapsed time", children: (0, import_genre_tree31.formatTime)(safeCurrentTime) }),
3676
- /* @__PURE__ */ (0, import_jsx_runtime47.jsx)(
3711
+ /* @__PURE__ */ (0, import_jsx_runtime48.jsxs)("div", { className: "flex min-w-0 items-center gap-2 text-xs tabular-nums text-zinc-400", children: [
3712
+ /* @__PURE__ */ (0, import_jsx_runtime48.jsx)("span", { "aria-label": "Elapsed time", children: (0, import_genre_tree31.formatTime)(safeCurrentTime) }),
3713
+ /* @__PURE__ */ (0, import_jsx_runtime48.jsx)(
3677
3714
  "input",
3678
3715
  {
3679
3716
  "aria-label": "Seek",
@@ -3687,11 +3724,11 @@ function PlayerControls() {
3687
3724
  onChange: (event) => seekTo(Number(event.currentTarget.value))
3688
3725
  }
3689
3726
  ),
3690
- /* @__PURE__ */ (0, import_jsx_runtime47.jsx)("span", { "aria-label": "Duration", children: (0, import_genre_tree31.formatTime)(safeDuration) })
3727
+ /* @__PURE__ */ (0, import_jsx_runtime48.jsx)("span", { "aria-label": "Duration", children: (0, import_genre_tree31.formatTime)(safeDuration) })
3691
3728
  ] }),
3692
- /* @__PURE__ */ (0, import_jsx_runtime47.jsxs)("div", { className: "grid min-h-9 grid-cols-[1fr_auto_1fr] items-center gap-3", children: [
3693
- /* @__PURE__ */ (0, import_jsx_runtime47.jsxs)("div", { className: "col-start-2 flex items-center justify-center gap-2", children: [
3694
- /* @__PURE__ */ (0, import_jsx_runtime47.jsx)(
3729
+ /* @__PURE__ */ (0, import_jsx_runtime48.jsxs)("div", { className: "grid min-h-9 grid-cols-[1fr_auto_1fr] items-center gap-3", children: [
3730
+ /* @__PURE__ */ (0, import_jsx_runtime48.jsxs)("div", { className: "col-start-2 flex items-center justify-center gap-2", children: [
3731
+ /* @__PURE__ */ (0, import_jsx_runtime48.jsx)(
3695
3732
  "button",
3696
3733
  {
3697
3734
  type: "button",
@@ -3700,10 +3737,10 @@ function PlayerControls() {
3700
3737
  disabled: !isReady || !hasPreviousTrack,
3701
3738
  onClick: playPreviousTrack,
3702
3739
  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",
3703
- children: /* @__PURE__ */ (0, import_jsx_runtime47.jsx)(import_lucide_react19.SkipBack, { "aria-hidden": "true", className: "h-4 w-4" })
3740
+ children: /* @__PURE__ */ (0, import_jsx_runtime48.jsx)(import_lucide_react19.SkipBack, { "aria-hidden": "true", className: "h-4 w-4" })
3704
3741
  }
3705
3742
  ),
3706
- /* @__PURE__ */ (0, import_jsx_runtime47.jsx)(
3743
+ /* @__PURE__ */ (0, import_jsx_runtime48.jsx)(
3707
3744
  "button",
3708
3745
  {
3709
3746
  type: "button",
@@ -3712,10 +3749,10 @@ function PlayerControls() {
3712
3749
  disabled: !isReady,
3713
3750
  onClick: handlePlayPauseAction,
3714
3751
  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",
3715
- children: isPlaying ? /* @__PURE__ */ (0, import_jsx_runtime47.jsx)(import_lucide_react19.Pause, { "aria-hidden": "true", className: "h-4 w-4", fill: "currentColor" }) : /* @__PURE__ */ (0, import_jsx_runtime47.jsx)(import_lucide_react19.Play, { "aria-hidden": "true", className: "h-4 w-4", fill: "currentColor" })
3752
+ children: isPlaying ? /* @__PURE__ */ (0, import_jsx_runtime48.jsx)(import_lucide_react19.Pause, { "aria-hidden": "true", className: "h-4 w-4", fill: "currentColor" }) : /* @__PURE__ */ (0, import_jsx_runtime48.jsx)(import_lucide_react19.Play, { "aria-hidden": "true", className: "h-4 w-4", fill: "currentColor" })
3716
3753
  }
3717
3754
  ),
3718
- /* @__PURE__ */ (0, import_jsx_runtime47.jsx)(
3755
+ /* @__PURE__ */ (0, import_jsx_runtime48.jsx)(
3719
3756
  "button",
3720
3757
  {
3721
3758
  type: "button",
@@ -3724,14 +3761,14 @@ function PlayerControls() {
3724
3761
  disabled: !isReady || !hasNextTrack,
3725
3762
  onClick: playNextTrack,
3726
3763
  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",
3727
- children: /* @__PURE__ */ (0, import_jsx_runtime47.jsx)(import_lucide_react19.SkipForward, { "aria-hidden": "true", className: "h-4 w-4" })
3764
+ children: /* @__PURE__ */ (0, import_jsx_runtime48.jsx)(import_lucide_react19.SkipForward, { "aria-hidden": "true", className: "h-4 w-4" })
3728
3765
  }
3729
3766
  )
3730
3767
  ] }),
3731
- /* @__PURE__ */ (0, import_jsx_runtime47.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: [
3732
- volume === 0 ? /* @__PURE__ */ (0, import_jsx_runtime47.jsx)(import_lucide_react19.VolumeX, { "aria-hidden": "true", className: "h-[0.7rem] w-[0.7rem] shrink-0" }) : /* @__PURE__ */ (0, import_jsx_runtime47.jsx)(import_lucide_react19.Volume2, { "aria-hidden": "true", className: "h-[0.7rem] w-[0.7rem] shrink-0" }),
3733
- /* @__PURE__ */ (0, import_jsx_runtime47.jsx)("span", { className: "sr-only", children: "Volume" }),
3734
- /* @__PURE__ */ (0, import_jsx_runtime47.jsx)(
3768
+ /* @__PURE__ */ (0, import_jsx_runtime48.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: [
3769
+ volume === 0 ? /* @__PURE__ */ (0, import_jsx_runtime48.jsx)(import_lucide_react19.VolumeX, { "aria-hidden": "true", className: "h-[0.7rem] w-[0.7rem] shrink-0" }) : /* @__PURE__ */ (0, import_jsx_runtime48.jsx)(import_lucide_react19.Volume2, { "aria-hidden": "true", className: "h-[0.7rem] w-[0.7rem] shrink-0" }),
3770
+ /* @__PURE__ */ (0, import_jsx_runtime48.jsx)("span", { className: "sr-only", children: "Volume" }),
3771
+ /* @__PURE__ */ (0, import_jsx_runtime48.jsx)(
3735
3772
  "input",
3736
3773
  {
3737
3774
  "aria-label": "Volume",
@@ -3753,7 +3790,7 @@ function PlayerControls() {
3753
3790
  }
3754
3791
 
3755
3792
  // src/genre-tree/PlayerPanel.tsx
3756
- var import_jsx_runtime48 = require("react/jsx-runtime");
3793
+ var import_jsx_runtime49 = require("react/jsx-runtime");
3757
3794
  var PANEL_ID = "player-panel";
3758
3795
  var DEFAULT_EXTENDED_WIDTH = "calc(100vw - var(--layout-center-left) - var(--layout-gutter))";
3759
3796
  function PlayerPanel({
@@ -3767,12 +3804,12 @@ function PlayerPanel({
3767
3804
  }) {
3768
3805
  const { playerTrackObject, retryPlayback } = (0, import_player13.usePlayer)();
3769
3806
  const { toggleTrackListSidebar, isTrackListSidebarVisible } = (0, import_genre_tree32.useTrackListSidebarVisibility)();
3770
- const [lastLoadedTrackTitle, setLastLoadedTrackTitle] = (0, import_react19.useState)("");
3807
+ const [lastLoadedTrackTitle, setLastLoadedTrackTitle] = (0, import_react20.useState)("");
3771
3808
  const loadedTrackTitle = playerTrackObject?.track.title;
3772
- (0, import_react19.useEffect)(() => {
3809
+ (0, import_react20.useEffect)(() => {
3773
3810
  if (loadedTrackTitle) setLastLoadedTrackTitle(loadedTrackTitle);
3774
3811
  }, [loadedTrackTitle]);
3775
- const [uncontrolledExtended, setUncontrolledExtended] = (0, import_react19.useState)(false);
3812
+ const [uncontrolledExtended, setUncontrolledExtended] = (0, import_react20.useState)(false);
3776
3813
  const isExtendRequested = expanded ?? uncontrolledExtended;
3777
3814
  const setIsExtendRequested = (next) => {
3778
3815
  setUncontrolledExtended(next);
@@ -3793,7 +3830,7 @@ function PlayerPanel({
3793
3830
  onClick: () => setIsExtendRequested(true),
3794
3831
  expanded: false
3795
3832
  } : null;
3796
- return /* @__PURE__ */ (0, import_jsx_runtime48.jsxs)(
3833
+ return /* @__PURE__ */ (0, import_jsx_runtime49.jsxs)(
3797
3834
  "div",
3798
3835
  {
3799
3836
  id: PANEL_ID,
@@ -3806,7 +3843,7 @@ function PlayerPanel({
3806
3843
  ),
3807
3844
  style: isExtended ? { width: extendedWidth } : void 0,
3808
3845
  children: [
3809
- /* @__PURE__ */ (0, import_jsx_runtime48.jsx)(
3846
+ /* @__PURE__ */ (0, import_jsx_runtime49.jsx)(
3810
3847
  import_ui21.PanelEdgeTab,
3811
3848
  {
3812
3849
  side: "right",
@@ -3816,7 +3853,7 @@ function PlayerPanel({
3816
3853
  className: "absolute right-full top-1/2 -translate-y-1/2"
3817
3854
  }
3818
3855
  ),
3819
- /* @__PURE__ */ (0, import_jsx_runtime48.jsxs)(
3856
+ /* @__PURE__ */ (0, import_jsx_runtime49.jsxs)(
3820
3857
  "div",
3821
3858
  {
3822
3859
  className: (0, import_tailwind_merge.twMerge)(
@@ -3824,20 +3861,20 @@ function PlayerPanel({
3824
3861
  isExtended ? "flex-row" : "flex-col"
3825
3862
  ),
3826
3863
  children: [
3827
- /* @__PURE__ */ (0, import_jsx_runtime48.jsxs)("div", { className: "flex w-[var(--layout-right-w)] shrink-0 flex-col", children: [
3828
- /* @__PURE__ */ (0, import_jsx_runtime48.jsx)("div", { className: "flex shrink-0 items-center justify-between gap-2 border-b border-zinc-800 p-2", children: /* @__PURE__ */ (0, import_jsx_runtime48.jsx)("span", { className: "min-w-0 truncate text-sm font-semibold", children: loadedTrackTitle || lastLoadedTrackTitle }) }),
3829
- /* @__PURE__ */ (0, import_jsx_runtime48.jsxs)("div", { className: "relative w-full shrink-0", children: [
3830
- /* @__PURE__ */ (0, import_jsx_runtime48.jsx)(import_player13.PlayerVideoSurface, { className: "aspect-video w-full bg-black" }),
3831
- playerTrackObject?.loadError && /* @__PURE__ */ (0, import_jsx_runtime48.jsxs)("span", { className: "absolute bottom-2 left-2 right-2 text-xs text-red-400", children: [
3864
+ /* @__PURE__ */ (0, import_jsx_runtime49.jsxs)("div", { className: "flex w-[var(--layout-right-w)] shrink-0 flex-col", children: [
3865
+ /* @__PURE__ */ (0, import_jsx_runtime49.jsx)("div", { className: "flex shrink-0 items-center justify-between gap-2 border-b border-zinc-800 p-2", children: /* @__PURE__ */ (0, import_jsx_runtime49.jsx)("span", { className: "min-w-0 truncate text-sm font-semibold", children: loadedTrackTitle || lastLoadedTrackTitle }) }),
3866
+ /* @__PURE__ */ (0, import_jsx_runtime49.jsxs)("div", { className: "relative w-full shrink-0", children: [
3867
+ /* @__PURE__ */ (0, import_jsx_runtime49.jsx)(import_player13.PlayerVideoSurface, { className: "aspect-video w-full bg-black" }),
3868
+ playerTrackObject?.loadError && /* @__PURE__ */ (0, import_jsx_runtime49.jsxs)("span", { className: "absolute bottom-2 left-2 right-2 text-xs text-red-400", children: [
3832
3869
  playerTrackObject.loadError,
3833
- /* @__PURE__ */ (0, import_jsx_runtime48.jsx)("button", { type: "button", className: "ml-2 underline", onClick: () => {
3870
+ /* @__PURE__ */ (0, import_jsx_runtime49.jsx)("button", { type: "button", className: "ml-2 underline", onClick: () => {
3834
3871
  void retryPlayback();
3835
3872
  }, children: "Retry" })
3836
3873
  ] })
3837
3874
  ] }),
3838
- /* @__PURE__ */ (0, import_jsx_runtime48.jsx)(PlayerControls, {})
3875
+ /* @__PURE__ */ (0, import_jsx_runtime49.jsx)(PlayerControls, {})
3839
3876
  ] }),
3840
- /* @__PURE__ */ (0, import_jsx_runtime48.jsx)(
3877
+ /* @__PURE__ */ (0, import_jsx_runtime49.jsx)(
3841
3878
  TrackListSidebar,
3842
3879
  {
3843
3880
  layout: "inline",
@@ -3864,11 +3901,11 @@ function PlayerPanel({
3864
3901
  var import_uuid = require("uuid");
3865
3902
  var import_ai = require("react-icons/ai");
3866
3903
  var import_genre_tree33 = require("@behindthemusictree/app-kit/genre-tree");
3867
- var import_jsx_runtime49 = require("react/jsx-runtime");
3904
+ var import_jsx_runtime50 = require("react/jsx-runtime");
3868
3905
  function Rating({ rating, handleChange }) {
3869
3906
  const instanceId = (0, import_uuid.v4)();
3870
- return /* @__PURE__ */ (0, import_jsx_runtime49.jsx)("div", { className: "flex items-center", children: [...Array(6)].map((_, i) => /* @__PURE__ */ (0, import_jsx_runtime49.jsxs)("div", { className: `relative ${i === 0 ? "mr-4" : "mr-1"}`, children: [
3871
- /* @__PURE__ */ (0, import_jsx_runtime49.jsx)(
3907
+ return /* @__PURE__ */ (0, import_jsx_runtime50.jsx)("div", { className: "flex items-center", children: [...Array(6)].map((_, i) => /* @__PURE__ */ (0, import_jsx_runtime50.jsxs)("div", { className: `relative ${i === 0 ? "mr-4" : "mr-1"}`, children: [
3908
+ /* @__PURE__ */ (0, import_jsx_runtime50.jsx)(
3872
3909
  "input",
3873
3910
  {
3874
3911
  id: `star-${instanceId}-${i}`,
@@ -3880,16 +3917,16 @@ function Rating({ rating, handleChange }) {
3880
3917
  onChange: handleChange
3881
3918
  }
3882
3919
  ),
3883
- /* @__PURE__ */ (0, import_jsx_runtime49.jsx)("label", { htmlFor: `star-${instanceId}-${i}`, className: "cursor-pointer", children: i === 0 ? /* @__PURE__ */ (0, import_jsx_runtime49.jsxs)("div", { className: "flex", children: [
3884
- /* @__PURE__ */ (0, import_jsx_runtime49.jsx)(
3920
+ /* @__PURE__ */ (0, import_jsx_runtime50.jsx)("label", { htmlFor: `star-${instanceId}-${i}`, className: "cursor-pointer", children: i === 0 ? /* @__PURE__ */ (0, import_jsx_runtime50.jsxs)("div", { className: "flex", children: [
3921
+ /* @__PURE__ */ (0, import_jsx_runtime50.jsx)(
3885
3922
  import_ai.AiOutlineStar,
3886
3923
  {
3887
3924
  className: `w-7 h-7 absolute top-0 left-0 ${rating === import_genre_tree33.FORM_RATING_NULL_VALUE ? "text-[var(--color-accent-rating)]" : "text-[var(--color-text-muted)]"}`
3888
3925
  }
3889
3926
  ),
3890
- /* @__PURE__ */ (0, import_jsx_runtime49.jsx)("span", { className: "flex-grow w-8" }),
3927
+ /* @__PURE__ */ (0, import_jsx_runtime50.jsx)("span", { className: "flex-grow w-8" }),
3891
3928
  " "
3892
- ] }) : /* @__PURE__ */ (0, import_jsx_runtime49.jsx)(
3929
+ ] }) : /* @__PURE__ */ (0, import_jsx_runtime50.jsx)(
3893
3930
  import_ai.AiFillStar,
3894
3931
  {
3895
3932
  className: `w-7 h-7 absolute top-0 left-0 ${(rating ?? -1) >= i ? "text-[var(--color-accent-rating)]" : "text-[var(--color-text-muted)]"}`