@behindthemusictree/app-kit-ui 30.0.0 → 32.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.
@@ -30,20 +30,19 @@ __export(genre_tree_exports, {
30
30
  GenreTreeView: () => GenreTreeView,
31
31
  GenreTreeViewSkeleton: () => import_genre_tree_view6.GenreTreeViewSkeleton,
32
32
  GenreTreeWheelSkeleton: () => import_genre_tree_view6.GenreTreeWheelSkeleton,
33
- PanelEdgeTab: () => PanelEdgeTab,
34
33
  PlayerPanel: () => PlayerPanel,
35
34
  Rating: () => Rating,
36
35
  TrackItem: () => TrackItem,
37
36
  TrackListSidebar: () => TrackListSidebar,
38
37
  TrackPositionPlayPause: () => TrackPositionPlayPause,
39
- useGenrePlaylistPlayback: () => useGenrePlaylistPlayback,
40
- useLeftDetailPanelLayout: () => useLeftDetailPanelLayout
38
+ useGenrePlaylistPlayback: () => useGenrePlaylistPlayback
41
39
  });
42
40
  module.exports = __toCommonJS(genre_tree_exports);
43
41
 
44
42
  // src/genre-tree/GenreTreeView.tsx
45
- var import_react17 = require("react");
43
+ var import_react16 = require("react");
46
44
  var import_react_query3 = require("@tanstack/react-query");
45
+ var import_lucide_react11 = require("lucide-react");
47
46
  var import_lucide_react12 = require("lucide-react");
48
47
  var import_ui15 = require("@behindthemusictree/ui");
49
48
  var import_genre_tree_view4 = require("@behindthemusictree/genre-tree-view");
@@ -51,6 +50,32 @@ var import_genre_tree18 = require("@behindthemusictree/app-kit/genre-tree");
51
50
  var import_genre_tree19 = require("@behindthemusictree/app-kit/genre-tree");
52
51
  var import_popup5 = require("@behindthemusictree/app-kit/popup");
53
52
 
53
+ // src/genre-tree/playlist-tree/size-metric.ts
54
+ var hasViewsCount = (genrePlaylists) => genrePlaylists.every((genrePlaylist) => genrePlaylist.viewsCount !== void 0);
55
+ var genrePlaylistSize = (genrePlaylist, metric) => {
56
+ if (metric === "songs") return genrePlaylist.tracksCount;
57
+ if (genrePlaylist.viewsCount === void 0) {
58
+ throw new Error(`Genre playlist ${genrePlaylist.uuid} has no viewsCount to size by views`);
59
+ }
60
+ return genrePlaylist.viewsCount;
61
+ };
62
+
63
+ // src/genre-tree/playlist-tree/toTreeNodes.ts
64
+ function toTreeNodes(genrePlaylists, sizeMetric) {
65
+ return genrePlaylists.map((genrePlaylist) => ({
66
+ id: genrePlaylist.uuid,
67
+ parentId: genrePlaylist.parent?.uuid ?? null,
68
+ name: genrePlaylist.name,
69
+ itemCount: genrePlaylistSize(genrePlaylist, sizeMetric),
70
+ actionable: Boolean(genrePlaylist.criteria),
71
+ detached: !genrePlaylist.criteria,
72
+ side: genrePlaylist.criteria?.side ?? void 0,
73
+ wikidataId: genrePlaylist.criteria?.wikidataId ?? null,
74
+ umbrella: genrePlaylist.isUmbrella,
75
+ scene: genrePlaylist.scene ?? void 0
76
+ }));
77
+ }
78
+
54
79
  // src/genre-tree/playlist-tree/TreePerRoot.tsx
55
80
  var import_react3 = require("react");
56
81
  var import_genre_tree_view = require("@behindthemusictree/genre-tree-view");
@@ -303,7 +328,6 @@ function useGenrePlaylistPlayback({
303
328
  }
304
329
 
305
330
  // src/genre-tree/playlist-tree/TreePerRoot.tsx
306
- var import_track_noun = require("@behindthemusictree/app-kit/track-noun");
307
331
  var import_jsx_runtime5 = require("react/jsx-runtime");
308
332
  function GenrePlaylistTreePerRoot({
309
333
  scope,
@@ -315,24 +339,19 @@ function GenrePlaylistTreePerRoot({
315
339
  additionalActions,
316
340
  onNodeClick,
317
341
  onNodeHover,
318
- renderInfoPanelBody,
319
342
  selectedNodeId,
320
343
  onSelectedNodeChange,
321
- hideInfoPanelClose,
322
- hideInfoPanelTitle,
323
344
  readOnly = false,
324
- showToolbar,
325
345
  wheelZoom,
326
346
  defaultViewport,
327
347
  viewportRestore,
328
348
  onViewportChange,
329
349
  playback,
330
- ...infoPanelCuration
350
+ ...hostPanel
331
351
  }) {
332
352
  const { isPlaying, setIsPlaying } = (0, import_player.usePlayer)();
333
353
  const defaultPlayback = useGenrePlaylistPlayback({ scope, genrePlaylists: genrePlaylistTreePerRoot });
334
354
  const { playingNodeId, play } = playback ?? defaultPlayback;
335
- const trackNoun = (0, import_track_noun.useTrackNoun)();
336
355
  const { showPopup } = (0, import_popup.usePopup)();
337
356
  const nodes = (0, import_react3.useMemo)(
338
357
  () => genrePlaylistTreePerRoot.map((genrePlaylist) => ({
@@ -397,7 +416,6 @@ function GenrePlaylistTreePerRoot({
397
416
  {
398
417
  className: className2,
399
418
  nodes,
400
- itemCountLabel: `${(0, import_track_noun.capitalize)(trackNoun.singular)} count`,
401
419
  rootColor: (0, import_genre_tree_view.getGenreTreeColor)(rootUuid),
402
420
  playingNodeId,
403
421
  playState: isPlaying ? "playing" : "paused",
@@ -408,18 +426,13 @@ function GenrePlaylistTreePerRoot({
408
426
  additionalActions,
409
427
  onNodeClick,
410
428
  onNodeHover,
411
- renderInfoPanelBody,
412
429
  selectedNodeId,
413
430
  onSelectedNodeChange,
414
- hideInfoPanelClose,
415
- hideInfoPanelTitle,
416
- showToolbar,
417
431
  wheelZoom,
418
432
  defaultViewport,
419
433
  viewportRestore,
420
434
  onViewportChange,
421
- ...infoPanelCuration,
422
- readOnly
435
+ ...hostPanel
423
436
  }
424
437
  );
425
438
  }
@@ -430,19 +443,6 @@ var import_genre_tree_view2 = require("@behindthemusictree/genre-tree-view");
430
443
  var import_player2 = require("@behindthemusictree/app-kit/player");
431
444
  var import_popup2 = require("@behindthemusictree/app-kit/popup");
432
445
  var import_transport2 = require("@behindthemusictree/app-kit/transport");
433
-
434
- // src/genre-tree/playlist-tree/size-metric.ts
435
- var hasViewsCount = (genrePlaylists) => genrePlaylists.every((genrePlaylist) => genrePlaylist.viewsCount !== void 0);
436
- var genrePlaylistSize = (genrePlaylist, metric) => {
437
- if (metric === "songs") return genrePlaylist.tracksCount;
438
- if (genrePlaylist.viewsCount === void 0) {
439
- throw new Error(`Genre playlist ${genrePlaylist.uuid} has no viewsCount to size by views`);
440
- }
441
- return genrePlaylist.viewsCount;
442
- };
443
-
444
- // src/genre-tree/playlist-tree/TreeWheel.tsx
445
- var import_track_noun2 = require("@behindthemusictree/app-kit/track-noun");
446
446
  var import_jsx_runtime6 = require("react/jsx-runtime");
447
447
  function GenrePlaylistTreeWheel({
448
448
  scope,
@@ -453,14 +453,10 @@ function GenrePlaylistTreeWheel({
453
453
  additionalActions,
454
454
  onNodeClick,
455
455
  onNodeHover,
456
- renderInfoPanelBody,
457
456
  selectedNodeId,
458
457
  onSelectedNodeChange,
459
- hideInfoPanelClose,
460
- hideInfoPanelTitle,
461
458
  readOnly = false,
462
459
  allowWheelRotation,
463
- showToolbar,
464
460
  sizeMetric = "songs",
465
461
  floatingControlsLeading,
466
462
  busy,
@@ -468,12 +464,11 @@ function GenrePlaylistTreeWheel({
468
464
  viewportRestore,
469
465
  onViewportChange,
470
466
  playback,
471
- ...infoPanelCuration
467
+ ...hostPanel
472
468
  }) {
473
469
  const { isPlaying, setIsPlaying } = (0, import_player2.usePlayer)();
474
470
  const defaultPlayback = useGenrePlaylistPlayback({ scope, genrePlaylists });
475
471
  const { playingNodeId, play } = playback ?? defaultPlayback;
476
- const trackNoun = (0, import_track_noun2.useTrackNoun)();
477
472
  const { showPopup } = (0, import_popup2.usePopup)();
478
473
  const nodes = (0, import_react4.useMemo)(
479
474
  () => genrePlaylists.map((genrePlaylist) => ({
@@ -537,7 +532,6 @@ function GenrePlaylistTreeWheel({
537
532
  {
538
533
  className: className2,
539
534
  nodes,
540
- itemCountLabel: sizeMetric === "views" ? "Views" : `${(0, import_track_noun2.capitalize)(trackNoun.singular)} count`,
541
535
  playingNodeId,
542
536
  playState: isPlaying ? "playing" : "paused",
543
537
  onPlayPause: handlePlayPause,
@@ -547,20 +541,15 @@ function GenrePlaylistTreeWheel({
547
541
  additionalActions,
548
542
  onNodeClick,
549
543
  onNodeHover,
550
- renderInfoPanelBody,
551
544
  selectedNodeId,
552
545
  onSelectedNodeChange,
553
- hideInfoPanelClose,
554
- hideInfoPanelTitle,
555
546
  allowWheelRotation,
556
- showToolbar,
557
547
  floatingControlsLeading,
558
548
  busy,
559
549
  defaultViewport,
560
550
  viewportRestore,
561
551
  onViewportChange,
562
- ...infoPanelCuration,
563
- readOnly
552
+ ...hostPanel
564
553
  }
565
554
  );
566
555
  }
@@ -569,7 +558,6 @@ function GenrePlaylistTreeWheel({
569
558
  var import_react5 = require("react");
570
559
  var import_genre_tree_view3 = require("@behindthemusictree/genre-tree-view");
571
560
  var import_player3 = require("@behindthemusictree/app-kit/player");
572
- var import_track_noun3 = require("@behindthemusictree/app-kit/track-noun");
573
561
  var import_jsx_runtime7 = require("react/jsx-runtime");
574
562
  function GenrePlaylistTreeWheelRadialPopCore(props) {
575
563
  const {
@@ -581,14 +569,10 @@ function GenrePlaylistTreeWheelRadialPopCore(props) {
581
569
  additionalActions,
582
570
  onNodeClick,
583
571
  onNodeHover,
584
- renderInfoPanelBody,
585
572
  selectedNodeId,
586
573
  onSelectedNodeChange,
587
- hideInfoPanelClose,
588
- hideInfoPanelTitle,
589
574
  readOnly = false,
590
575
  allowWheelRotation,
591
- showToolbar,
592
576
  sizeMetric = "songs",
593
577
  floatingControlsLeading,
594
578
  busy,
@@ -599,33 +583,14 @@ function GenrePlaylistTreeWheelRadialPopCore(props) {
599
583
  playback
600
584
  } = props;
601
585
  const focus = props;
602
- const infoPanelCuration = {
603
- renderDangerZone: props.renderDangerZone,
604
- expanded: props.expanded,
605
- onExpandedChange: props.onExpandedChange,
606
- collapsed: props.collapsed,
607
- hideExpandButton: props.hideExpandButton,
608
- overlayPanel: props.overlayPanel
586
+ const hostPanel = {
587
+ obscuredInset: props.obscuredInset,
588
+ onNodeStyleResolverChange: props.onNodeStyleResolverChange
609
589
  };
610
590
  const { isPlaying, setIsPlaying } = (0, import_player3.usePlayer)();
611
591
  const defaultPlayback = useGenrePlaylistPlayback({ scope, genrePlaylists });
612
592
  const { playingNodeId, play } = playback ?? defaultPlayback;
613
- const trackNoun = (0, import_track_noun3.useTrackNoun)();
614
- const nodes = (0, import_react5.useMemo)(
615
- () => genrePlaylists.map((genrePlaylist) => ({
616
- id: genrePlaylist.uuid,
617
- parentId: genrePlaylist.parent?.uuid ?? null,
618
- name: genrePlaylist.name,
619
- itemCount: genrePlaylistSize(genrePlaylist, sizeMetric),
620
- actionable: Boolean(genrePlaylist.criteria),
621
- detached: !genrePlaylist.criteria,
622
- side: genrePlaylist.criteria?.side ?? void 0,
623
- wikidataId: genrePlaylist.criteria?.wikidataId ?? null,
624
- umbrella: genrePlaylist.isUmbrella,
625
- scene: genrePlaylist.scene ?? void 0
626
- })),
627
- [genrePlaylists, sizeMetric]
628
- );
593
+ const nodes = (0, import_react5.useMemo)(() => toTreeNodes(genrePlaylists, sizeMetric), [genrePlaylists, sizeMetric]);
629
594
  const handlePlayPause = (0, import_react5.useCallback)(
630
595
  (nodeId) => {
631
596
  const genrePlaylist = genrePlaylists.find((g) => g.uuid === nodeId);
@@ -666,7 +631,6 @@ function GenrePlaylistTreeWheelRadialPopCore(props) {
666
631
  const treeProps = {
667
632
  className: className2,
668
633
  nodes,
669
- itemCountLabel: sizeMetric === "views" ? "Views" : `${(0, import_track_noun3.capitalize)(trackNoun.singular)} count`,
670
634
  playingNodeId,
671
635
  playState: isPlaying ? "playing" : "paused",
672
636
  onPlayPause: handlePlayPause,
@@ -676,14 +640,9 @@ function GenrePlaylistTreeWheelRadialPopCore(props) {
676
640
  additionalActions,
677
641
  onNodeClick,
678
642
  onNodeHover,
679
- renderInfoPanelBody,
680
643
  selectedNodeId,
681
644
  onSelectedNodeChange,
682
- hideInfoPanelClose,
683
- hideInfoPanelTitle,
684
- showToolbar,
685
- ...infoPanelCuration,
686
- readOnly
645
+ ...hostPanel
687
646
  };
688
647
  return outline ? /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(import_genre_tree_view3.GenreTreeOutline, { ...treeProps }) : /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(
689
648
  import_genre_tree_view3.GenreTreeWheelRadialPopCore,
@@ -803,7 +762,7 @@ function GenreSearch({
803
762
  type: "button",
804
763
  "aria-label": "Previous",
805
764
  title: "Previous",
806
- className: "genre-search-previous absolute left-2 top-1/2 z-10 hidden items-center justify-center p-1.5 [@media(pointer:coarse)]:min-h-[var(--layout-touch-target)] [@media(pointer:coarse)]:min-w-[var(--layout-touch-target)] text-[var(--color-text-muted)] rounded-full -translate-y-1/2 hover:bg-[var(--color-surface-muted)] hover:text-[var(--color-text)]",
765
+ className: "genre-search-previous absolute left-2 top-1/2 z-10 flex items-center justify-center p-1.5 [@media(pointer:coarse)]:min-h-[var(--layout-touch-target)] [@media(pointer:coarse)]:min-w-[var(--layout-touch-target)] text-[var(--color-text-muted)] rounded-full -translate-y-1/2 hover:bg-[var(--color-surface-muted)] hover:text-[var(--color-text)]",
807
766
  onClick: onPrevious,
808
767
  children: /* @__PURE__ */ (0, import_jsx_runtime9.jsx)(import_lucide_react3.ArrowLeft, { "aria-hidden": "true", className: "w-4 h-4" })
809
768
  }
@@ -825,7 +784,7 @@ function GenreSearch({
825
784
  onPick: handlePick,
826
785
  status,
827
786
  loading: loading ? "Searching\u2026" : void 0,
828
- inputClassName: "h-[var(--layout-search-h,3rem)] pl-6 pr-20 text-base transition-[padding] duration-200 ease-out motion-reduce:transition-none bg-[var(--color-surface)] border-[var(--color-border)] rounded-full shadow-md",
787
+ inputClassName: `h-[var(--layout-search-h,3rem)] ${onPrevious ? "pl-11" : "pl-6"} pr-20 text-base transition-[padding] duration-200 ease-out motion-reduce:transition-none bg-[var(--color-surface)] border-[var(--color-border)] rounded-full shadow-md`,
829
788
  className: "genre-search-results z-40 rounded-xl shadow-md",
830
789
  listClassName: "max-h-[min(20rem,50dvh)]",
831
790
  optionClassName: "[@media(pointer:coarse)]:min-h-[var(--layout-touch-target)]"
@@ -1289,7 +1248,7 @@ function MusicBrainzRecordingLink({
1289
1248
 
1290
1249
  // src/genre-tree/GenrePlaylistTracks.tsx
1291
1250
  var import_genre_tree8 = require("@behindthemusictree/app-kit/genre-tree");
1292
- var import_track_noun4 = require("@behindthemusictree/app-kit/track-noun");
1251
+ var import_track_noun = require("@behindthemusictree/app-kit/track-noun");
1293
1252
  var import_jsx_runtime18 = require("react/jsx-runtime");
1294
1253
  var CATALOGUE_PAGE_SIZE = 100;
1295
1254
  var SongPageSchema = (0, import_transport4.PaginatedResponseSchema)(import_genre_tree6.SongDetailedSchema).extend({
@@ -1323,7 +1282,7 @@ function GenrePlaylistTracks({
1323
1282
  const { session, sessionRestored } = (0, import_auth.useSession)();
1324
1283
  const { playerTrackObject, playState, handlePlayPauseAction } = (0, import_player4.usePlayer)();
1325
1284
  const { showPopup } = (0, import_popup4.usePopup)();
1326
- const trackNoun = (0, import_track_noun4.useTrackNoun)();
1285
+ const trackNoun = (0, import_track_noun.useTrackNoun)();
1327
1286
  const { uuid } = genrePlaylist;
1328
1287
  const { fetch } = (0, import_transport3.useFetchWrapper)(getBackendBaseUrl);
1329
1288
  const { data, isPending, isError, refetch, hasNextPage, isFetchingNextPage, fetchNextPage } = (0, import_react_query.useInfiniteQuery)({
@@ -1344,9 +1303,8 @@ function GenrePlaylistTracks({
1344
1303
  getNextPageParam: (lastPage) => lastPage.nextPage ?? void 0,
1345
1304
  enabled: scope !== "me" || sessionRestored && !!session?.accessToken
1346
1305
  });
1347
- const sentinelRef = (0, import_react11.useRef)(null);
1306
+ const [sentinel, setSentinel] = (0, import_react11.useState)(null);
1348
1307
  (0, import_react11.useEffect)(() => {
1349
- const sentinel = sentinelRef.current;
1350
1308
  if (!hasNextPage || isFetchingNextPage || !sentinel) return;
1351
1309
  const observer = new IntersectionObserver((entries) => {
1352
1310
  if (entries.some((entry) => entry.isIntersecting)) {
@@ -1355,7 +1313,7 @@ function GenrePlaylistTracks({
1355
1313
  });
1356
1314
  observer.observe(sentinel);
1357
1315
  return () => observer.disconnect();
1358
- }, [hasNextPage, isFetchingNextPage, fetchNextPage]);
1316
+ }, [hasNextPage, isFetchingNextPage, fetchNextPage, sentinel]);
1359
1317
  const pages = data?.pages ?? [];
1360
1318
  const tracks = pages.flatMap((page) => page.tracks);
1361
1319
  const total = pages[0]?.total ?? void 0;
@@ -1366,11 +1324,7 @@ function GenrePlaylistTracks({
1366
1324
  showPopup(/* @__PURE__ */ (0, import_jsx_runtime18.jsx)(InternalErrorPopup, { errorCode: import_transport6.ErrorCode.CLIENT_UNKNOWN }));
1367
1325
  });
1368
1326
  };
1369
- return /* @__PURE__ */ (0, import_jsx_runtime18.jsxs)("div", { className: "gtv-info-panel-children", children: [
1370
- /* @__PURE__ */ (0, import_jsx_runtime18.jsxs)("span", { className: "gtv-info-panel-children-title", children: [
1371
- (0, import_track_noun4.capitalize)(trackNoun.plural),
1372
- total !== void 0 && ` (${total})`
1373
- ] }),
1327
+ return /* @__PURE__ */ (0, import_jsx_runtime18.jsx)(import_ui4.PanelSection, { title: `${(0, import_track_noun.capitalize)(trackNoun.plural)}${total !== void 0 ? ` (${total})` : ""}`, children: /* @__PURE__ */ (0, import_jsx_runtime18.jsxs)("div", { children: [
1374
1328
  renderTracksToolbar?.(),
1375
1329
  isPending ? /* @__PURE__ */ (0, import_jsx_runtime18.jsx)(import_ui4.Skeleton, { className: "h-4 w-3/4", "aria-busy": "true" }) : isError && tracks.length === 0 ? /* @__PURE__ */ (0, import_jsx_runtime18.jsxs)("p", { role: "alert", children: [
1376
1330
  "Couldn't load ",
@@ -1433,13 +1387,13 @@ function GenrePlaylistTracks({
1433
1387
  track.uuid
1434
1388
  );
1435
1389
  }),
1436
- hasNextPage && /* @__PURE__ */ (0, import_jsx_runtime18.jsx)("li", { ref: sentinelRef, "aria-hidden": "true", className: "h-4" })
1390
+ hasNextPage && /* @__PURE__ */ (0, import_jsx_runtime18.jsx)("li", { ref: setSentinel, "aria-hidden": "true", className: "h-4" })
1437
1391
  ] })
1438
- ] });
1392
+ ] }) });
1439
1393
  }
1440
1394
 
1441
1395
  // src/genre-tree/GenreDetailExtras.tsx
1442
- var import_track_noun5 = require("@behindthemusictree/app-kit/track-noun");
1396
+ var import_track_noun2 = require("@behindthemusictree/app-kit/track-noun");
1443
1397
  var import_si2 = require("react-icons/si");
1444
1398
  var import_jsx_runtime19 = require("react/jsx-runtime");
1445
1399
  function GenreDetailExtras({
@@ -1457,7 +1411,7 @@ function GenreDetailExtras({
1457
1411
  family,
1458
1412
  ...trackSlots
1459
1413
  }) {
1460
- const trackNoun = (0, import_track_noun5.useTrackNoun)();
1414
+ const trackNoun = (0, import_track_noun2.useTrackNoun)();
1461
1415
  const { data: overview, isPending } = (0, import_genre_tree9.useFetchGenreOverview)(
1462
1416
  genreUuid,
1463
1417
  scope,
@@ -1466,10 +1420,7 @@ function GenreDetailExtras({
1466
1420
  );
1467
1421
  if (isPending) {
1468
1422
  return /* @__PURE__ */ (0, import_jsx_runtime19.jsxs)(import_jsx_runtime19.Fragment, { children: [
1469
- /* @__PURE__ */ (0, import_jsx_runtime19.jsxs)("div", { className: "gtv-info-panel-children", "aria-busy": "true", "data-testid": "genre-detail-extras-skeleton", children: [
1470
- /* @__PURE__ */ (0, import_jsx_runtime19.jsx)("span", { className: "gtv-info-panel-children-title", children: "Summary" }),
1471
- /* @__PURE__ */ (0, import_jsx_runtime19.jsx)(import_ui5.Skeleton, { className: "h-4 w-3/4" })
1472
- ] }),
1423
+ /* @__PURE__ */ (0, import_jsx_runtime19.jsx)(import_ui5.PanelSection, { title: "Summary", children: /* @__PURE__ */ (0, import_jsx_runtime19.jsx)("div", { "aria-busy": "true", "data-testid": "genre-detail-extras-skeleton", children: /* @__PURE__ */ (0, import_jsx_runtime19.jsx)(import_ui5.Skeleton, { className: "h-4 w-3/4" }) }) }),
1473
1424
  family
1474
1425
  ] });
1475
1426
  }
@@ -1477,50 +1428,38 @@ function GenreDetailExtras({
1477
1428
  const { summary, summarySourceUrl, essentialSongs } = overview;
1478
1429
  const wikidataUrl = wikidataId ? `https://www.wikidata.org/wiki/${encodeURIComponent(wikidataId)}` : null;
1479
1430
  return /* @__PURE__ */ (0, import_jsx_runtime19.jsxs)(import_jsx_runtime19.Fragment, { children: [
1480
- (summarySourceUrl || wikidataUrl) && /* @__PURE__ */ (0, import_jsx_runtime19.jsxs)("div", { className: "gtv-info-panel-children text-[0.8125rem] leading-[1.45] text-[var(--color-text)]", children: [
1481
- /* @__PURE__ */ (0, import_jsx_runtime19.jsx)("span", { className: "gtv-info-panel-children-title", children: "Links" }),
1482
- /* @__PURE__ */ (0, import_jsx_runtime19.jsxs)("div", { className: "mt-1 flex flex-wrap gap-x-3 gap-y-1 text-xs", children: [
1483
- summarySourceUrl && /* @__PURE__ */ (0, import_jsx_runtime19.jsxs)(
1484
- "a",
1485
- {
1486
- href: summarySourceUrl,
1487
- target: "_blank",
1488
- rel: "noopener noreferrer",
1489
- className: "inline-flex items-center gap-1.5 text-[var(--color-text-muted)] hover:text-[var(--color-text)]",
1490
- children: [
1491
- /* @__PURE__ */ (0, import_jsx_runtime19.jsx)(import_si2.SiWikipedia, { "aria-hidden": "true", className: "h-3.5 w-3.5" }),
1492
- "Wikipedia"
1493
- ]
1494
- }
1495
- ),
1496
- wikidataUrl && /* @__PURE__ */ (0, import_jsx_runtime19.jsxs)(
1497
- "a",
1498
- {
1499
- href: wikidataUrl,
1500
- target: "_blank",
1501
- rel: "noopener noreferrer",
1502
- className: "inline-flex items-center gap-1.5 text-[var(--color-text-muted)] hover:text-[var(--color-text)]",
1503
- children: [
1504
- /* @__PURE__ */ (0, import_jsx_runtime19.jsx)(import_si2.SiWikidata, { "aria-hidden": "true", className: "h-3.5 w-3.5" }),
1505
- "Wikidata"
1506
- ]
1507
- }
1508
- )
1509
- ] })
1510
- ] }),
1511
- renderSummary ? renderSummary(overview) : /* @__PURE__ */ (0, import_jsx_runtime19.jsxs)("div", { className: "gtv-info-panel-children text-[0.8125rem] leading-[1.45] text-[var(--color-text)]", children: [
1512
- /* @__PURE__ */ (0, import_jsx_runtime19.jsx)("span", { className: "gtv-info-panel-children-title", children: "Summary" }),
1513
- /* @__PURE__ */ (0, import_jsx_runtime19.jsx)(ClampedText, { className: "mt-1", children: summary ?? "\u2014" }, genreUuid)
1514
- ] }),
1431
+ (summarySourceUrl || wikidataUrl) && /* @__PURE__ */ (0, import_jsx_runtime19.jsx)(import_ui5.PanelSection, { title: "Links", children: /* @__PURE__ */ (0, import_jsx_runtime19.jsxs)("div", { className: "flex flex-wrap gap-x-3 gap-y-1 text-xs text-[var(--color-text)]", children: [
1432
+ summarySourceUrl && /* @__PURE__ */ (0, import_jsx_runtime19.jsxs)(
1433
+ "a",
1434
+ {
1435
+ href: summarySourceUrl,
1436
+ target: "_blank",
1437
+ rel: "noopener noreferrer",
1438
+ className: "inline-flex items-center gap-1.5 text-[var(--color-text-muted)] hover:text-[var(--color-text)]",
1439
+ children: [
1440
+ /* @__PURE__ */ (0, import_jsx_runtime19.jsx)(import_si2.SiWikipedia, { "aria-hidden": "true", className: "h-3.5 w-3.5" }),
1441
+ "Wikipedia"
1442
+ ]
1443
+ }
1444
+ ),
1445
+ wikidataUrl && /* @__PURE__ */ (0, import_jsx_runtime19.jsxs)(
1446
+ "a",
1447
+ {
1448
+ href: wikidataUrl,
1449
+ target: "_blank",
1450
+ rel: "noopener noreferrer",
1451
+ className: "inline-flex items-center gap-1.5 text-[var(--color-text-muted)] hover:text-[var(--color-text)]",
1452
+ children: [
1453
+ /* @__PURE__ */ (0, import_jsx_runtime19.jsx)(import_si2.SiWikidata, { "aria-hidden": "true", className: "h-3.5 w-3.5" }),
1454
+ "Wikidata"
1455
+ ]
1456
+ }
1457
+ )
1458
+ ] }) }),
1459
+ /* @__PURE__ */ (0, import_jsx_runtime19.jsx)(import_ui5.PanelSection, { title: "Summary", children: renderSummary ? renderSummary(overview) : /* @__PURE__ */ (0, import_jsx_runtime19.jsx)(ClampedText, { className: "text-[0.8125rem] leading-[1.45] text-[var(--color-text)]", children: summary ?? "\u2014" }, genreUuid) }),
1515
1460
  family,
1516
1461
  renderGenreDetailExtras?.(overview),
1517
- renderEssentialTracks ? renderEssentialTracks(overview) : essentialSongs && /* @__PURE__ */ (0, import_jsx_runtime19.jsxs)("div", { className: "gtv-info-panel-children text-[0.8125rem] leading-[1.45] text-[var(--color-text)]", children: [
1518
- /* @__PURE__ */ (0, import_jsx_runtime19.jsxs)("span", { className: "gtv-info-panel-children-title", children: [
1519
- "Essential ",
1520
- trackNoun.plural
1521
- ] }),
1522
- essentialSongs.length > 0 ? /* @__PURE__ */ (0, import_jsx_runtime19.jsx)("ul", { className: "mt-1 divide-y divide-[var(--color-border)]", children: essentialSongs.map((track) => /* @__PURE__ */ (0, import_jsx_runtime19.jsx)("li", { className: "py-1", children: track.title }, track.uuid)) }) : /* @__PURE__ */ (0, import_jsx_runtime19.jsx)("p", { className: "mt-1", children: "\u2014" })
1523
- ] }),
1462
+ (renderEssentialTracks || essentialSongs) && /* @__PURE__ */ (0, import_jsx_runtime19.jsx)(import_ui5.PanelSection, { title: `Essential ${trackNoun.plural}`, children: renderEssentialTracks ? renderEssentialTracks(overview) : essentialSongs && essentialSongs.length > 0 ? /* @__PURE__ */ (0, import_jsx_runtime19.jsx)("ul", { className: "text-[0.8125rem] leading-[1.45] text-[var(--color-text)]", children: essentialSongs.map((track) => /* @__PURE__ */ (0, import_jsx_runtime19.jsx)("li", { className: "py-1", children: track.title }, track.uuid)) }) : /* @__PURE__ */ (0, import_jsx_runtime19.jsx)("p", { className: "text-[0.8125rem] leading-[1.45] text-[var(--color-text)]", children: "\u2014" }) }),
1524
1463
  /* @__PURE__ */ (0, import_jsx_runtime19.jsx)(
1525
1464
  GenrePlaylistTracks,
1526
1465
  {
@@ -1591,66 +1530,10 @@ function GenreInfoPanelBody({
1591
1530
  ] });
1592
1531
  }
1593
1532
 
1594
- // src/genre-tree/PanelEdgeTab.tsx
1595
- var import_lucide_react8 = require("lucide-react");
1596
- var import_tailwind_merge = require("tailwind-merge");
1597
- var import_jsx_runtime21 = require("react/jsx-runtime");
1598
- var buttonClassName = "flex h-12 w-full items-center justify-center p-0 text-[var(--color-text-muted)] hover:bg-[var(--color-surface-muted)] hover:text-[var(--color-text)] focus-visible:outline focus-visible:outline-2 focus-visible:-outline-offset-2 focus-visible:outline-[var(--color-primary)]";
1599
- function PanelEdgeTab({ side, edge, center, controls, className: className2 }) {
1600
- if (!edge && !center) return null;
1601
- const isLeft = side === "left";
1602
- const EdgeIcon = isLeft ? import_lucide_react8.ChevronLeft : import_lucide_react8.ChevronRight;
1603
- const CenterIcon = isLeft ? import_lucide_react8.ChevronRight : import_lucide_react8.ChevronLeft;
1604
- const renderButton = (action, icon) => /* @__PURE__ */ (0, import_jsx_runtime21.jsx)(
1605
- "button",
1606
- {
1607
- type: "button",
1608
- "aria-label": action.label,
1609
- title: action.label,
1610
- "aria-controls": controls,
1611
- "aria-expanded": action.expanded,
1612
- className: buttonClassName,
1613
- onClick: action.onClick,
1614
- children: icon
1615
- }
1616
- );
1617
- const edgeButton = edge && renderButton(edge, /* @__PURE__ */ (0, import_jsx_runtime21.jsx)(EdgeIcon, { "aria-hidden": "true", className: "h-5 w-5" }));
1618
- const centerButton = center && renderButton(center, /* @__PURE__ */ (0, import_jsx_runtime21.jsx)(CenterIcon, { "aria-hidden": "true", className: "h-5 w-5" }));
1619
- const buttons = isLeft ? [edgeButton, centerButton] : [centerButton, edgeButton];
1620
- return /* @__PURE__ */ (0, import_jsx_runtime21.jsxs)(
1621
- "div",
1622
- {
1623
- className: (0, import_tailwind_merge.twMerge)(
1624
- "panel-edge-tab flex w-[var(--layout-edge-tab-w)] flex-col overflow-hidden bg-[var(--color-surface)] shadow-[0_1px_2px_rgba(60,64,67,0.3),0_2px_6px_2px_rgba(60,64,67,0.15)] [clip-path:inset(-0.5rem_-0.5rem_-0.5rem_0)]",
1625
- isLeft ? "rounded-r-lg" : "rounded-l-lg [clip-path:inset(-0.5rem_0_-0.5rem_-0.5rem)]",
1626
- className2
1627
- ),
1628
- children: [
1629
- buttons[0],
1630
- buttons[0] && buttons[1] && /* @__PURE__ */ (0, import_jsx_runtime21.jsx)("div", { role: "presentation", className: "h-px shrink-0 bg-[var(--color-border)]" }),
1631
- buttons[1]
1632
- ]
1633
- }
1634
- );
1635
- }
1636
-
1637
- // src/genre-tree/useLeftDetailPanelLayout.ts
1638
- var import_react13 = require("react");
1639
- function useLeftDetailPanelLayout(isPanelShown, enabled = true) {
1640
- (0, import_react13.useEffect)(() => {
1641
- if (!enabled || isPanelShown) return void 0;
1642
- const root = document.documentElement;
1643
- root.style.setProperty("--layout-left-w", "0px");
1644
- return () => {
1645
- root.style.removeProperty("--layout-left-w");
1646
- };
1647
- }, [enabled, isPanelShown]);
1648
- }
1649
-
1650
1533
  // src/genre-tree/ChangeParentPopup.tsx
1651
- var import_react14 = require("react");
1534
+ var import_react13 = require("react");
1652
1535
  var import_genre_tree10 = require("@behindthemusictree/app-kit/genre-tree");
1653
- var import_jsx_runtime22 = require("react/jsx-runtime");
1536
+ var import_jsx_runtime21 = require("react/jsx-runtime");
1654
1537
  function ChangeParentPopup({
1655
1538
  genrePlaylist,
1656
1539
  genrePlaylists,
@@ -1660,7 +1543,7 @@ function ChangeParentPopup({
1660
1543
  onParentChanged
1661
1544
  }) {
1662
1545
  const { mutate, isPending, formErrors } = (0, import_genre_tree10.useUpdateGenre)(scope, getBackendBaseUrl);
1663
- const candidates = (0, import_react14.useMemo)(
1546
+ const candidates = (0, import_react13.useMemo)(
1664
1547
  () => (0, import_genre_tree10.getReparentCandidates)(genrePlaylists, genrePlaylist.uuid),
1665
1548
  [genrePlaylists, genrePlaylist.uuid]
1666
1549
  );
@@ -1678,19 +1561,19 @@ function ChangeParentPopup({
1678
1561
  }
1679
1562
  );
1680
1563
  };
1681
- (0, import_react14.useEffect)(() => {
1564
+ (0, import_react13.useEffect)(() => {
1682
1565
  const handleKeyDown = (event) => {
1683
1566
  if (event.key === "Escape") onClose();
1684
1567
  };
1685
1568
  window.addEventListener("keydown", handleKeyDown);
1686
1569
  return () => window.removeEventListener("keydown", handleKeyDown);
1687
1570
  }, [onClose]);
1688
- return /* @__PURE__ */ (0, import_jsx_runtime22.jsxs)(BasePopup_default, { title: `Change parent of ${genrePlaylist.name}`, onClose, showCancelButton: true, fitContent: true, children: [
1689
- /* @__PURE__ */ (0, import_jsx_runtime22.jsxs)("p", { className: "mb-3 text-sm text-[var(--color-text-muted)]", children: [
1571
+ return /* @__PURE__ */ (0, import_jsx_runtime21.jsxs)(BasePopup_default, { title: `Change parent of ${genrePlaylist.name}`, onClose, showCancelButton: true, fitContent: true, children: [
1572
+ /* @__PURE__ */ (0, import_jsx_runtime21.jsxs)("p", { className: "mb-3 text-sm text-[var(--color-text-muted)]", children: [
1690
1573
  "Current parent: ",
1691
1574
  genrePlaylist.parent?.name ?? "none"
1692
1575
  ] }),
1693
- /* @__PURE__ */ (0, import_jsx_runtime22.jsx)("div", { "aria-busy": isPending, className: isPending ? "pointer-events-none opacity-60" : void 0, children: /* @__PURE__ */ (0, import_jsx_runtime22.jsx)(
1576
+ /* @__PURE__ */ (0, import_jsx_runtime21.jsx)("div", { "aria-busy": isPending, className: isPending ? "pointer-events-none opacity-60" : void 0, children: /* @__PURE__ */ (0, import_jsx_runtime21.jsx)(
1694
1577
  GenreSearch,
1695
1578
  {
1696
1579
  genrePlaylists: candidates,
@@ -1698,7 +1581,7 @@ function ChangeParentPopup({
1698
1581
  placeholder: "Search a new parent\u2026"
1699
1582
  }
1700
1583
  ) }),
1701
- formErrors.length > 0 && /* @__PURE__ */ (0, import_jsx_runtime22.jsx)("p", { role: "alert", className: "mt-3 text-sm text-[var(--color-red-500)]", children: formErrors.map(({ message }) => message).join(" ") })
1584
+ formErrors.length > 0 && /* @__PURE__ */ (0, import_jsx_runtime21.jsx)("p", { role: "alert", className: "mt-3 text-sm text-[var(--color-red-500)]", children: formErrors.map(({ message }) => message).join(" ") })
1702
1585
  ] });
1703
1586
  }
1704
1587
 
@@ -1710,12 +1593,12 @@ var import_genre_tree15 = require("@behindthemusictree/app-kit/genre-tree");
1710
1593
  var import_ui7 = require("@behindthemusictree/ui");
1711
1594
 
1712
1595
  // src/genre-tree/artist/ArtistLinks.tsx
1713
- var import_lucide_react9 = require("lucide-react");
1596
+ var import_lucide_react8 = require("lucide-react");
1714
1597
  var import_si3 = require("react-icons/si");
1715
1598
  var import_ui6 = require("@behindthemusictree/ui");
1716
- var import_jsx_runtime23 = require("react/jsx-runtime");
1599
+ var import_jsx_runtime22 = require("react/jsx-runtime");
1717
1600
  var LINK_KINDS = {
1718
- official: { label: "Official website", Icon: import_lucide_react9.Globe },
1601
+ official: { label: "Official website", Icon: import_lucide_react8.Globe },
1719
1602
  wikipedia: { label: "Wikipedia", Icon: import_si3.SiWikipedia },
1720
1603
  wikidata: { label: "Wikidata", Icon: import_si3.SiWikidata },
1721
1604
  spotify: { label: "Spotify", Icon: import_si3.SiSpotify },
@@ -1723,7 +1606,7 @@ var LINK_KINDS = {
1723
1606
  bandcamp: { label: "Bandcamp", Icon: import_si3.SiBandcamp },
1724
1607
  soundcloud: { label: "SoundCloud", Icon: import_si3.SiSoundcloud },
1725
1608
  discogs: { label: "Discogs", Icon: import_si3.SiDiscogs },
1726
- social: { label: "Social", Icon: import_lucide_react9.Share2 }
1609
+ social: { label: "Social", Icon: import_lucide_react8.Share2 }
1727
1610
  };
1728
1611
  function socialLabel(url) {
1729
1612
  try {
@@ -1750,7 +1633,7 @@ function artistLinks(artist) {
1750
1633
  return links;
1751
1634
  }
1752
1635
  function ArtistLinks({ links }) {
1753
- return /* @__PURE__ */ (0, import_jsx_runtime23.jsx)("ul", { className: "flex flex-wrap gap-1.5", children: links.map(({ key, url, Icon, label }) => /* @__PURE__ */ (0, import_jsx_runtime23.jsx)("li", { children: /* @__PURE__ */ (0, import_jsx_runtime23.jsx)(import_ui6.Chip, { href: url, icon: /* @__PURE__ */ (0, import_jsx_runtime23.jsx)(Icon, { className: "h-3.5 w-3.5 shrink-0", "aria-hidden": "true" }), children: label }) }, key)) });
1636
+ return /* @__PURE__ */ (0, import_jsx_runtime22.jsx)("ul", { className: "flex flex-wrap gap-1.5", children: links.map(({ key, url, Icon, label }) => /* @__PURE__ */ (0, import_jsx_runtime22.jsx)("li", { children: /* @__PURE__ */ (0, import_jsx_runtime22.jsx)(import_ui6.Chip, { href: url, icon: /* @__PURE__ */ (0, import_jsx_runtime22.jsx)(Icon, { className: "h-3.5 w-3.5 shrink-0", "aria-hidden": "true" }), children: label }) }, key)) });
1754
1637
  }
1755
1638
 
1756
1639
  // src/genre-tree/artist/formatYears.ts
@@ -1761,7 +1644,7 @@ function formatYears(beginYear, endYear, ended) {
1761
1644
  }
1762
1645
 
1763
1646
  // src/genre-tree/artist/ArtistHeader.tsx
1764
- var import_jsx_runtime24 = require("react/jsx-runtime");
1647
+ var import_jsx_runtime23 = require("react/jsx-runtime");
1765
1648
  var countryNames = new Intl.DisplayNames(["en"], { type: "region" });
1766
1649
  function ArtistHeader({ artist }) {
1767
1650
  const fields = [];
@@ -1771,35 +1654,35 @@ function ArtistHeader({ artist }) {
1771
1654
  const years = formatYears(artist.beginYear, artist.endYear, artist.ended);
1772
1655
  if (years) fields.push({ label: "Active", value: years });
1773
1656
  const links = artistLinks(artist);
1774
- if (links.length > 0) fields.push({ label: "Links", value: /* @__PURE__ */ (0, import_jsx_runtime24.jsx)(ArtistLinks, { links }) });
1657
+ if (links.length > 0) fields.push({ label: "Links", value: /* @__PURE__ */ (0, import_jsx_runtime23.jsx)(ArtistLinks, { links }) });
1775
1658
  if (fields.length === 0) return null;
1776
- return /* @__PURE__ */ (0, import_jsx_runtime24.jsx)(import_ui7.FieldList, { items: fields });
1659
+ return /* @__PURE__ */ (0, import_jsx_runtime23.jsx)(import_ui7.FieldList, { items: fields });
1777
1660
  }
1778
1661
 
1779
1662
  // src/genre-tree/artist/ArtistPhoto.tsx
1780
- var import_jsx_runtime25 = require("react/jsx-runtime");
1663
+ var import_jsx_runtime24 = require("react/jsx-runtime");
1781
1664
  function ExternalLink({ href, children }) {
1782
- return /* @__PURE__ */ (0, import_jsx_runtime25.jsx)("a", { href, target: "_blank", rel: "noopener noreferrer", className: "underline-offset-2 hover:underline", children });
1665
+ return /* @__PURE__ */ (0, import_jsx_runtime24.jsx)("a", { href, target: "_blank", rel: "noopener noreferrer", className: "underline-offset-2 hover:underline", children });
1783
1666
  }
1784
1667
  function ArtistPhoto({ image, name }) {
1785
- return /* @__PURE__ */ (0, import_jsx_runtime25.jsxs)("figure", { className: "flex flex-col gap-1.5 px-6 py-2.5", children: [
1786
- /* @__PURE__ */ (0, import_jsx_runtime25.jsx)("img", { src: image.url, alt: name, className: "h-64 w-full object-contain bg-[var(--color-surface-muted)] rounded" }),
1787
- /* @__PURE__ */ (0, import_jsx_runtime25.jsxs)("figcaption", { className: "text-xs text-[var(--color-text-muted)]", children: [
1668
+ return /* @__PURE__ */ (0, import_jsx_runtime24.jsxs)("figure", { className: "flex flex-col gap-1.5 px-6 py-2.5", children: [
1669
+ /* @__PURE__ */ (0, import_jsx_runtime24.jsx)("img", { src: image.url, alt: name, className: "h-64 w-full object-contain bg-[var(--color-surface-muted)] rounded" }),
1670
+ /* @__PURE__ */ (0, import_jsx_runtime24.jsxs)("figcaption", { className: "text-xs text-[var(--color-text-muted)]", children: [
1788
1671
  "Photo: ",
1789
- /* @__PURE__ */ (0, import_jsx_runtime25.jsx)(ExternalLink, { href: image.pageUrl, children: image.author ?? "Wikimedia Commons" }),
1672
+ /* @__PURE__ */ (0, import_jsx_runtime24.jsx)(ExternalLink, { href: image.pageUrl, children: image.author ?? "Wikimedia Commons" }),
1790
1673
  ",",
1791
1674
  " ",
1792
- image.licenseUrl ? /* @__PURE__ */ (0, import_jsx_runtime25.jsx)(ExternalLink, { href: image.licenseUrl, children: image.license }) : image.license
1675
+ image.licenseUrl ? /* @__PURE__ */ (0, import_jsx_runtime24.jsx)(ExternalLink, { href: image.licenseUrl, children: image.license }) : image.license
1793
1676
  ] })
1794
1677
  ] });
1795
1678
  }
1796
1679
 
1797
1680
  // src/genre-tree/artist/ArtistMembers.tsx
1798
1681
  var import_ui8 = require("@behindthemusictree/ui");
1799
- var import_jsx_runtime26 = require("react/jsx-runtime");
1682
+ var import_jsx_runtime25 = require("react/jsx-runtime");
1800
1683
  function MembershipList({ title, memberships, renderArtistLink }) {
1801
1684
  if (memberships.length === 0) return null;
1802
- return /* @__PURE__ */ (0, import_jsx_runtime26.jsx)(import_ui8.PanelGroup, { title, children: /* @__PURE__ */ (0, import_jsx_runtime26.jsx)("ul", { children: memberships.map((membership, index) => {
1685
+ return /* @__PURE__ */ (0, import_jsx_runtime25.jsx)(import_ui8.PanelGroup, { title, children: /* @__PURE__ */ (0, import_jsx_runtime25.jsx)("ul", { children: memberships.map((membership, index) => {
1803
1686
  const details = [
1804
1687
  formatYears(membership.beginYear, membership.endYear, membership.ended),
1805
1688
  ...membership.roles
@@ -1807,9 +1690,9 @@ function MembershipList({ title, memberships, renderArtistLink }) {
1807
1690
  const detailsText = details.filter(Boolean).join(" \xB7 ");
1808
1691
  return (
1809
1692
  // A member can have several periods in the same group.
1810
- /* @__PURE__ */ (0, import_jsx_runtime26.jsxs)("li", { className: "truncate py-0.5", title: detailsText || void 0, children: [
1811
- /* @__PURE__ */ (0, import_jsx_runtime26.jsx)("span", { className: "font-medium text-[var(--color-text)]", children: renderArtistLink(membership.artist) }),
1812
- detailsText && /* @__PURE__ */ (0, import_jsx_runtime26.jsxs)("span", { className: "text-[var(--color-text-muted)]", children: [
1693
+ /* @__PURE__ */ (0, import_jsx_runtime25.jsxs)("li", { className: "truncate py-0.5", title: detailsText || void 0, children: [
1694
+ /* @__PURE__ */ (0, import_jsx_runtime25.jsx)("span", { className: "font-medium text-[var(--color-text)]", children: renderArtistLink(membership.artist) }),
1695
+ detailsText && /* @__PURE__ */ (0, import_jsx_runtime25.jsxs)("span", { className: "text-[var(--color-text-muted)]", children: [
1813
1696
  " \u2014 ",
1814
1697
  detailsText
1815
1698
  ] })
@@ -1820,9 +1703,9 @@ function MembershipList({ title, memberships, renderArtistLink }) {
1820
1703
  function ArtistMembers({ title, memberships, renderArtistLink }) {
1821
1704
  if (memberships.length === 0) return null;
1822
1705
  const isCurrent = (membership) => !membership.ended && membership.endYear === null;
1823
- return /* @__PURE__ */ (0, import_jsx_runtime26.jsxs)(import_ui8.PanelSection, { title, children: [
1824
- /* @__PURE__ */ (0, import_jsx_runtime26.jsx)(MembershipList, { title: "Current", memberships: memberships.filter(isCurrent), renderArtistLink }),
1825
- /* @__PURE__ */ (0, import_jsx_runtime26.jsx)(
1706
+ return /* @__PURE__ */ (0, import_jsx_runtime25.jsxs)(import_ui8.PanelSection, { title, children: [
1707
+ /* @__PURE__ */ (0, import_jsx_runtime25.jsx)(MembershipList, { title: "Current", memberships: memberships.filter(isCurrent), renderArtistLink }),
1708
+ /* @__PURE__ */ (0, import_jsx_runtime25.jsx)(
1826
1709
  MembershipList,
1827
1710
  {
1828
1711
  title: "Past",
@@ -1835,7 +1718,7 @@ function ArtistMembers({ title, memberships, renderArtistLink }) {
1835
1718
 
1836
1719
  // src/genre-tree/artist/ArtistDiscography.tsx
1837
1720
  var import_ui9 = require("@behindthemusictree/ui");
1838
- var import_jsx_runtime27 = require("react/jsx-runtime");
1721
+ var import_jsx_runtime26 = require("react/jsx-runtime");
1839
1722
  var GROUPS = ["Albums", "EPs", "Singles", "Live", "Compilations", "Other"];
1840
1723
  var PRIMARY_GROUPS = { album: "Albums", ep: "EPs", single: "Singles" };
1841
1724
  function discographyGroup({ primaryType, secondaryTypes }) {
@@ -1855,20 +1738,20 @@ function byYear(a, b) {
1855
1738
  function ArtistDiscography({ albums }) {
1856
1739
  if (albums.length === 0) return null;
1857
1740
  const sorted = [...albums].sort(byYear);
1858
- return /* @__PURE__ */ (0, import_jsx_runtime27.jsx)(import_ui9.PanelSection, { title: "Discography", children: GROUPS.map((group) => {
1741
+ return /* @__PURE__ */ (0, import_jsx_runtime26.jsx)(import_ui9.PanelSection, { title: "Discography", children: GROUPS.map((group) => {
1859
1742
  const groupAlbums = sorted.filter((album) => discographyGroup(album) === group);
1860
1743
  if (groupAlbums.length === 0) return null;
1861
- return /* @__PURE__ */ (0, import_jsx_runtime27.jsx)(import_ui9.PanelGroup, { title: group, children: /* @__PURE__ */ (0, import_jsx_runtime27.jsx)("ul", { children: groupAlbums.map((album) => /* @__PURE__ */ (0, import_jsx_runtime27.jsxs)("li", { className: "grid grid-cols-[2.5rem_minmax(0,1fr)] gap-2 py-0.5", children: [
1862
- /* @__PURE__ */ (0, import_jsx_runtime27.jsx)("span", { className: "tabular-nums text-[var(--color-text-muted)]", children: album.year ?? "\u2014" }),
1863
- /* @__PURE__ */ (0, import_jsx_runtime27.jsx)("span", { className: "truncate text-[var(--color-text)]", title: album.name, children: album.name })
1744
+ return /* @__PURE__ */ (0, import_jsx_runtime26.jsx)(import_ui9.PanelGroup, { title: group, children: /* @__PURE__ */ (0, import_jsx_runtime26.jsx)("ul", { children: groupAlbums.map((album) => /* @__PURE__ */ (0, import_jsx_runtime26.jsxs)("li", { className: "grid grid-cols-[2.5rem_minmax(0,1fr)] gap-2 py-0.5", children: [
1745
+ /* @__PURE__ */ (0, import_jsx_runtime26.jsx)("span", { className: "tabular-nums text-[var(--color-text-muted)]", children: album.year ?? "\u2014" }),
1746
+ /* @__PURE__ */ (0, import_jsx_runtime26.jsx)("span", { className: "truncate text-[var(--color-text)]", title: album.name, children: album.name })
1864
1747
  ] }, album.uuid)) }) }, group);
1865
1748
  }) });
1866
1749
  }
1867
1750
 
1868
1751
  // src/genre-tree/artist/ArtistSongs.tsx
1869
- var import_react15 = require("react");
1752
+ var import_react14 = require("react");
1870
1753
  var import_react_query2 = require("@tanstack/react-query");
1871
- var import_lucide_react10 = require("lucide-react");
1754
+ var import_lucide_react9 = require("lucide-react");
1872
1755
  var import_ui10 = require("@behindthemusictree/ui");
1873
1756
  var import_player10 = require("@behindthemusictree/app-kit/player");
1874
1757
  var import_genre_tree14 = require("@behindthemusictree/app-kit/genre-tree");
@@ -1882,7 +1765,7 @@ var import_genre_tree11 = require("@behindthemusictree/app-kit/genre-tree");
1882
1765
  var import_fa3 = require("react-icons/fa");
1883
1766
  var import_player7 = require("@behindthemusictree/app-kit/player");
1884
1767
  var import_player8 = require("@behindthemusictree/app-kit/player");
1885
- var import_jsx_runtime28 = require("react/jsx-runtime");
1768
+ var import_jsx_runtime27 = require("react/jsx-runtime");
1886
1769
  function TrackPositionPlayPause({
1887
1770
  position,
1888
1771
  uuid,
@@ -1896,14 +1779,14 @@ function TrackPositionPlayPause({
1896
1779
  const isPlaying = isThisTrack && isConfirmedPlayable && playState === import_player8.PlayStates.PLAYING;
1897
1780
  const isPaused = isThisTrack && isConfirmedPlayable && playState === import_player8.PlayStates.PAUSED;
1898
1781
  const isCurrentTrackActive = isPlaying || isPaused;
1899
- return /* @__PURE__ */ (0, import_jsx_runtime28.jsxs)(
1782
+ return /* @__PURE__ */ (0, import_jsx_runtime27.jsxs)(
1900
1783
  "div",
1901
1784
  {
1902
1785
  className: "track-position-play-pause relative flex items-center justify-center gap-2 text-lg w-16 cursor-pointer",
1903
1786
  style: { minWidth: "4rem", maxWidth: "4rem" },
1904
1787
  onClick: handlePlayPauseClick,
1905
1788
  children: [
1906
- /* @__PURE__ */ (0, import_jsx_runtime28.jsx)("div", { className: "track-position-play-pause__status group-hover:hidden flex items-center justify-center", children: isAwaitingPlayback ? /* @__PURE__ */ (0, import_jsx_runtime28.jsx)("span", { className: "track-position-play-pause__spinner", role: "status", "aria-label": "Loading track" }) : isCurrentTrackActive ? /* @__PURE__ */ (0, import_jsx_runtime28.jsxs)(
1789
+ /* @__PURE__ */ (0, import_jsx_runtime27.jsx)("div", { className: "track-position-play-pause__status group-hover:hidden flex items-center justify-center", children: isAwaitingPlayback ? /* @__PURE__ */ (0, import_jsx_runtime27.jsx)("span", { className: "track-position-play-pause__spinner", role: "status", "aria-label": "Loading track" }) : isCurrentTrackActive ? /* @__PURE__ */ (0, import_jsx_runtime27.jsxs)(
1907
1790
  "span",
1908
1791
  {
1909
1792
  className: "track-position-play-pause__bars",
@@ -1911,18 +1794,18 @@ function TrackPositionPlayPause({
1911
1794
  role: "img",
1912
1795
  "aria-label": isPlaying ? "Playing" : "Paused",
1913
1796
  children: [
1914
- /* @__PURE__ */ (0, import_jsx_runtime28.jsx)("span", { className: "track-position-play-pause__bar" }),
1915
- /* @__PURE__ */ (0, import_jsx_runtime28.jsx)("span", { className: "track-position-play-pause__bar" }),
1916
- /* @__PURE__ */ (0, import_jsx_runtime28.jsx)("span", { className: "track-position-play-pause__bar" })
1797
+ /* @__PURE__ */ (0, import_jsx_runtime27.jsx)("span", { className: "track-position-play-pause__bar" }),
1798
+ /* @__PURE__ */ (0, import_jsx_runtime27.jsx)("span", { className: "track-position-play-pause__bar" }),
1799
+ /* @__PURE__ */ (0, import_jsx_runtime27.jsx)("span", { className: "track-position-play-pause__bar" })
1917
1800
  ]
1918
1801
  }
1919
1802
  ) : position }),
1920
- /* @__PURE__ */ (0, import_jsx_runtime28.jsx)(
1803
+ /* @__PURE__ */ (0, import_jsx_runtime27.jsx)(
1921
1804
  "div",
1922
1805
  {
1923
1806
  className: `hidden group-hover:flex items-center justify-center ${isCurrentTrackActive ? "" : "absolute inset-0"}`,
1924
1807
  "aria-hidden": "true",
1925
- children: isAwaitingPlayback ? /* @__PURE__ */ (0, import_jsx_runtime28.jsx)("span", { className: "track-position-play-pause__spinner" }) : isPlaying ? /* @__PURE__ */ (0, import_jsx_runtime28.jsx)(import_fa3.FaPause, {}) : /* @__PURE__ */ (0, import_jsx_runtime28.jsx)(import_fa3.FaPlay, {})
1808
+ children: isAwaitingPlayback ? /* @__PURE__ */ (0, import_jsx_runtime27.jsx)("span", { className: "track-position-play-pause__spinner" }) : isPlaying ? /* @__PURE__ */ (0, import_jsx_runtime27.jsx)(import_fa3.FaPause, {}) : /* @__PURE__ */ (0, import_jsx_runtime27.jsx)(import_fa3.FaPlay, {})
1926
1809
  }
1927
1810
  )
1928
1811
  ]
@@ -1933,7 +1816,7 @@ function TrackPositionPlayPause({
1933
1816
  // src/genre-tree/track-list-sidebar/TrackItem.tsx
1934
1817
  var import_genre_tree12 = require("@behindthemusictree/app-kit/genre-tree");
1935
1818
  var import_genre_tree13 = require("@behindthemusictree/app-kit/genre-tree");
1936
- var import_jsx_runtime29 = require("react/jsx-runtime");
1819
+ var import_jsx_runtime28 = require("react/jsx-runtime");
1937
1820
  function TrackItem({
1938
1821
  track,
1939
1822
  position,
@@ -1957,12 +1840,12 @@ function TrackItem({
1957
1840
  loadTrackForPlayer(track.uuid);
1958
1841
  }
1959
1842
  };
1960
- return /* @__PURE__ */ (0, import_jsx_runtime29.jsxs)(
1843
+ return /* @__PURE__ */ (0, import_jsx_runtime28.jsxs)(
1961
1844
  "div",
1962
1845
  {
1963
1846
  className: `track-item flex h-14 text-[var(--color-text)] ${isUnplayable ? "opacity-50" : "hover:bg-[var(--color-surface-muted)] group"}`,
1964
1847
  children: [
1965
- isUnplayable ? /* @__PURE__ */ (0, import_jsx_runtime29.jsx)(
1848
+ isUnplayable ? /* @__PURE__ */ (0, import_jsx_runtime28.jsx)(
1966
1849
  "div",
1967
1850
  {
1968
1851
  className: "flex items-center justify-center text-lg w-16",
@@ -1970,7 +1853,7 @@ function TrackItem({
1970
1853
  "aria-disabled": "true",
1971
1854
  children: position
1972
1855
  }
1973
- ) : /* @__PURE__ */ (0, import_jsx_runtime29.jsx)(
1856
+ ) : /* @__PURE__ */ (0, import_jsx_runtime28.jsx)(
1974
1857
  TrackPositionPlayPause,
1975
1858
  {
1976
1859
  position,
@@ -1979,12 +1862,12 @@ function TrackItem({
1979
1862
  isSelected: selectedTrack?.uuid === track.uuid
1980
1863
  }
1981
1864
  ),
1982
- /* @__PURE__ */ (0, import_jsx_runtime29.jsxs)("div", { className: "title-artist-container flex flex-col items-start justify-center min-w-0 flex-1", children: [
1983
- /* @__PURE__ */ (0, import_jsx_runtime29.jsx)("div", { className: "flex max-w-full items-center gap-2", children: /* @__PURE__ */ (0, import_jsx_runtime29.jsx)("div", { className: "title text-lg font-bold text-[var(--color-text)] max-w-full truncate", children: renderSongLink ? renderSongLink(track) : (0, import_genre_tree12.isSong)(track) ? /* @__PURE__ */ (0, import_jsx_runtime29.jsx)(MusicBrainzRecordingLink, { song: track, className: "hover:underline focus-visible:underline", children: track.title }) : track.title }) }),
1984
- track.artists && track.artists.length > 0 ? /* @__PURE__ */ (0, import_jsx_runtime29.jsx)("div", { className: "artist text-base max-w-full truncate text-[var(--color-text-muted)]", children: /* @__PURE__ */ (0, import_jsx_runtime29.jsx)(import_genre_tree13.ArtistNames, { artists: track.artists, renderArtistLink }) }) : ""
1865
+ /* @__PURE__ */ (0, import_jsx_runtime28.jsxs)("div", { className: "title-artist-container flex flex-col items-start justify-center min-w-0 flex-1", children: [
1866
+ /* @__PURE__ */ (0, import_jsx_runtime28.jsx)("div", { className: "flex max-w-full items-center gap-2", children: /* @__PURE__ */ (0, import_jsx_runtime28.jsx)("div", { className: "title text-lg font-bold text-[var(--color-text)] max-w-full truncate", children: renderSongLink ? renderSongLink(track) : (0, import_genre_tree12.isSong)(track) ? /* @__PURE__ */ (0, import_jsx_runtime28.jsx)(MusicBrainzRecordingLink, { song: track, className: "hover:underline focus-visible:underline", children: track.title }) : track.title }) }),
1867
+ track.artists && track.artists.length > 0 ? /* @__PURE__ */ (0, import_jsx_runtime28.jsx)("div", { className: "artist text-base max-w-full truncate text-[var(--color-text-muted)]", children: /* @__PURE__ */ (0, import_jsx_runtime28.jsx)(import_genre_tree13.ArtistNames, { artists: track.artists, renderArtistLink }) }) : ""
1985
1868
  ] }),
1986
- /* @__PURE__ */ (0, import_jsx_runtime29.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_runtime29.jsx)("div", { className: "genre-name max-w-full whitespace-normal break-words p-1 text-xs font-bold", children: track.genre.name }) : "" }),
1987
- renderDuration && /* @__PURE__ */ (0, import_jsx_runtime29.jsx)(
1869
+ /* @__PURE__ */ (0, import_jsx_runtime28.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_runtime28.jsx)("div", { className: "genre-name max-w-full whitespace-normal break-words p-1 text-xs font-bold", children: track.genre.name }) : "" }),
1870
+ renderDuration && /* @__PURE__ */ (0, import_jsx_runtime28.jsx)(
1988
1871
  "div",
1989
1872
  {
1990
1873
  className: "duration flex text-base w-16 items-center justify-center",
@@ -1992,22 +1875,22 @@ function TrackItem({
1992
1875
  children: renderDuration(track)
1993
1876
  }
1994
1877
  ),
1995
- renderActions && /* @__PURE__ */ (0, import_jsx_runtime29.jsx)("div", { className: "edit flex text-base w-6 items-center justify-center mr-2 cursor-pointer", children: renderActions(track) })
1878
+ renderActions && /* @__PURE__ */ (0, import_jsx_runtime28.jsx)("div", { className: "edit flex text-base w-6 items-center justify-center mr-2 cursor-pointer", children: renderActions(track) })
1996
1879
  ]
1997
1880
  }
1998
1881
  );
1999
1882
  }
2000
1883
 
2001
1884
  // src/genre-tree/artist/ArtistSongs.tsx
2002
- var import_jsx_runtime30 = require("react/jsx-runtime");
1885
+ var import_jsx_runtime29 = require("react/jsx-runtime");
2003
1886
  var SongPageSchema2 = (0, import_transport7.PaginatedResponseSchema)(import_genre_tree14.SongDetailedSchema);
2004
1887
  var SORTS = [
2005
1888
  { ordering: "title", label: "Title" },
2006
1889
  { ordering: "-albumYear", label: "Year" }
2007
1890
  ];
2008
1891
  function ArtistSongs({ artist, getBackendBaseUrl, renderArtistLink, renderSongLink }) {
2009
- const [page, setPage] = (0, import_react15.useState)(1);
2010
- const [ordering, setOrdering] = (0, import_react15.useState)("title");
1892
+ const [page, setPage] = (0, import_react14.useState)(1);
1893
+ const [ordering, setOrdering] = (0, import_react14.useState)("title");
2011
1894
  const { fetch } = (0, import_transport7.useFetchWrapper)(getBackendBaseUrl);
2012
1895
  const { playNewTrackListFromArtist } = (0, import_genre_tree14.useTrackList)();
2013
1896
  const { playerTrackObject } = (0, import_player10.usePlayer)();
@@ -2032,21 +1915,21 @@ function ArtistSongs({ artist, getBackendBaseUrl, renderArtistLink, renderSongLi
2032
1915
  event.stopPropagation();
2033
1916
  void play(song.uuid);
2034
1917
  };
2035
- return /* @__PURE__ */ (0, import_jsx_runtime30.jsx)(import_ui10.PanelSection, { title: "Songs", children: isPending ? /* @__PURE__ */ (0, import_jsx_runtime30.jsx)(import_ui10.Skeleton, { className: "h-14", "aria-busy": "true" }) : isError ? /* @__PURE__ */ (0, import_jsx_runtime30.jsx)("p", { className: "text-red-600", children: "Could not load songs." }) : data.results.length === 0 ? /* @__PURE__ */ (0, import_jsx_runtime30.jsx)("p", { className: "text-[var(--color-text-muted)]", children: "No songs yet." }) : /* @__PURE__ */ (0, import_jsx_runtime30.jsxs)(import_jsx_runtime30.Fragment, { children: [
2036
- /* @__PURE__ */ (0, import_jsx_runtime30.jsxs)("div", { className: "flex flex-wrap items-center justify-between gap-2 mb-2", children: [
2037
- /* @__PURE__ */ (0, import_jsx_runtime30.jsxs)(
1918
+ return /* @__PURE__ */ (0, import_jsx_runtime29.jsx)(import_ui10.PanelSection, { title: "Songs", children: isPending ? /* @__PURE__ */ (0, import_jsx_runtime29.jsx)(import_ui10.Skeleton, { className: "h-14", "aria-busy": "true" }) : isError ? /* @__PURE__ */ (0, import_jsx_runtime29.jsx)("p", { className: "text-red-600", children: "Could not load songs." }) : data.results.length === 0 ? /* @__PURE__ */ (0, import_jsx_runtime29.jsx)("p", { className: "text-[var(--color-text-muted)]", children: "No songs yet." }) : /* @__PURE__ */ (0, import_jsx_runtime29.jsxs)(import_jsx_runtime29.Fragment, { children: [
1919
+ /* @__PURE__ */ (0, import_jsx_runtime29.jsxs)("div", { className: "flex flex-wrap items-center justify-between gap-2 mb-2", children: [
1920
+ /* @__PURE__ */ (0, import_jsx_runtime29.jsxs)(
2038
1921
  "button",
2039
1922
  {
2040
1923
  type: "button",
2041
1924
  onClick: () => void play(),
2042
1925
  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",
2043
1926
  children: [
2044
- /* @__PURE__ */ (0, import_jsx_runtime30.jsx)(import_lucide_react10.Play, { className: "h-3.5 w-3.5", "aria-hidden": "true" }),
1927
+ /* @__PURE__ */ (0, import_jsx_runtime29.jsx)(import_lucide_react9.Play, { className: "h-3.5 w-3.5", "aria-hidden": "true" }),
2045
1928
  "Play all"
2046
1929
  ]
2047
1930
  }
2048
1931
  ),
2049
- /* @__PURE__ */ (0, import_jsx_runtime30.jsx)("div", { role: "group", "aria-label": "Sort songs by", className: "flex gap-1 text-xs", children: SORTS.map((sort) => /* @__PURE__ */ (0, import_jsx_runtime30.jsx)(
1932
+ /* @__PURE__ */ (0, import_jsx_runtime29.jsx)("div", { role: "group", "aria-label": "Sort songs by", className: "flex gap-1 text-xs", children: SORTS.map((sort) => /* @__PURE__ */ (0, import_jsx_runtime29.jsx)(
2050
1933
  "button",
2051
1934
  {
2052
1935
  type: "button",
@@ -2061,7 +1944,7 @@ function ArtistSongs({ artist, getBackendBaseUrl, renderArtistLink, renderSongLi
2061
1944
  sort.ordering
2062
1945
  )) })
2063
1946
  ] }),
2064
- /* @__PURE__ */ (0, import_jsx_runtime30.jsx)("ul", { className: "overflow-hidden mb-2 bg-zinc-950 rounded-lg", children: data.results.map((song, index) => /* @__PURE__ */ (0, import_jsx_runtime30.jsx)("li", { onClickCapture: (event) => handleRowClickCapture(event, song), children: /* @__PURE__ */ (0, import_jsx_runtime30.jsx)(
1947
+ /* @__PURE__ */ (0, import_jsx_runtime29.jsx)("ul", { className: "overflow-hidden mb-2 bg-zinc-950 rounded-lg", children: data.results.map((song, index) => /* @__PURE__ */ (0, import_jsx_runtime29.jsx)("li", { onClickCapture: (event) => handleRowClickCapture(event, song), children: /* @__PURE__ */ (0, import_jsx_runtime29.jsx)(
2065
1948
  TrackItem,
2066
1949
  {
2067
1950
  track: song,
@@ -2070,12 +1953,12 @@ function ArtistSongs({ artist, getBackendBaseUrl, renderArtistLink, renderSongLi
2070
1953
  renderSongLink
2071
1954
  }
2072
1955
  ) }, song.uuid)) }),
2073
- /* @__PURE__ */ (0, import_jsx_runtime30.jsx)(import_ui10.Pagination, { currentPage: page, totalPages: data.totalPages, onPageChange: setPage })
1956
+ /* @__PURE__ */ (0, import_jsx_runtime29.jsx)(import_ui10.Pagination, { currentPage: page, totalPages: data.totalPages, onPageChange: setPage })
2074
1957
  ] }) });
2075
1958
  }
2076
1959
 
2077
1960
  // src/genre-tree/artist/useArtistPanel.tsx
2078
- var import_jsx_runtime31 = require("react/jsx-runtime");
1961
+ var import_jsx_runtime30 = require("react/jsx-runtime");
2079
1962
  function useArtistPanel({
2080
1963
  uuid,
2081
1964
  scope,
@@ -2096,26 +1979,26 @@ function useArtistPanel({
2096
1979
  return {
2097
1980
  ...panel,
2098
1981
  title: "Artist",
2099
- content: /* @__PURE__ */ (0, import_jsx_runtime31.jsxs)("div", { "aria-busy": "true", className: "flex flex-col gap-2.5 px-6 py-2.5", children: [
2100
- /* @__PURE__ */ (0, import_jsx_runtime31.jsx)(import_ui11.Skeleton, { className: "h-3 w-16" }),
2101
- /* @__PURE__ */ (0, import_jsx_runtime31.jsx)(import_ui11.Skeleton, { className: "h-4 w-32" }),
2102
- /* @__PURE__ */ (0, import_jsx_runtime31.jsx)(import_ui11.Skeleton, { className: "h-3 w-16" }),
2103
- /* @__PURE__ */ (0, import_jsx_runtime31.jsx)(import_ui11.Skeleton, { className: "h-4 w-24" }),
2104
- /* @__PURE__ */ (0, import_jsx_runtime31.jsx)(import_ui11.Skeleton, { className: "h-16 mt-2.5" }),
2105
- /* @__PURE__ */ (0, import_jsx_runtime31.jsx)(import_ui11.Skeleton, { className: "h-16" })
1982
+ content: /* @__PURE__ */ (0, import_jsx_runtime30.jsxs)("div", { "aria-busy": "true", className: "flex flex-col gap-2.5 px-6 py-2.5", children: [
1983
+ /* @__PURE__ */ (0, import_jsx_runtime30.jsx)(import_ui11.Skeleton, { className: "h-3 w-16" }),
1984
+ /* @__PURE__ */ (0, import_jsx_runtime30.jsx)(import_ui11.Skeleton, { className: "h-4 w-32" }),
1985
+ /* @__PURE__ */ (0, import_jsx_runtime30.jsx)(import_ui11.Skeleton, { className: "h-3 w-16" }),
1986
+ /* @__PURE__ */ (0, import_jsx_runtime30.jsx)(import_ui11.Skeleton, { className: "h-4 w-24" }),
1987
+ /* @__PURE__ */ (0, import_jsx_runtime30.jsx)(import_ui11.Skeleton, { className: "h-16 mt-2.5" }),
1988
+ /* @__PURE__ */ (0, import_jsx_runtime30.jsx)(import_ui11.Skeleton, { className: "h-16" })
2106
1989
  ] })
2107
1990
  };
2108
1991
  case "not-found":
2109
1992
  return {
2110
1993
  ...panel,
2111
1994
  title: "Artist not found",
2112
- content: /* @__PURE__ */ (0, import_jsx_runtime31.jsx)("p", { className: "px-6 py-2.5 text-[var(--color-text-muted)]", children: "This artist does not exist." })
1995
+ content: /* @__PURE__ */ (0, import_jsx_runtime30.jsx)("p", { className: "px-6 py-2.5 text-[var(--color-text-muted)]", children: "This artist does not exist." })
2113
1996
  };
2114
1997
  case "error":
2115
1998
  return {
2116
1999
  ...panel,
2117
2000
  title: "Artist",
2118
- content: /* @__PURE__ */ (0, import_jsx_runtime31.jsx)("p", { className: "px-6 py-2.5 text-red-600", children: "Could not load this artist." })
2001
+ content: /* @__PURE__ */ (0, import_jsx_runtime30.jsx)("p", { className: "px-6 py-2.5 text-red-600", children: "Could not load this artist." })
2119
2002
  };
2120
2003
  case "success": {
2121
2004
  const { artist } = result;
@@ -2124,13 +2007,13 @@ function useArtistPanel({
2124
2007
  return {
2125
2008
  ...panel,
2126
2009
  title: artist.name,
2127
- content: /* @__PURE__ */ (0, import_jsx_runtime31.jsxs)(import_jsx_runtime31.Fragment, { children: [
2128
- shareUrl && /* @__PURE__ */ (0, import_jsx_runtime31.jsx)("div", { className: "genre-action-band border-b border-[var(--color-border)]", children: /* @__PURE__ */ (0, import_jsx_runtime31.jsx)("div", { className: "flex flex-wrap gap-1 px-3.5 py-3", children: shareTarget && /* @__PURE__ */ (0, import_jsx_runtime31.jsx)(ShareActionButton, { target: shareTarget, url: shareUrl }) }) }),
2129
- artist.image && /* @__PURE__ */ (0, import_jsx_runtime31.jsx)(ArtistPhoto, { image: artist.image, name: artist.name }),
2130
- /* @__PURE__ */ (0, import_jsx_runtime31.jsx)(ArtistHeader, { artist }),
2131
- /* @__PURE__ */ (0, import_jsx_runtime31.jsx)(ArtistMembers, { title: "Members", memberships: artist.members, renderArtistLink }),
2132
- /* @__PURE__ */ (0, import_jsx_runtime31.jsx)(ArtistMembers, { title: "Member of", memberships: artist.memberOf, renderArtistLink }),
2133
- artist.tracksCount > 0 && /* @__PURE__ */ (0, import_jsx_runtime31.jsx)(
2010
+ content: /* @__PURE__ */ (0, import_jsx_runtime30.jsxs)(import_jsx_runtime30.Fragment, { children: [
2011
+ shareUrl && /* @__PURE__ */ (0, import_jsx_runtime30.jsx)("div", { className: "genre-action-band border-b border-[var(--color-border)]", children: /* @__PURE__ */ (0, import_jsx_runtime30.jsx)("div", { className: "flex flex-wrap gap-1 px-3.5 py-3", children: shareTarget && /* @__PURE__ */ (0, import_jsx_runtime30.jsx)(ShareActionButton, { target: shareTarget, url: shareUrl }) }) }),
2012
+ artist.image && /* @__PURE__ */ (0, import_jsx_runtime30.jsx)(ArtistPhoto, { image: artist.image, name: artist.name }),
2013
+ /* @__PURE__ */ (0, import_jsx_runtime30.jsx)(ArtistHeader, { artist }),
2014
+ /* @__PURE__ */ (0, import_jsx_runtime30.jsx)(ArtistMembers, { title: "Members", memberships: artist.members, renderArtistLink }),
2015
+ /* @__PURE__ */ (0, import_jsx_runtime30.jsx)(ArtistMembers, { title: "Member of", memberships: artist.memberOf, renderArtistLink }),
2016
+ artist.tracksCount > 0 && /* @__PURE__ */ (0, import_jsx_runtime30.jsx)(
2134
2017
  ArtistSongs,
2135
2018
  {
2136
2019
  artist: { uuid: artist.uuid, name: artist.name },
@@ -2140,7 +2023,7 @@ function useArtistPanel({
2140
2023
  }
2141
2024
  )
2142
2025
  ] }),
2143
- extraContent: artist.albums.length > 0 ? /* @__PURE__ */ (0, import_jsx_runtime31.jsx)(ArtistDiscography, { albums: artist.albums }) : void 0
2026
+ extraContent: artist.albums.length > 0 ? /* @__PURE__ */ (0, import_jsx_runtime30.jsx)(ArtistDiscography, { albums: artist.albums }) : void 0
2144
2027
  };
2145
2028
  }
2146
2029
  }
@@ -2151,45 +2034,45 @@ var import_ui13 = require("@behindthemusictree/ui");
2151
2034
  var import_genre_tree17 = require("@behindthemusictree/app-kit/genre-tree");
2152
2035
 
2153
2036
  // src/genre-tree/song/SongPanelContent.tsx
2154
- var import_react16 = require("react");
2155
- var import_lucide_react11 = require("lucide-react");
2037
+ var import_react15 = require("react");
2038
+ var import_lucide_react10 = require("lucide-react");
2156
2039
  var import_ui12 = require("@behindthemusictree/ui");
2157
2040
  var import_player11 = require("@behindthemusictree/app-kit/player");
2158
2041
  var import_genre_tree16 = require("@behindthemusictree/app-kit/genre-tree");
2159
- var import_jsx_runtime32 = require("react/jsx-runtime");
2042
+ var import_jsx_runtime31 = require("react/jsx-runtime");
2160
2043
  var numberFormat = new Intl.NumberFormat("en");
2161
2044
  var dateFormat = new Intl.DateTimeFormat("en", { dateStyle: "medium" });
2162
2045
  var externalLink = "text-[var(--color-primary)] hover:underline focus-visible:underline";
2163
2046
  function SongHistory({ uuid, getBackendBaseUrl }) {
2164
- const [open, setOpen] = (0, import_react16.useState)(false);
2047
+ const [open, setOpen] = (0, import_react15.useState)(false);
2165
2048
  const history = (0, import_genre_tree16.useSongHistory)(uuid, getBackendBaseUrl, open);
2166
2049
  let body = null;
2167
- if (history.status === "loading") body = /* @__PURE__ */ (0, import_jsx_runtime32.jsx)(import_ui12.Skeleton, { className: "h-10", "aria-busy": "true" });
2168
- else if (history.status === "error") body = /* @__PURE__ */ (0, import_jsx_runtime32.jsx)("p", { className: "text-red-600", children: "Could not load the history." });
2050
+ if (history.status === "loading") body = /* @__PURE__ */ (0, import_jsx_runtime31.jsx)(import_ui12.Skeleton, { className: "h-10", "aria-busy": "true" });
2051
+ else if (history.status === "error") body = /* @__PURE__ */ (0, import_jsx_runtime31.jsx)("p", { className: "text-red-600", children: "Could not load the history." });
2169
2052
  else if (history.status === "success")
2170
- body = history.entries.length === 0 ? /* @__PURE__ */ (0, import_jsx_runtime32.jsx)("p", { className: "text-[var(--color-text-muted)]", children: "No changes yet." }) : /* @__PURE__ */ (0, import_jsx_runtime32.jsx)("ul", { className: "divide-y divide-[var(--color-border)]", children: history.entries.map((entry) => /* @__PURE__ */ (0, import_jsx_runtime32.jsxs)("li", { className: "py-1.5 text-sm", children: [
2171
- /* @__PURE__ */ (0, import_jsx_runtime32.jsx)("span", { className: "font-medium", children: entry.action }),
2172
- entry.oldValue !== null || entry.newValue !== null ? /* @__PURE__ */ (0, import_jsx_runtime32.jsxs)("span", { className: "text-[var(--color-text-muted)]", children: [
2053
+ body = history.entries.length === 0 ? /* @__PURE__ */ (0, import_jsx_runtime31.jsx)("p", { className: "text-[var(--color-text-muted)]", children: "No changes yet." }) : /* @__PURE__ */ (0, import_jsx_runtime31.jsx)("ul", { className: "divide-y divide-[var(--color-border)]", children: history.entries.map((entry) => /* @__PURE__ */ (0, import_jsx_runtime31.jsxs)("li", { className: "py-1.5 text-sm", children: [
2054
+ /* @__PURE__ */ (0, import_jsx_runtime31.jsx)("span", { className: "font-medium", children: entry.action }),
2055
+ entry.oldValue !== null || entry.newValue !== null ? /* @__PURE__ */ (0, import_jsx_runtime31.jsxs)("span", { className: "text-[var(--color-text-muted)]", children: [
2173
2056
  " ",
2174
2057
  "\u2014 ",
2175
2058
  entry.oldValue ?? "none",
2176
2059
  " \u2192 ",
2177
2060
  entry.newValue ?? "none"
2178
2061
  ] }) : null,
2179
- /* @__PURE__ */ (0, import_jsx_runtime32.jsxs)("span", { className: "block text-xs text-[var(--color-text-muted)]", children: [
2062
+ /* @__PURE__ */ (0, import_jsx_runtime31.jsxs)("span", { className: "block text-xs text-[var(--color-text-muted)]", children: [
2180
2063
  entry.actorPseudo ?? "Anonymous",
2181
2064
  " \xB7 ",
2182
2065
  dateFormat.format(new Date(entry.createdOn))
2183
2066
  ] })
2184
2067
  ] }, entry.uuid)) });
2185
- return /* @__PURE__ */ (0, import_jsx_runtime32.jsxs)(
2068
+ return /* @__PURE__ */ (0, import_jsx_runtime31.jsxs)(
2186
2069
  "details",
2187
2070
  {
2188
2071
  onToggle: (event) => setOpen(event.currentTarget.open),
2189
2072
  className: "px-6 pb-2.5 mt-2.5 border-t border-[var(--color-border)]",
2190
2073
  children: [
2191
- /* @__PURE__ */ (0, import_jsx_runtime32.jsx)("summary", { className: "mt-2.5 cursor-pointer text-[0.6875rem] font-semibold uppercase text-[var(--color-text-muted)]", children: "History" }),
2192
- /* @__PURE__ */ (0, import_jsx_runtime32.jsx)("div", { className: "mt-1.5", children: body })
2074
+ /* @__PURE__ */ (0, import_jsx_runtime31.jsx)("summary", { className: "mt-2.5 cursor-pointer text-[0.6875rem] font-semibold uppercase text-[var(--color-text-muted)]", children: "History" }),
2075
+ /* @__PURE__ */ (0, import_jsx_runtime31.jsx)("div", { className: "mt-1.5", children: body })
2193
2076
  ]
2194
2077
  }
2195
2078
  );
@@ -2215,13 +2098,13 @@ function SongPanelContent({
2215
2098
  const path = genrePath.length > 0 ? genrePath : null;
2216
2099
  fields.push({
2217
2100
  label: "Genre",
2218
- value: path ? /* @__PURE__ */ (0, import_jsx_runtime32.jsx)("span", { className: "flex flex-wrap items-center gap-x-1", children: path.map((genrePlaylist, index) => /* @__PURE__ */ (0, import_jsx_runtime32.jsxs)("span", { children: [
2219
- index > 0 && /* @__PURE__ */ (0, import_jsx_runtime32.jsx)("span", { "aria-hidden": "true", children: "\u203A " }),
2220
- /* @__PURE__ */ (0, import_jsx_runtime32.jsx)("button", { type: "button", onClick: () => onSelectGenre(genrePlaylist), className: "hover:underline", children: genrePlaylist.name })
2101
+ value: path ? /* @__PURE__ */ (0, import_jsx_runtime31.jsx)("span", { className: "flex flex-wrap items-center gap-x-1", children: path.map((genrePlaylist, index) => /* @__PURE__ */ (0, import_jsx_runtime31.jsxs)("span", { children: [
2102
+ index > 0 && /* @__PURE__ */ (0, import_jsx_runtime31.jsx)("span", { "aria-hidden": "true", children: "\u203A " }),
2103
+ /* @__PURE__ */ (0, import_jsx_runtime31.jsx)("button", { type: "button", onClick: () => onSelectGenre(genrePlaylist), className: "hover:underline", children: genrePlaylist.name })
2221
2104
  ] }, genrePlaylist.uuid)) }) : song.genre.name
2222
2105
  });
2223
2106
  } else {
2224
- fields.push({ label: "Genre", value: /* @__PURE__ */ (0, import_jsx_runtime32.jsx)("span", { className: "text-[var(--color-text-muted)]", children: "No genre" }) });
2107
+ fields.push({ label: "Genre", value: /* @__PURE__ */ (0, import_jsx_runtime31.jsx)("span", { className: "text-[var(--color-text-muted)]", children: "No genre" }) });
2225
2108
  }
2226
2109
  if (song.firstReleaseYear) fields.push({ label: "First released", value: song.firstReleaseYear });
2227
2110
  const viewCount = song.youtubeVideo?.viewCount;
@@ -2231,11 +2114,11 @@ function SongPanelContent({
2231
2114
  const links = [];
2232
2115
  if (musicBrainzUrl)
2233
2116
  links.push(
2234
- /* @__PURE__ */ (0, import_jsx_runtime32.jsx)("a", { href: musicBrainzUrl, target: "_blank", rel: "noopener noreferrer", className: externalLink, children: "MusicBrainz" }, "mb")
2117
+ /* @__PURE__ */ (0, import_jsx_runtime31.jsx)("a", { href: musicBrainzUrl, target: "_blank", rel: "noopener noreferrer", className: externalLink, children: "MusicBrainz" }, "mb")
2235
2118
  );
2236
2119
  if (song.youtubeVideo)
2237
2120
  links.push(
2238
- /* @__PURE__ */ (0, import_jsx_runtime32.jsx)(
2121
+ /* @__PURE__ */ (0, import_jsx_runtime31.jsx)(
2239
2122
  "a",
2240
2123
  {
2241
2124
  href: `https://www.youtube.com/watch?v=${encodeURIComponent(song.youtubeVideo.id)}`,
@@ -2250,33 +2133,33 @@ function SongPanelContent({
2250
2133
  if (links.length > 0)
2251
2134
  fields.push({
2252
2135
  label: "Links",
2253
- value: /* @__PURE__ */ (0, import_jsx_runtime32.jsx)("span", { className: "flex gap-3", children: links })
2136
+ value: /* @__PURE__ */ (0, import_jsx_runtime31.jsx)("span", { className: "flex gap-3", children: links })
2254
2137
  });
2255
- return /* @__PURE__ */ (0, import_jsx_runtime32.jsxs)(import_jsx_runtime32.Fragment, { children: [
2256
- /* @__PURE__ */ (0, import_jsx_runtime32.jsx)("div", { className: "genre-action-band border-b border-[var(--color-border)]", children: /* @__PURE__ */ (0, import_jsx_runtime32.jsxs)("div", { className: "flex flex-wrap gap-1 px-3.5 py-3", children: [
2257
- unplayableReason ? /* @__PURE__ */ (0, import_jsx_runtime32.jsx)("p", { className: "flex items-center px-2.5 text-sm text-[var(--color-text-muted)]", children: unplayableReason }) : /* @__PURE__ */ (0, import_jsx_runtime32.jsxs)(
2138
+ return /* @__PURE__ */ (0, import_jsx_runtime31.jsxs)(import_jsx_runtime31.Fragment, { children: [
2139
+ /* @__PURE__ */ (0, import_jsx_runtime31.jsx)("div", { className: "genre-action-band border-b border-[var(--color-border)]", children: /* @__PURE__ */ (0, import_jsx_runtime31.jsxs)("div", { className: "flex flex-wrap gap-1 px-3.5 py-3", children: [
2140
+ unplayableReason ? /* @__PURE__ */ (0, import_jsx_runtime31.jsx)("p", { className: "flex items-center px-2.5 text-sm text-[var(--color-text-muted)]", children: unplayableReason }) : /* @__PURE__ */ (0, import_jsx_runtime31.jsxs)(
2258
2141
  "button",
2259
2142
  {
2260
2143
  type: "button",
2261
2144
  onClick: () => isCurrent ? handlePlayPauseAction() : playNewTrackListFromTrackUuid(song, scope),
2262
2145
  className: "group flex flex-col items-center gap-1 w-16 text-xs text-center text-[var(--color-text)] focus-visible:outline-none",
2263
2146
  children: [
2264
- /* @__PURE__ */ (0, import_jsx_runtime32.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_runtime32.jsx)(import_lucide_react11.Pause, { "aria-hidden": "true", className: "w-5 h-5" }) : /* @__PURE__ */ (0, import_jsx_runtime32.jsx)(import_lucide_react11.Play, { "aria-hidden": "true", className: "w-5 h-5" }) }),
2147
+ /* @__PURE__ */ (0, import_jsx_runtime31.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_runtime31.jsx)(import_lucide_react10.Pause, { "aria-hidden": "true", className: "w-5 h-5" }) : /* @__PURE__ */ (0, import_jsx_runtime31.jsx)(import_lucide_react10.Play, { "aria-hidden": "true", className: "w-5 h-5" }) }),
2265
2148
  isPlaying ? "Pause" : "Play"
2266
2149
  ]
2267
2150
  }
2268
2151
  ),
2269
- shareUrl && /* @__PURE__ */ (0, import_jsx_runtime32.jsx)(ShareActionButton, { target: shareTarget, url: shareUrl })
2152
+ shareUrl && /* @__PURE__ */ (0, import_jsx_runtime31.jsx)(ShareActionButton, { target: shareTarget, url: shareUrl })
2270
2153
  ] }) }),
2271
- song.artists && song.artists.length > 0 && /* @__PURE__ */ (0, import_jsx_runtime32.jsx)("p", { className: "px-6 pt-2.5 text-[var(--color-text-muted)]", children: /* @__PURE__ */ (0, import_jsx_runtime32.jsx)(import_genre_tree16.ArtistNames, { artists: song.artists, renderArtistLink }) }),
2272
- /* @__PURE__ */ (0, import_jsx_runtime32.jsx)(import_ui12.FieldList, { items: fields }),
2273
- song.essentialGenres && song.essentialGenres.length > 0 && /* @__PURE__ */ (0, import_jsx_runtime32.jsx)(import_ui12.PanelSection, { title: "Essential in", children: /* @__PURE__ */ (0, import_jsx_runtime32.jsx)("ul", { children: song.essentialGenres.map((genre) => /* @__PURE__ */ (0, import_jsx_runtime32.jsx)("li", { className: "py-0.5", children: genre.name }, genre.uuid)) }) }),
2274
- /* @__PURE__ */ (0, import_jsx_runtime32.jsx)(SongHistory, { uuid: song.uuid, getBackendBaseUrl })
2154
+ song.artists && song.artists.length > 0 && /* @__PURE__ */ (0, import_jsx_runtime31.jsx)("p", { className: "px-6 pt-2.5 text-[var(--color-text-muted)]", children: /* @__PURE__ */ (0, import_jsx_runtime31.jsx)(import_genre_tree16.ArtistNames, { artists: song.artists, renderArtistLink }) }),
2155
+ /* @__PURE__ */ (0, import_jsx_runtime31.jsx)(import_ui12.FieldList, { items: fields }),
2156
+ song.essentialGenres && song.essentialGenres.length > 0 && /* @__PURE__ */ (0, import_jsx_runtime31.jsx)(import_ui12.PanelSection, { title: "Essential in", children: /* @__PURE__ */ (0, import_jsx_runtime31.jsx)("ul", { children: song.essentialGenres.map((genre) => /* @__PURE__ */ (0, import_jsx_runtime31.jsx)("li", { className: "py-0.5", children: genre.name }, genre.uuid)) }) }),
2157
+ /* @__PURE__ */ (0, import_jsx_runtime31.jsx)(SongHistory, { uuid: song.uuid, getBackendBaseUrl })
2275
2158
  ] });
2276
2159
  }
2277
2160
 
2278
2161
  // src/genre-tree/song/useSongPanel.tsx
2279
- var import_jsx_runtime33 = require("react/jsx-runtime");
2162
+ var import_jsx_runtime32 = require("react/jsx-runtime");
2280
2163
  function useSongPanel({
2281
2164
  uuid,
2282
2165
  scope,
@@ -2298,24 +2181,24 @@ function useSongPanel({
2298
2181
  return {
2299
2182
  ...panel,
2300
2183
  title: "Song",
2301
- content: /* @__PURE__ */ (0, import_jsx_runtime33.jsxs)("div", { "aria-busy": "true", className: "flex flex-col gap-2.5 px-6 py-2.5", children: [
2302
- /* @__PURE__ */ (0, import_jsx_runtime33.jsx)(import_ui13.Skeleton, { className: "h-3 w-16" }),
2303
- /* @__PURE__ */ (0, import_jsx_runtime33.jsx)(import_ui13.Skeleton, { className: "h-4 w-32" }),
2304
- /* @__PURE__ */ (0, import_jsx_runtime33.jsx)(import_ui13.Skeleton, { className: "h-3 w-16" }),
2305
- /* @__PURE__ */ (0, import_jsx_runtime33.jsx)(import_ui13.Skeleton, { className: "h-4 w-24" })
2184
+ content: /* @__PURE__ */ (0, import_jsx_runtime32.jsxs)("div", { "aria-busy": "true", className: "flex flex-col gap-2.5 px-6 py-2.5", children: [
2185
+ /* @__PURE__ */ (0, import_jsx_runtime32.jsx)(import_ui13.Skeleton, { className: "h-3 w-16" }),
2186
+ /* @__PURE__ */ (0, import_jsx_runtime32.jsx)(import_ui13.Skeleton, { className: "h-4 w-32" }),
2187
+ /* @__PURE__ */ (0, import_jsx_runtime32.jsx)(import_ui13.Skeleton, { className: "h-3 w-16" }),
2188
+ /* @__PURE__ */ (0, import_jsx_runtime32.jsx)(import_ui13.Skeleton, { className: "h-4 w-24" })
2306
2189
  ] })
2307
2190
  };
2308
2191
  case "not-found":
2309
2192
  return {
2310
2193
  ...panel,
2311
2194
  title: "Song not found",
2312
- content: /* @__PURE__ */ (0, import_jsx_runtime33.jsx)("p", { className: "px-6 py-2.5 text-[var(--color-text-muted)]", children: "This song does not exist." })
2195
+ content: /* @__PURE__ */ (0, import_jsx_runtime32.jsx)("p", { className: "px-6 py-2.5 text-[var(--color-text-muted)]", children: "This song does not exist." })
2313
2196
  };
2314
2197
  case "error":
2315
2198
  return {
2316
2199
  ...panel,
2317
2200
  title: "Song",
2318
- content: /* @__PURE__ */ (0, import_jsx_runtime33.jsx)("p", { className: "px-6 py-2.5 text-red-600", children: "Could not load this song." })
2201
+ content: /* @__PURE__ */ (0, import_jsx_runtime32.jsx)("p", { className: "px-6 py-2.5 text-red-600", children: "Could not load this song." })
2319
2202
  };
2320
2203
  case "success": {
2321
2204
  const { song } = result;
@@ -2323,7 +2206,7 @@ function useSongPanel({
2323
2206
  return {
2324
2207
  ...panel,
2325
2208
  title: song.title,
2326
- content: /* @__PURE__ */ (0, import_jsx_runtime33.jsx)(
2209
+ content: /* @__PURE__ */ (0, import_jsx_runtime32.jsx)(
2327
2210
  SongPanelContent,
2328
2211
  {
2329
2212
  song,
@@ -2343,12 +2226,12 @@ function useSongPanel({
2343
2226
  // src/genre-tree/SizeMetricToggle.tsx
2344
2227
  var import_md = require("react-icons/md");
2345
2228
  var import_ui14 = require("@behindthemusictree/ui");
2346
- var import_jsx_runtime34 = require("react/jsx-runtime");
2229
+ var import_jsx_runtime33 = require("react/jsx-runtime");
2347
2230
  function SizeMetricToggle({
2348
2231
  value,
2349
2232
  onChange
2350
2233
  }) {
2351
- return /* @__PURE__ */ (0, import_jsx_runtime34.jsx)(
2234
+ return /* @__PURE__ */ (0, import_jsx_runtime33.jsx)(
2352
2235
  import_ui14.SegmentedControl,
2353
2236
  {
2354
2237
  variant: "floating",
@@ -2356,26 +2239,19 @@ function SizeMetricToggle({
2356
2239
  value,
2357
2240
  onChange,
2358
2241
  options: [
2359
- { value: "songs", label: /* @__PURE__ */ (0, import_jsx_runtime34.jsx)(import_md.MdMusicNote, { size: 18, "aria-hidden": true }), "aria-label": "Size by songs", title: "Node size by song count" },
2360
- { value: "views", label: /* @__PURE__ */ (0, import_jsx_runtime34.jsx)(import_md.MdVisibility, { size: 18, "aria-hidden": true }), "aria-label": "Size by views", title: "Node size by song views" }
2242
+ { value: "songs", label: /* @__PURE__ */ (0, import_jsx_runtime33.jsx)(import_md.MdMusicNote, { size: 18, "aria-hidden": true }), "aria-label": "Size by songs", title: "Node size by song count" },
2243
+ { value: "views", label: /* @__PURE__ */ (0, import_jsx_runtime33.jsx)(import_md.MdVisibility, { size: 18, "aria-hidden": true }), "aria-label": "Size by views", title: "Node size by song views" }
2361
2244
  ]
2362
2245
  }
2363
2246
  );
2364
2247
  }
2365
2248
 
2366
2249
  // src/genre-tree/GenreTreeView.tsx
2367
- var import_jsx_runtime35 = require("react/jsx-runtime");
2368
- var PREVIOUS_BUTTON_CSS = `
2369
- .genre-tree-view .search-overlay .gtv-info-panel-previous { display: none; }
2370
- .genre-tree-view:has(.search-overlay .gtv-info-panel-previous) .genre-search input { padding-left: 2.75rem; }
2371
- .genre-tree-view:has(.search-overlay .gtv-info-panel-previous) .genre-search-previous { display: flex; }
2372
- @keyframes genre-tree-view-previous-in { from { opacity: 0; transform: translate(-0.5rem, -50%); } to { opacity: 1; transform: translate(0, -50%); } }
2373
- @media (prefers-reduced-motion: no-preference) {
2374
- .genre-search-previous { animation: genre-tree-view-previous-in 200ms ease-out; }
2375
- }
2376
- `;
2250
+ var import_jsx_runtime34 = require("react/jsx-runtime");
2377
2251
  var NO_POP_ROOT_TITLE = "This genre tree has no 'Mainstream Pop' root yet";
2378
- var REGIONS_CLASS = "[--gtv-center-left:var(--layout-center-left)] [--gtv-center-right:var(--layout-center-right)] [--gtv-center-top:var(--layout-band-top)] [&_.gtv-info-panel--overlay]:[--gtv-info-panel-header-inset:0px]";
2252
+ var REGIONS_CLASS = "[--gtv-center-left:var(--layout-center-left)] [--gtv-center-right:var(--layout-center-right)] [--gtv-center-top:var(--layout-band-top)]";
2253
+ var noop = () => {
2254
+ };
2379
2255
  function GenreTreeView({
2380
2256
  scope,
2381
2257
  handleGenreCreationAction,
@@ -2429,21 +2305,21 @@ function GenreTreeView({
2429
2305
  placement = "default",
2430
2306
  ...focus
2431
2307
  }) {
2432
- const getShareUrlRef = (0, import_react17.useRef)(getShareUrl);
2308
+ const getShareUrlRef = (0, import_react16.useRef)(getShareUrl);
2433
2309
  getShareUrlRef.current = getShareUrl;
2434
- const [internalArtistUuid, setInternalArtistUuid] = (0, import_react17.useState)(null);
2310
+ const [internalArtistUuid, setInternalArtistUuid] = (0, import_react16.useState)(null);
2435
2311
  const isArtistControlled = controlledArtistUuid !== void 0;
2436
2312
  const artistUuid = isArtistControlled ? controlledArtistUuid : internalArtistUuid;
2437
- const [internalSongUuid, setInternalSongUuid] = (0, import_react17.useState)(null);
2313
+ const [internalSongUuid, setInternalSongUuid] = (0, import_react16.useState)(null);
2438
2314
  const isSongControlled = controlledSongUuid !== void 0;
2439
2315
  const songUuid = isSongControlled ? controlledSongUuid : internalSongUuid;
2440
2316
  const changeSong = (uuid) => {
2441
2317
  if (!isSongControlled) setInternalSongUuid(uuid);
2442
2318
  onSongUuidChange?.(uuid);
2443
2319
  };
2444
- const [internalSizeMetric, setInternalSizeMetric] = (0, import_react17.useState)("songs");
2320
+ const [internalSizeMetric, setInternalSizeMetric] = (0, import_react16.useState)("songs");
2445
2321
  const sizeMetric = controlledSizeMetric ?? internalSizeMetric;
2446
- const deferredSizeMetric = (0, import_react17.useDeferredValue)(sizeMetric);
2322
+ const deferredSizeMetric = (0, import_react16.useDeferredValue)(sizeMetric);
2447
2323
  const changeSizeMetric = (metric) => {
2448
2324
  if (controlledSizeMetric === void 0) setInternalSizeMetric(metric);
2449
2325
  onSizeMetricChange?.(metric);
@@ -2480,7 +2356,7 @@ function GenreTreeView({
2480
2356
  artistUuid
2481
2357
  });
2482
2358
  const restoreRevision = viewportRestore?.revision;
2483
- const [stackedRestoreOwner, setStackedRestoreOwner] = (0, import_react17.useState)({ revision: restoreRevision, rootUuid: selectedGenrePlaylist?.root.uuid });
2359
+ const [stackedRestoreOwner, setStackedRestoreOwner] = (0, import_react16.useState)({ revision: restoreRevision, rootUuid: selectedGenrePlaylist?.root.uuid });
2484
2360
  let restoreRootUuid = stackedRestoreOwner.rootUuid;
2485
2361
  if (stackedRestoreOwner.revision !== restoreRevision) {
2486
2362
  restoreRootUuid = selectedGenrePlaylist?.root.uuid;
@@ -2496,7 +2372,7 @@ function GenreTreeView({
2496
2372
  const canSizeByViews = !isArtistFiltered && hasViewsCount(visibleGenrePlaylists);
2497
2373
  const artistResult = (0, import_genre_tree19.useArtist)(artistUuid, artistSearchBaseUrl ?? getBackendBaseUrl);
2498
2374
  const filterArtistName = artistResult.status === "success" ? artistResult.artist.name : null;
2499
- const filterArtist = (0, import_react17.useMemo)(
2375
+ const filterArtist = (0, import_react16.useMemo)(
2500
2376
  () => artistUuid !== null && filterArtistName !== null ? { uuid: artistUuid, name: filterArtistName } : null,
2501
2377
  [artistUuid, filterArtistName]
2502
2378
  );
@@ -2511,14 +2387,14 @@ function GenreTreeView({
2511
2387
  if (selectedNodeId !== null) clearSelection();
2512
2388
  changeSong(uuid);
2513
2389
  };
2514
- const openSongRef = (0, import_react17.useRef)(openSong);
2390
+ const openSongRef = (0, import_react16.useRef)(openSong);
2515
2391
  openSongRef.current = openSong;
2516
- const resolvedRenderSongLink = (0, import_react17.useCallback)(
2517
- (song) => renderSongLink ? renderSongLink(song) : /* @__PURE__ */ (0, import_jsx_runtime35.jsx)("button", { type: "button", onClick: () => openSongRef.current(song.uuid), className: "hover:underline", children: song.title }),
2392
+ const resolvedRenderSongLink = (0, import_react16.useCallback)(
2393
+ (song) => renderSongLink ? renderSongLink(song) : /* @__PURE__ */ (0, import_jsx_runtime34.jsx)("button", { type: "button", onClick: () => openSongRef.current(song.uuid), className: "hover:underline", children: song.title }),
2518
2394
  [renderSongLink]
2519
2395
  );
2520
- const [expandedSongUuid, setExpandedSongUuid] = (0, import_react17.useState)(null);
2521
- const [expandedArtistUuid, setExpandedArtistUuid] = (0, import_react17.useState)(null);
2396
+ const [expandedSongUuid, setExpandedSongUuid] = (0, import_react16.useState)(null);
2397
+ const [expandedArtistUuid, setExpandedArtistUuid] = (0, import_react16.useState)(null);
2522
2398
  const artistBaseUrl = artistSearchBaseUrl ?? getBackendBaseUrl;
2523
2399
  const songBaseUrl = getBackendBaseUrl;
2524
2400
  const artistPanel = useArtistPanel({
@@ -2526,7 +2402,7 @@ function GenreTreeView({
2526
2402
  scope,
2527
2403
  getBackendBaseUrl: artistBaseUrl,
2528
2404
  getShareUrl: getShareUrlRef.current,
2529
- renderArtistLink: renderArtistLink ?? ((artist) => artist.isSpecialPurpose ? artist.name : /* @__PURE__ */ (0, import_jsx_runtime35.jsx)("button", { type: "button", onClick: () => openArtist(artist.uuid), className: "hover:underline", children: artist.name })),
2405
+ renderArtistLink: renderArtistLink ?? ((artist) => artist.isSpecialPurpose ? artist.name : /* @__PURE__ */ (0, import_jsx_runtime34.jsx)("button", { type: "button", onClick: () => openArtist(artist.uuid), className: "hover:underline", children: artist.name })),
2530
2406
  expanded: controlledArtistExpanded ?? (artistUuid !== null && expandedArtistUuid === artistUuid),
2531
2407
  onExpandedChange: (on) => {
2532
2408
  if (controlledArtistExpanded === void 0) setExpandedArtistUuid(on ? artistUuid : null);
@@ -2535,7 +2411,7 @@ function GenreTreeView({
2535
2411
  renderSongLink: resolvedRenderSongLink,
2536
2412
  onClose: () => changeArtist(null)
2537
2413
  });
2538
- const resolvedRenderArtistLink = renderArtistLink ?? ((artist) => artist.isSpecialPurpose ? artist.name : /* @__PURE__ */ (0, import_jsx_runtime35.jsx)("button", { type: "button", onClick: () => openArtist(artist.uuid), className: "hover:underline", children: artist.name }));
2414
+ const resolvedRenderArtistLink = renderArtistLink ?? ((artist) => artist.isSpecialPurpose ? artist.name : /* @__PURE__ */ (0, import_jsx_runtime34.jsx)("button", { type: "button", onClick: () => openArtist(artist.uuid), className: "hover:underline", children: artist.name }));
2539
2415
  const getGenrePath = (genre) => {
2540
2416
  const path = [];
2541
2417
  let current = visibleGenrePlaylists.find((playlist) => playlist.criteria?.uuid === genre.uuid);
@@ -2567,9 +2443,8 @@ function GenreTreeView({
2567
2443
  const builtInSongPanel = overlayPanel || hasSelectedGenreNode ? void 0 : songPanel;
2568
2444
  const builtInArtistPanel = overlayPanel || hasSelectedGenreNode || builtInSongPanel ? void 0 : artistPanel;
2569
2445
  const builtInPanel = builtInSongPanel ?? builtInArtistPanel;
2570
- const activeOverlayPanel = overlayPanel ?? (builtInPanel && { ...builtInPanel, hideTitle: true, hideClose: true });
2571
- const [uncontrolledCollapsed, setUncontrolledCollapsed] = (0, import_react17.useState)(false);
2572
- const [uncontrolledExpanded, setUncontrolledExpanded] = (0, import_react17.useState)(false);
2446
+ const [uncontrolledCollapsed, setUncontrolledCollapsed] = (0, import_react16.useState)(false);
2447
+ const [uncontrolledExpanded, setUncontrolledExpanded] = (0, import_react16.useState)(false);
2573
2448
  const isCollapsed = collapsed ?? uncontrolledCollapsed;
2574
2449
  const isExpanded = expanded ?? uncontrolledExpanded;
2575
2450
  const changeCollapsed = (next) => {
@@ -2580,55 +2455,56 @@ function GenreTreeView({
2580
2455
  setUncontrolledExpanded(next);
2581
2456
  onExpandedChange?.(next);
2582
2457
  };
2583
- (0, import_react17.useEffect)(() => {
2458
+ (0, import_react16.useEffect)(() => {
2584
2459
  setUncontrolledCollapsed(false);
2585
2460
  }, [selectedNodeId, artistUuid, songUuid]);
2586
- const isDetailHidden = isCollapsed || !hasSelectedGenreNode && !activeOverlayPanel;
2587
- useLeftDetailPanelLayout(!isDetailHidden, placement === "regions");
2588
- const detailOverlay = placement === "regions" && activeOverlayPanel?.onExpandedChange ? activeOverlayPanel : void 0;
2589
- const detailExpanded = detailOverlay ? !!detailOverlay.expanded : isExpanded;
2590
- const onDetailExpandedChange = detailOverlay ? detailOverlay.onExpandedChange : changeExpanded;
2591
- const infoPanelCuration = {
2592
- renderDangerZone,
2593
- expanded: placement === "regions" ? isExpanded : expanded,
2594
- onExpandedChange: placement === "regions" ? changeExpanded : onExpandedChange,
2595
- collapsed: placement === "regions" ? isCollapsed : void 0,
2596
- hideExpandButton: placement === "regions" ? true : void 0,
2597
- overlayPanel: detailOverlay ? { ...detailOverlay, collapsed: isCollapsed, hideExpandButton: true } : activeOverlayPanel
2598
- };
2599
- const [isWheelRevealed, setIsWheelRevealed] = (0, import_react17.useState)(false);
2461
+ const [isWheelRevealed, setIsWheelRevealed] = (0, import_react16.useState)(false);
2600
2462
  const usesHandoff = viewMode === "wheel" || viewMode === "pop-core";
2601
2463
  const isTreeReady = !isLoading && (!usesHandoff || isWheelRevealed);
2602
- const rootRef = (0, import_react17.useRef)(null);
2603
- const handlePrevious = () => {
2604
- rootRef.current?.querySelector(".search-overlay .gtv-info-panel-previous")?.click();
2605
- };
2606
- (0, import_react17.useEffect)(() => {
2607
- if (!isTreeReady || viewMode !== "pop-core" || !hasSelectedGenreNode || !selectedGenrePlaylist) return;
2608
- const root = rootRef.current;
2609
- const centerChip = root?.querySelector(".gtv-wheel-center-node .gtv-wheel-chip");
2610
- if (!centerChip || centerChip.getAttribute("aria-label") !== selectedGenrePlaylist.name) return;
2611
- const panelTitle = root?.querySelector(".gtv-info-panel-title");
2612
- if (panelTitle?.textContent?.trim().startsWith(selectedGenrePlaylist.name)) return;
2613
- centerChip.click();
2614
- }, [hasSelectedGenreNode, isTreeReady, selectedGenrePlaylist, viewMode]);
2464
+ const [resolveStyle, setResolveStyle] = (0, import_react16.useState)(null);
2465
+ const handleNodeStyleResolverChange = (0, import_react16.useCallback)(
2466
+ (resolver) => setResolveStyle(() => resolver),
2467
+ []
2468
+ );
2469
+ const panelRef = (0, import_react16.useRef)(null);
2470
+ const obscuredInset = (0, import_ui15.useObscuredInset)(panelRef);
2471
+ const hostPanel = { obscuredInset, onNodeStyleResolverChange: handleNodeStyleResolverChange };
2472
+ const nodeMetric = isArtistFiltered || !canSizeByViews ? "songs" : deferredSizeMetric;
2473
+ const treeNodes = (0, import_react16.useMemo)(() => toTreeNodes(visibleGenrePlaylists, nodeMetric), [visibleGenrePlaylists, nodeMetric]);
2474
+ const relations = (0, import_genre_tree_view4.useGenreNodeRelations)(
2475
+ treeNodes,
2476
+ !overlayPanel && hasSelectedGenreNode ? selectedNodeId : null,
2477
+ resolveStyle
2478
+ );
2479
+ const [selectionHistory, setSelectionHistory] = (0, import_react16.useState)({
2480
+ current: selectedNodeId,
2481
+ previous: null
2482
+ });
2483
+ if (selectionHistory.current !== selectedNodeId) {
2484
+ setSelectionHistory({
2485
+ current: selectedNodeId,
2486
+ previous: selectionHistory.current ?? selectionHistory.previous
2487
+ });
2488
+ }
2489
+ const previousPlaylist = selectionHistory.previous !== null && selectionHistory.previous !== selectedNodeId ? genrePlaylistByNodeId.get(selectionHistory.previous) : void 0;
2490
+ const handlePrevious = previousPlaylist && visibleGenrePlaylists.includes(previousPlaylist) ? () => handleGenreSearchSelect(previousPlaylist) : void 0;
2615
2491
  const { showPopup, hidePopup } = (0, import_popup5.usePopup)();
2616
- const [searchText, setSearchText] = (0, import_react17.useState)("");
2492
+ const [searchText, setSearchText] = (0, import_react16.useState)("");
2617
2493
  const artistSearchResult = (0, import_genre_tree18.useArtistSearch)(artistBaseUrl, artistSearch ? searchText : "");
2618
2494
  const artistGroup = artistSearch ? {
2619
2495
  key: "artists",
2620
2496
  label: "Artists",
2621
2497
  items: artistSearchResult.artists.map((artist) => ({
2622
2498
  key: artist.uuid,
2623
- content: /* @__PURE__ */ (0, import_jsx_runtime35.jsxs)(import_jsx_runtime35.Fragment, { children: [
2624
- /* @__PURE__ */ (0, import_jsx_runtime35.jsxs)("span", { className: "block truncate font-medium", children: [
2499
+ content: /* @__PURE__ */ (0, import_jsx_runtime34.jsxs)(import_jsx_runtime34.Fragment, { children: [
2500
+ /* @__PURE__ */ (0, import_jsx_runtime34.jsxs)("span", { className: "block truncate font-medium", children: [
2625
2501
  artist.name,
2626
- artist.disambiguation && /* @__PURE__ */ (0, import_jsx_runtime35.jsxs)("span", { className: "font-normal text-[var(--color-neutral-500)]", children: [
2502
+ artist.disambiguation && /* @__PURE__ */ (0, import_jsx_runtime34.jsxs)("span", { className: "font-normal text-[var(--color-neutral-500)]", children: [
2627
2503
  " \xB7 ",
2628
2504
  artist.disambiguation
2629
2505
  ] })
2630
2506
  ] }),
2631
- artist.matchedAlias && /* @__PURE__ */ (0, import_jsx_runtime35.jsxs)("span", { className: "block truncate text-xs text-[var(--color-neutral-500)]", children: [
2507
+ artist.matchedAlias && /* @__PURE__ */ (0, import_jsx_runtime34.jsxs)("span", { className: "block truncate text-xs text-[var(--color-neutral-500)]", children: [
2632
2508
  "also known as ",
2633
2509
  artist.matchedAlias
2634
2510
  ] })
@@ -2647,9 +2523,9 @@ function GenreTreeView({
2647
2523
  label: "Songs",
2648
2524
  items: songSearchResult.songs.map((song) => ({
2649
2525
  key: song.uuid,
2650
- content: /* @__PURE__ */ (0, import_jsx_runtime35.jsxs)("span", { className: (0, import_genre_tree18.isSongSearchResultPlayable)(song) ? void 0 : "opacity-60", children: [
2651
- /* @__PURE__ */ (0, import_jsx_runtime35.jsx)("span", { className: "block truncate font-medium", children: song.title }),
2652
- /* @__PURE__ */ (0, import_jsx_runtime35.jsx)("span", { className: "block truncate text-xs text-[var(--color-neutral-500)]", children: [song.artists.map((artist) => artist.name).join(", "), song.genre?.name ?? "No genre"].filter(Boolean).join(" \xB7 ") })
2526
+ content: /* @__PURE__ */ (0, import_jsx_runtime34.jsxs)("span", { className: (0, import_genre_tree18.isSongSearchResultPlayable)(song) ? void 0 : "opacity-60", children: [
2527
+ /* @__PURE__ */ (0, import_jsx_runtime34.jsx)("span", { className: "block truncate font-medium", children: song.title }),
2528
+ /* @__PURE__ */ (0, import_jsx_runtime34.jsx)("span", { className: "block truncate text-xs text-[var(--color-neutral-500)]", children: [song.artists.map((artist) => artist.name).join(", "), song.genre?.name ?? "No genre"].filter(Boolean).join(" \xB7 ") })
2653
2529
  ] }),
2654
2530
  onSelect: () => {
2655
2531
  openSong(song.uuid);
@@ -2675,36 +2551,49 @@ function GenreTreeView({
2675
2551
  const handleGenreSearchZoomIn = () => {
2676
2552
  if (selectedUuid) focus.onFocusGenreChange?.(selectedUuid, "zoom-in");
2677
2553
  };
2678
- const zoomInRef = (0, import_react17.useRef)(void 0);
2554
+ const zoomInRef = (0, import_react16.useRef)(void 0);
2679
2555
  zoomInRef.current = canZoomIn ? handleGenreSearchZoomIn : void 0;
2680
- const renderInfoPanelBody = (0, import_react17.useCallback)(
2681
- (node, { toolbar, ancestors, family }) => {
2682
- const genrePlaylist = genrePlaylistByNodeId.get(node.id);
2683
- const criteria = genrePlaylist?.criteria;
2684
- if (!genrePlaylist || !criteria) return { primary: /* @__PURE__ */ (0, import_jsx_runtime35.jsxs)(import_jsx_runtime35.Fragment, { children: [
2685
- toolbar,
2686
- family
2687
- ] }), aside: ancestors };
2688
- const genreUuid = criteria.uuid;
2689
- const parentPlaylist = visibleGenrePlaylists.find(({ uuid }) => uuid === genrePlaylist.parent?.uuid);
2690
- const canToggleSide = !readOnly && parentPlaylist !== void 0 && parentPlaylist.parent === null;
2691
- const side = criteria.side ?? "core";
2692
- const shareTarget = {
2693
- kind: "genre",
2694
- genrePlaylistUuid: genrePlaylist.uuid,
2695
- name: criteria.name,
2696
- scope,
2697
- artistUuid
2698
- };
2699
- const shareUrl = getShareUrlRef.current?.(shareTarget) ?? void 0;
2700
- const popSibling = canToggleSide && side === "core" ? visibleGenrePlaylists.find(
2701
- (sibling) => sibling.parent?.uuid === parentPlaylist.uuid && sibling.uuid !== genrePlaylist.uuid && sibling.criteria?.side === "pop"
2702
- ) : void 0;
2703
- return {
2704
- primary: /* @__PURE__ */ (0, import_jsx_runtime35.jsx)(
2556
+ const selectGenreFromPanel = (nodeId) => {
2557
+ const genrePlaylist = genrePlaylistByNodeId.get(nodeId);
2558
+ if (genrePlaylist) handleGenreSearchSelect(genrePlaylist);
2559
+ };
2560
+ const renderGenreBody = (relation) => {
2561
+ const { node } = relation;
2562
+ const family = /* @__PURE__ */ (0, import_jsx_runtime34.jsx)(
2563
+ import_genre_tree_view4.GenreNodeFamily,
2564
+ {
2565
+ node,
2566
+ fill: relation.fill,
2567
+ textColor: relation.textColor,
2568
+ parentNode: relation.parentNode,
2569
+ childNodes: relation.childNodes,
2570
+ onSelectNode: selectGenreFromPanel
2571
+ }
2572
+ );
2573
+ const ancestors = /* @__PURE__ */ (0, import_jsx_runtime34.jsx)(import_genre_tree_view4.GenreNodeAncestors, { ancestorNodes: relation.ancestorNodes, onSelectNode: selectGenreFromPanel });
2574
+ const genrePlaylist = genrePlaylistByNodeId.get(node.id);
2575
+ const criteria = genrePlaylist?.criteria;
2576
+ if (!genrePlaylist || !criteria) return { primary: family, aside: ancestors };
2577
+ const genreUuid = criteria.uuid;
2578
+ const parentPlaylist = visibleGenrePlaylists.find(({ uuid }) => uuid === genrePlaylist.parent?.uuid);
2579
+ const canToggleSide = !readOnly && parentPlaylist !== void 0 && parentPlaylist.parent === null;
2580
+ const side = criteria.side ?? "core";
2581
+ const shareTarget = {
2582
+ kind: "genre",
2583
+ genrePlaylistUuid: genrePlaylist.uuid,
2584
+ name: criteria.name,
2585
+ scope,
2586
+ artistUuid
2587
+ };
2588
+ const shareUrl = getShareUrlRef.current?.(shareTarget) ?? void 0;
2589
+ const popSibling = canToggleSide && side === "core" ? visibleGenrePlaylists.find(
2590
+ (sibling) => sibling.parent?.uuid === parentPlaylist.uuid && sibling.uuid !== genrePlaylist.uuid && sibling.criteria?.side === "pop"
2591
+ ) : void 0;
2592
+ return {
2593
+ primary: /* @__PURE__ */ (0, import_jsx_runtime34.jsxs)(import_jsx_runtime34.Fragment, { children: [
2594
+ /* @__PURE__ */ (0, import_jsx_runtime34.jsx)(
2705
2595
  GenreInfoPanelBody,
2706
2596
  {
2707
- toolbar,
2708
2597
  family,
2709
2598
  genreUuid,
2710
2599
  genrePlaylist,
@@ -2720,7 +2609,7 @@ function GenreTreeView({
2720
2609
  onAddChild: readOnly ? void 0 : () => handleGenreCreationAction(criteria),
2721
2610
  onRename: readOnly ? void 0 : () => handleGenreRenameAction(criteria),
2722
2611
  onChangeParent: readOnly ? void 0 : () => showPopup(
2723
- /* @__PURE__ */ (0, import_jsx_runtime35.jsx)(
2612
+ /* @__PURE__ */ (0, import_jsx_runtime34.jsx)(
2724
2613
  ChangeParentPopup,
2725
2614
  {
2726
2615
  genrePlaylist: { ...genrePlaylist, criteria },
@@ -2751,49 +2640,65 @@ function GenreTreeView({
2751
2640
  }
2752
2641
  }
2753
2642
  ),
2754
- aside: ancestors
2755
- };
2756
- },
2757
- [
2758
- genrePlaylistByNodeId,
2759
- additionalActions,
2760
- readOnly,
2761
- handleGenreCreationAction,
2762
- handleGenreRenameAction,
2763
- showPopup,
2764
- hidePopup,
2765
- onParentChanged,
2766
- visibleGenrePlaylists,
2767
- updateGenre,
2768
- playback,
2769
- filterArtist,
2770
- scope,
2771
- getBackendBaseUrl,
2772
- criteriaOverviewSchema,
2773
- renderGenreDetailExtras,
2774
- renderArtistLink,
2775
- resolvedRenderSongLink,
2776
- renderSummary,
2777
- renderEssentialTracks,
2778
- renderSongLeading,
2779
- renderSongActions,
2780
- renderTracksToolbar
2781
- ]
2782
- );
2643
+ renderDangerZone?.(node)
2644
+ ] }),
2645
+ aside: ancestors
2646
+ };
2647
+ };
2783
2648
  const isRotationControlled = controlledAllowWheelRotation !== void 0;
2784
2649
  const allowWheelRotation = controlledAllowWheelRotation ?? internalAllowWheelRotation;
2785
- const detailTab = placement === "regions" && (hasSelectedGenreNode || detailOverlay) ? /* @__PURE__ */ (0, import_jsx_runtime35.jsx)(
2786
- PanelEdgeTab,
2650
+ let activePanel = null;
2651
+ if (overlayPanel) {
2652
+ activePanel = {
2653
+ title: overlayPanel.title,
2654
+ content: overlayPanel.content,
2655
+ aside: overlayPanel.extraContent,
2656
+ expanded: overlayPanel.expanded ?? false,
2657
+ onExpandedChange: overlayPanel.onExpandedChange,
2658
+ onClose: overlayPanel.onClose,
2659
+ compact: false
2660
+ };
2661
+ } else if (relations) {
2662
+ const { primary, aside } = renderGenreBody(relations);
2663
+ activePanel = {
2664
+ title: relations.node.name,
2665
+ content: primary,
2666
+ aside,
2667
+ expanded: isExpanded,
2668
+ onExpandedChange: changeExpanded,
2669
+ onClose: clearSelection,
2670
+ compact: true,
2671
+ headerAccent: { background: relations.fill, color: relations.textColor }
2672
+ };
2673
+ } else if (builtInPanel) {
2674
+ activePanel = {
2675
+ title: builtInPanel.title,
2676
+ content: builtInPanel.content,
2677
+ aside: builtInPanel.extraContent,
2678
+ expanded: builtInPanel.expanded,
2679
+ onExpandedChange: builtInPanel.onExpandedChange,
2680
+ onClose: builtInPanel.onClose,
2681
+ compact: true
2682
+ };
2683
+ }
2684
+ const isPanelShown = isTreeReady && activePanel !== null;
2685
+ const detailTab = placement === "regions" && activePanel?.onExpandedChange ? {
2686
+ edge: activePanel.expanded ? { label: "Shrink panel", onClick: () => activePanel.onExpandedChange?.(false) } : isCollapsed ? null : { label: "Hide panel", onClick: () => changeCollapsed(true) },
2687
+ center: activePanel.expanded ? null : isCollapsed ? { label: "Show panel", onClick: () => changeCollapsed(false) } : activePanel.onExpandedChange ? { label: "Extend panel", onClick: () => activePanel.onExpandedChange?.(true) } : null
2688
+ } : void 0;
2689
+ const expandButton = placement === "default" && activePanel?.onExpandedChange ? /* @__PURE__ */ (0, import_jsx_runtime34.jsx)(
2690
+ import_ui15.IconButton,
2787
2691
  {
2788
- side: "left",
2789
- edge: detailExpanded ? { label: "Shrink panel", onClick: () => onDetailExpandedChange?.(false) } : isCollapsed ? null : { label: "Hide panel", onClick: () => changeCollapsed(true) },
2790
- center: detailExpanded ? null : isCollapsed ? { label: "Show panel", onClick: () => changeCollapsed(false) } : onDetailExpandedChange ? { label: "Extend panel", onClick: () => onDetailExpandedChange(true) } : null
2692
+ "aria-label": activePanel.expanded ? "Shrink panel" : "Extend panel",
2693
+ style: activePanel.headerAccent && { color: "inherit" },
2694
+ onClick: () => activePanel.onExpandedChange?.(!activePanel.expanded),
2695
+ children: activePanel.expanded ? /* @__PURE__ */ (0, import_jsx_runtime34.jsx)(import_lucide_react12.Minimize2, { "aria-hidden": "true", className: "h-4 w-4" }) : /* @__PURE__ */ (0, import_jsx_runtime34.jsx)(import_lucide_react12.Maximize2, { "aria-hidden": "true", className: "h-4 w-4" })
2791
2696
  }
2792
2697
  ) : null;
2793
2698
  const renderArtistFilter = () => {
2794
2699
  if (isLoading || !filterArtist) return null;
2795
- return /* @__PURE__ */ (0, import_jsx_runtime35.jsxs)("span", { className: "inline-flex items-center overflow-hidden rounded-full bg-[var(--color-neutral-100)] text-sm", children: [
2796
- /* @__PURE__ */ (0, import_jsx_runtime35.jsx)(
2700
+ return /* @__PURE__ */ (0, import_jsx_runtime34.jsxs)("span", { className: "inline-flex items-center overflow-hidden rounded-full bg-[var(--color-neutral-100)] text-sm", children: [
2701
+ /* @__PURE__ */ (0, import_jsx_runtime34.jsx)(
2797
2702
  "button",
2798
2703
  {
2799
2704
  type: "button",
@@ -2805,7 +2710,7 @@ function GenreTreeView({
2805
2710
  children: filterArtist.name
2806
2711
  }
2807
2712
  ),
2808
- /* @__PURE__ */ (0, import_jsx_runtime35.jsx)(
2713
+ /* @__PURE__ */ (0, import_jsx_runtime34.jsx)(
2809
2714
  "button",
2810
2715
  {
2811
2716
  type: "button",
@@ -2817,22 +2722,22 @@ function GenreTreeView({
2817
2722
  )
2818
2723
  ] });
2819
2724
  };
2820
- const actions = /* @__PURE__ */ (0, import_jsx_runtime35.jsxs)(import_jsx_runtime35.Fragment, { children: [
2821
- !isLoading && /* @__PURE__ */ (0, import_jsx_runtime35.jsx)(
2725
+ const actions = /* @__PURE__ */ (0, import_jsx_runtime34.jsxs)(import_jsx_runtime34.Fragment, { children: [
2726
+ !isLoading && /* @__PURE__ */ (0, import_jsx_runtime34.jsx)(
2822
2727
  GenreSearch,
2823
2728
  {
2824
2729
  genrePlaylists: visibleGenrePlaylists,
2825
2730
  onSelect: handleGenreSearchSelect,
2826
- selectedName: selectedGenrePlaylist?.name ?? builtInPanel?.title ?? null,
2731
+ selectedName: selectedGenrePlaylist?.name ?? (typeof builtInPanel?.title === "string" ? builtInPanel.title : null),
2827
2732
  onClear: builtInSongPanel ? () => changeSong(null) : builtInArtistPanel ? () => changeArtist(null) : handleGenreSearchClear,
2828
2733
  onPrevious: handlePrevious,
2829
2734
  onQueryChange: artistSearch || songSearch ? setSearchText : void 0,
2830
2735
  extraGroups
2831
2736
  }
2832
2737
  ),
2833
- !isLoading && filterArtist && /* @__PURE__ */ (0, import_jsx_runtime35.jsx)("div", { className: "hidden w-full justify-center max-md:flex", children: renderArtistFilter() }),
2834
- /* @__PURE__ */ (0, import_jsx_runtime35.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: [
2835
- !isLoading && !isControlled && /* @__PURE__ */ (0, import_jsx_runtime35.jsx)(
2738
+ !isLoading && filterArtist && /* @__PURE__ */ (0, import_jsx_runtime34.jsx)("div", { className: "hidden w-full justify-center max-md:flex", children: renderArtistFilter() }),
2739
+ /* @__PURE__ */ (0, import_jsx_runtime34.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: [
2740
+ !isLoading && !isControlled && /* @__PURE__ */ (0, import_jsx_runtime34.jsx)(
2836
2741
  import_ui15.SegmentedControl,
2837
2742
  {
2838
2743
  "aria-label": "Tree view mode",
@@ -2846,13 +2751,13 @@ function GenreTreeView({
2846
2751
  ]
2847
2752
  }
2848
2753
  ),
2849
- !isLoading && showDisplayOptions && /* @__PURE__ */ (0, import_jsx_runtime35.jsx)(
2754
+ !isLoading && showDisplayOptions && /* @__PURE__ */ (0, import_jsx_runtime34.jsx)(
2850
2755
  "div",
2851
2756
  {
2852
2757
  className: "flex items-center gap-1",
2853
2758
  role: "group",
2854
2759
  "aria-label": "Tree display options",
2855
- children: !isRotationControlled && viewMode !== "stacked" && viewMode !== "outline" && /* @__PURE__ */ (0, import_jsx_runtime35.jsx)(
2760
+ children: !isRotationControlled && viewMode !== "stacked" && viewMode !== "outline" && /* @__PURE__ */ (0, import_jsx_runtime34.jsx)(
2856
2761
  import_ui15.Button,
2857
2762
  {
2858
2763
  variant: allowWheelRotation ? "default" : "outline",
@@ -2863,52 +2768,44 @@ function GenreTreeView({
2863
2768
  )
2864
2769
  }
2865
2770
  ),
2866
- !isLoading && !readOnly && showAddRoot && /* @__PURE__ */ (0, import_jsx_runtime35.jsx)(
2771
+ !isLoading && !readOnly && showAddRoot && /* @__PURE__ */ (0, import_jsx_runtime34.jsx)(
2867
2772
  import_ui15.IconTextButton,
2868
2773
  {
2869
- icon: import_lucide_react12.Plus,
2774
+ icon: import_lucide_react11.Plus,
2870
2775
  text: "Add root",
2871
2776
  onClick: () => handleGenreCreationAction(null)
2872
2777
  }
2873
2778
  )
2874
2779
  ] })
2875
2780
  ] });
2876
- return /* @__PURE__ */ (0, import_jsx_runtime35.jsxs)(
2781
+ return /* @__PURE__ */ (0, import_jsx_runtime34.jsxs)(
2877
2782
  "div",
2878
2783
  {
2879
- ref: rootRef,
2784
+ "data-left-panel": isPanelShown && !isCollapsed ? void 0 : "hidden",
2880
2785
  className: ["genre-tree-view relative flex flex-col h-full", placement === "regions" && REGIONS_CLASS, className2].filter(Boolean).join(" "),
2881
2786
  children: [
2882
- /* @__PURE__ */ (0, import_jsx_runtime35.jsx)("style", { children: PREVIOUS_BUTTON_CSS }),
2883
- /* @__PURE__ */ (0, import_jsx_runtime35.jsx)(
2787
+ /* @__PURE__ */ (0, import_jsx_runtime34.jsx)(
2884
2788
  "div",
2885
2789
  {
2886
2790
  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"}`,
2887
2791
  children: actions
2888
2792
  }
2889
2793
  ),
2890
- !isLoading && filterArtist && /* @__PURE__ */ (0, import_jsx_runtime35.jsx)(
2794
+ !isLoading && filterArtist && /* @__PURE__ */ (0, import_jsx_runtime34.jsx)(
2891
2795
  "div",
2892
2796
  {
2893
2797
  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"}`,
2894
2798
  children: renderArtistFilter()
2895
2799
  }
2896
2800
  ),
2897
- detailTab && /* @__PURE__ */ (0, import_jsx_runtime35.jsx)(
2898
- "div",
2899
- {
2900
- className: `absolute top-1/2 z-30 -translate-y-1/2 max-md:hidden transition-[opacity,visibility] duration-150 ease-out ${isTreeReady ? "opacity-100" : "pointer-events-none invisible opacity-0"} ${detailExpanded ? "right-[calc(var(--layout-center-right)-var(--layout-edge-tab-w))]" : isCollapsed ? "left-0" : "left-[calc(var(--layout-gutter)+var(--layout-left-w))]"}`,
2901
- children: detailTab
2902
- }
2903
- ),
2904
- /* @__PURE__ */ (0, import_jsx_runtime35.jsx)("div", { className: "content-container flex-1 min-h-0 flex flex-row gap-4", children: /* @__PURE__ */ (0, import_jsx_runtime35.jsxs)("div", { className: "tree-view-container relative flex-1 min-w-0 flex flex-col h-full", children: [
2905
- !isLoading && viewMode === "outline" && floatingControlsLeading && /* @__PURE__ */ (0, import_jsx_runtime35.jsx)("div", { className: "absolute bottom-3 right-3 z-10", children: floatingControlsLeading }),
2906
- isLoading ? /* @__PURE__ */ (0, import_jsx_runtime35.jsx)(import_genre_tree_view4.GenreTreeViewSkeleton, { viewMode }) : viewMode === "wheel" ? (
2801
+ /* @__PURE__ */ (0, import_jsx_runtime34.jsx)("div", { className: "content-container flex-1 min-h-0 flex flex-row gap-4", children: /* @__PURE__ */ (0, import_jsx_runtime34.jsxs)("div", { className: "tree-view-container relative flex-1 min-w-0 flex flex-col h-full", children: [
2802
+ !isLoading && viewMode === "outline" && floatingControlsLeading && /* @__PURE__ */ (0, import_jsx_runtime34.jsx)("div", { className: "absolute bottom-3 right-3 z-10", children: floatingControlsLeading }),
2803
+ isLoading ? /* @__PURE__ */ (0, import_jsx_runtime34.jsx)(import_genre_tree_view4.GenreTreeViewSkeleton, { viewMode }) : viewMode === "wheel" ? (
2907
2804
  // The info panel's top edge aligns with the tree's; the search bar floats `--layout-panel-inset` below it,
2908
2805
  // and the header inset (the bar's height plus that inset) extends the header's fill behind the bar,
2909
2806
  // Google Maps style; the 8px bottom padding mirrors the inset above the bar, and the hidden title's height is zeroed.
2910
2807
  // The panel's own Zoom in button is hidden: the search bar carries it.
2911
- /* @__PURE__ */ (0, import_jsx_runtime35.jsx)("div", { className: "tree-container search-overlay relative flex-1 w-full min-h-0 [--gtv-info-panel-header-inset:calc(var(--layout-search-h,2.5rem)+var(--layout-panel-inset,0.5rem))] [&_.gtv-info-panel-header]:pb-2 [&_.gtv-info-panel-title--hidden]:h-0 [&_.gtv-toolbar]:hidden", children: /* @__PURE__ */ (0, import_jsx_runtime35.jsx)(GenreTreeWheelHandoff, { skeleton: /* @__PURE__ */ (0, import_jsx_runtime35.jsx)(import_genre_tree_view4.GenreTreeWheelSkeleton, {}), onRevealedChange: setIsWheelRevealed, children: /* @__PURE__ */ (0, import_jsx_runtime35.jsx)(
2808
+ /* @__PURE__ */ (0, import_jsx_runtime34.jsx)("div", { className: "tree-container search-overlay relative flex-1 w-full min-h-0 ", children: /* @__PURE__ */ (0, import_jsx_runtime34.jsx)(GenreTreeWheelHandoff, { skeleton: /* @__PURE__ */ (0, import_jsx_runtime34.jsx)(import_genre_tree_view4.GenreTreeWheelSkeleton, {}), onRevealedChange: setIsWheelRevealed, children: /* @__PURE__ */ (0, import_jsx_runtime34.jsx)(
2912
2809
  GenrePlaylistTreeWheel,
2913
2810
  {
2914
2811
  scope,
@@ -2917,29 +2814,26 @@ function GenreTreeView({
2917
2814
  handleGenreRenameAction,
2918
2815
  additionalActions,
2919
2816
  onSelectedNodeChange: handleSelectedNodeChange,
2920
- hideInfoPanelClose: true,
2921
- hideInfoPanelTitle: true,
2922
2817
  onNodeHover: handleNodeHover,
2923
- renderInfoPanelBody,
2818
+ ...hostPanel,
2924
2819
  playback,
2925
2820
  selectedNodeId,
2926
2821
  readOnly,
2927
2822
  allowWheelRotation,
2928
2823
  sizeMetric: isArtistFiltered || !canSizeByViews ? "songs" : deferredSizeMetric,
2929
2824
  busy,
2930
- floatingControlsLeading: floatingControlsLeading || canSizeByViews ? /* @__PURE__ */ (0, import_jsx_runtime35.jsxs)("div", { className: "flex items-center gap-2", children: [
2825
+ floatingControlsLeading: floatingControlsLeading || canSizeByViews ? /* @__PURE__ */ (0, import_jsx_runtime34.jsxs)("div", { className: "flex items-center gap-2", children: [
2931
2826
  floatingControlsLeading,
2932
- canSizeByViews && /* @__PURE__ */ (0, import_jsx_runtime35.jsx)(SizeMetricToggle, { value: sizeMetric, onChange: changeSizeMetric })
2827
+ canSizeByViews && /* @__PURE__ */ (0, import_jsx_runtime34.jsx)(SizeMetricToggle, { value: sizeMetric, onChange: changeSizeMetric })
2933
2828
  ] }) : void 0,
2934
2829
  defaultViewport,
2935
2830
  viewportRestore,
2936
- onViewportChange,
2937
- ...infoPanelCuration
2831
+ onViewportChange
2938
2832
  }
2939
2833
  ) }) })
2940
2834
  ) : viewMode === "pop-core" ? (
2941
2835
  // Same as the wheel: the header inset extends its fill behind the floating search bar, Google Maps style.
2942
- /* @__PURE__ */ (0, import_jsx_runtime35.jsx)("div", { className: "tree-container search-overlay relative flex-1 w-full min-h-0 [--gtv-info-panel-header-inset:calc(var(--layout-search-h,2.5rem)+var(--layout-panel-inset,0.5rem))] [&_.gtv-info-panel-header]:pb-2 [&_.gtv-info-panel-title--hidden]:h-0 [&_.gtv-toolbar]:hidden [&_.gtv-info-panel-zoom-in]:hidden", children: /* @__PURE__ */ (0, import_jsx_runtime35.jsx)(GenreTreeWheelHandoff, { skeleton: /* @__PURE__ */ (0, import_jsx_runtime35.jsx)(import_genre_tree_view4.GenreTreeWheelSkeleton, {}), onRevealedChange: setIsWheelRevealed, children: /* @__PURE__ */ (0, import_jsx_runtime35.jsx)(
2836
+ /* @__PURE__ */ (0, import_jsx_runtime34.jsx)("div", { className: "tree-container search-overlay relative flex-1 w-full min-h-0 ", children: /* @__PURE__ */ (0, import_jsx_runtime34.jsx)(GenreTreeWheelHandoff, { skeleton: /* @__PURE__ */ (0, import_jsx_runtime34.jsx)(import_genre_tree_view4.GenreTreeWheelSkeleton, {}), onRevealedChange: setIsWheelRevealed, children: /* @__PURE__ */ (0, import_jsx_runtime34.jsx)(
2943
2837
  GenrePlaylistTreeWheelRadialPopCore,
2944
2838
  {
2945
2839
  scope,
@@ -2948,28 +2842,25 @@ function GenreTreeView({
2948
2842
  handleGenreRenameAction,
2949
2843
  additionalActions,
2950
2844
  onSelectedNodeChange: handleSelectedNodeChange,
2951
- hideInfoPanelClose: true,
2952
- hideInfoPanelTitle: true,
2953
2845
  onNodeHover: handleNodeHover,
2954
- renderInfoPanelBody,
2846
+ ...hostPanel,
2955
2847
  playback,
2956
2848
  selectedNodeId,
2957
2849
  readOnly,
2958
2850
  allowWheelRotation,
2959
2851
  sizeMetric: isArtistFiltered || !canSizeByViews ? "songs" : deferredSizeMetric,
2960
2852
  busy,
2961
- floatingControlsLeading: floatingControlsLeading || canSizeByViews ? /* @__PURE__ */ (0, import_jsx_runtime35.jsxs)("div", { className: "flex items-center gap-2", children: [
2853
+ floatingControlsLeading: floatingControlsLeading || canSizeByViews ? /* @__PURE__ */ (0, import_jsx_runtime34.jsxs)("div", { className: "flex items-center gap-2", children: [
2962
2854
  floatingControlsLeading,
2963
- canSizeByViews && /* @__PURE__ */ (0, import_jsx_runtime35.jsx)(SizeMetricToggle, { value: sizeMetric, onChange: changeSizeMetric })
2855
+ canSizeByViews && /* @__PURE__ */ (0, import_jsx_runtime34.jsx)(SizeMetricToggle, { value: sizeMetric, onChange: changeSizeMetric })
2964
2856
  ] }) : void 0,
2965
2857
  defaultViewport,
2966
2858
  viewportRestore,
2967
2859
  onViewportChange,
2968
- ...infoPanelCuration,
2969
2860
  ...focus
2970
2861
  }
2971
2862
  ) }) })
2972
- ) : viewMode === "outline" ? /* @__PURE__ */ (0, import_jsx_runtime35.jsx)("div", { className: "tree-container flex-1 min-h-0 w-full overflow-y-auto relative [--gtv-info-panel-header-inset:calc(var(--layout-search-h,2.5rem)+var(--layout-panel-inset,0.5rem)+0.75rem)] [&_.gtv-toolbar]:hidden", children: /* @__PURE__ */ (0, import_jsx_runtime35.jsx)(
2863
+ ) : viewMode === "outline" ? /* @__PURE__ */ (0, import_jsx_runtime34.jsx)("div", { className: "tree-container flex-1 min-h-0 w-full overflow-y-auto relative", children: /* @__PURE__ */ (0, import_jsx_runtime34.jsx)(
2973
2864
  GenrePlaylistTreeWheelRadialPopCore,
2974
2865
  {
2975
2866
  outline: true,
@@ -2979,22 +2870,19 @@ function GenreTreeView({
2979
2870
  handleGenreRenameAction,
2980
2871
  additionalActions,
2981
2872
  onSelectedNodeChange: handleSelectedNodeChange,
2982
- hideInfoPanelClose: true,
2983
- hideInfoPanelTitle: true,
2984
2873
  onNodeHover: handleNodeHover,
2985
- renderInfoPanelBody,
2874
+ ...hostPanel,
2986
2875
  playback,
2987
2876
  selectedNodeId,
2988
- readOnly,
2989
- ...infoPanelCuration
2877
+ readOnly
2990
2878
  }
2991
- ) }) : /* @__PURE__ */ (0, import_jsx_runtime35.jsx)("div", { className: "tree-container relative flex flex-col gap-4 w-full pt-14 overflow-x-auto overflow-y-auto text-[var(--color-text)] [&_.gtv-toolbar]:hidden", children: Object.entries(groupedGenrePlaylistsByRoot).map(
2992
- ([uuid, genrePlaylistTreePerRoot], rootIndex) => {
2993
- return /* @__PURE__ */ (0, import_jsx_runtime35.jsx)(
2879
+ ) }) : /* @__PURE__ */ (0, import_jsx_runtime34.jsx)("div", { className: "tree-container relative flex flex-col gap-4 w-full pt-14 overflow-x-auto overflow-y-auto text-[var(--color-text)]", children: Object.entries(groupedGenrePlaylistsByRoot).map(
2880
+ ([uuid, genrePlaylistTreePerRoot]) => {
2881
+ return /* @__PURE__ */ (0, import_jsx_runtime34.jsx)(
2994
2882
  "div",
2995
2883
  {
2996
2884
  className: "tree-per-root-container relative shrink-0 h-[28rem] mt-2 mr-16 p-2 bg-[var(--color-neutral-100)] rounded-lg",
2997
- children: /* @__PURE__ */ (0, import_jsx_runtime35.jsx)("div", { className: "graph-container relative z-10 h-full", children: /* @__PURE__ */ (0, import_jsx_runtime35.jsx)(
2885
+ children: /* @__PURE__ */ (0, import_jsx_runtime34.jsx)("div", { className: "graph-container relative z-10 h-full", children: /* @__PURE__ */ (0, import_jsx_runtime34.jsx)(
2998
2886
  GenrePlaylistTreePerRoot,
2999
2887
  {
3000
2888
  scope,
@@ -3004,26 +2892,45 @@ function GenreTreeView({
3004
2892
  handleGenreRenameAction,
3005
2893
  additionalActions,
3006
2894
  onSelectedNodeChange: handleSelectedNodeChange,
3007
- hideInfoPanelClose: true,
3008
- hideInfoPanelTitle: true,
3009
2895
  onNodeHover: handleNodeHover,
3010
- renderInfoPanelBody,
2896
+ ...hostPanel,
3011
2897
  playback,
3012
2898
  selectedNodeId: uuid === selectedGenrePlaylist?.root.uuid ? selectedNodeId : null,
3013
2899
  readOnly,
3014
2900
  wheelZoom: "modifier",
3015
2901
  defaultViewport: uuid === selectedGenrePlaylist?.root.uuid ? defaultViewport : void 0,
3016
2902
  viewportRestore: uuid === selectedGenrePlaylist?.root.uuid && uuid === restoreRootUuid ? viewportRestore : void 0,
3017
- onViewportChange: uuid === selectedGenrePlaylist?.root.uuid ? onViewportChange : void 0,
3018
- ...infoPanelCuration,
3019
- overlayPanel: rootIndex === 0 ? activeOverlayPanel : void 0
2903
+ onViewportChange: uuid === selectedGenrePlaylist?.root.uuid ? onViewportChange : void 0
3020
2904
  }
3021
2905
  ) })
3022
2906
  },
3023
2907
  uuid
3024
2908
  );
3025
2909
  }
3026
- ) })
2910
+ ) }),
2911
+ /* @__PURE__ */ (0, import_jsx_runtime34.jsx)(
2912
+ import_ui15.DetailPanel,
2913
+ {
2914
+ ref: panelRef,
2915
+ side: "left",
2916
+ title: activePanel?.title ?? "",
2917
+ hideTitle: activePanel?.compact,
2918
+ hideClose: activePanel?.compact,
2919
+ headerAccent: activePanel?.headerAccent,
2920
+ headerActions: expandButton,
2921
+ onClose: activePanel?.onClose ?? noop,
2922
+ collapsed: isCollapsed || !isTreeReady || !activePanel,
2923
+ expanded: activePanel?.expanded,
2924
+ onExpandedChange: activePanel?.onExpandedChange,
2925
+ aside: activePanel?.aside,
2926
+ edgeTab: detailTab,
2927
+ headerClassName: activePanel?.compact ? "pt-[calc(var(--layout-search-h)+var(--layout-panel-inset)+0.5rem)]" : void 0,
2928
+ children: activePanel?.aside && !activePanel.expanded ? /* @__PURE__ */ (0, import_jsx_runtime34.jsxs)(import_jsx_runtime34.Fragment, { children: [
2929
+ activePanel.content,
2930
+ activePanel.aside
2931
+ ] }) : activePanel?.content
2932
+ }
2933
+ )
3027
2934
  ] }) })
3028
2935
  ]
3029
2936
  }
@@ -3031,12 +2938,12 @@ function GenreTreeView({
3031
2938
  }
3032
2939
 
3033
2940
  // src/genre-tree/GenreDetailPanel.tsx
3034
- var import_react18 = require("react");
2941
+ var import_react17 = require("react");
3035
2942
  var import_genre_tree_view5 = require("@behindthemusictree/genre-tree-view");
3036
2943
  var import_ui16 = require("@behindthemusictree/ui");
2944
+ var import_lucide_react13 = require("lucide-react");
3037
2945
  var import_genre_tree20 = require("@behindthemusictree/app-kit/genre-tree");
3038
- var import_jsx_runtime36 = require("react/jsx-runtime");
3039
- var ITEM_COUNT_LABEL = "Songs";
2946
+ var import_jsx_runtime35 = require("react/jsx-runtime");
3040
2947
  var RELATIVE_ITEM_COUNT = 1;
3041
2948
  function toNode(genre, parentId) {
3042
2949
  return {
@@ -3060,11 +2967,12 @@ function GenreDetailPanel({
3060
2967
  collapsed,
3061
2968
  expanded,
3062
2969
  onExpandedChange,
2970
+ edgeTab,
3063
2971
  extras
3064
2972
  }) {
3065
2973
  const { data: detail, isPending, isError } = (0, import_genre_tree20.useFetchGenreDetail)(genreUuid, scope, getBackendBaseUrl);
3066
2974
  const playback = useGenrePlaylistPlayback({ scope, genrePlaylists: [] });
3067
- const view = (0, import_react18.useMemo)(() => {
2975
+ const view = (0, import_react17.useMemo)(() => {
3068
2976
  if (!detail) return null;
3069
2977
  const rootColor = (0, import_genre_tree_view5.getGenreTreeColor)(detail.root.wikidataId);
3070
2978
  const relative = (genre, parentId) => ({
@@ -3072,7 +2980,7 @@ function GenreDetailPanel({
3072
2980
  fill: (0, import_genre_tree_view5.tintSurface)(rootColor),
3073
2981
  textColor: import_genre_tree_view5.TEXT_COLOR
3074
2982
  });
3075
- const ancestors = [...detail.ascendants].sort((a, b) => b.degree - a.degree);
2983
+ const ancestors2 = [...detail.ascendants].sort((a, b) => b.degree - a.degree);
3076
2984
  return {
3077
2985
  node: {
3078
2986
  id: detail.uuid,
@@ -3084,57 +2992,72 @@ function GenreDetailPanel({
3084
2992
  fill: rootColor,
3085
2993
  parentNode: detail.parent ? relative(detail.parent, null) : null,
3086
2994
  childNodes: detail.children.map((child) => relative(child, detail.uuid)),
3087
- ancestorNodes: ancestors.map(
3088
- ({ ascendant }, index) => relative(ascendant, index === 0 ? null : ancestors[index - 1].ascendant.uuid)
2995
+ ancestorNodes: ancestors2.map(
2996
+ ({ ascendant }, index) => relative(ascendant, index === 0 ? null : ancestors2[index - 1].ascendant.uuid)
3089
2997
  )
3090
2998
  };
3091
2999
  }, [detail]);
3092
3000
  if (isError) {
3093
- return /* @__PURE__ */ (0, import_jsx_runtime36.jsx)("div", { role: "alert", className: "p-4 text-sm text-[var(--color-text)]", children: "This genre could not be loaded." });
3001
+ return /* @__PURE__ */ (0, import_jsx_runtime35.jsx)("div", { role: "alert", className: "p-4 text-sm text-[var(--color-text)]", children: "This genre could not be loaded." });
3094
3002
  }
3095
3003
  if (isPending || !detail || !view) {
3096
- return /* @__PURE__ */ (0, import_jsx_runtime36.jsxs)("div", { className: "flex flex-col gap-3 p-4", "aria-busy": "true", "data-testid": "genre-detail-panel-skeleton", children: [
3097
- /* @__PURE__ */ (0, import_jsx_runtime36.jsx)(import_ui16.Skeleton, { className: "h-6 w-2/3" }),
3098
- /* @__PURE__ */ (0, import_jsx_runtime36.jsx)(import_ui16.Skeleton, { className: "h-4 w-full" }),
3099
- /* @__PURE__ */ (0, import_jsx_runtime36.jsx)(import_ui16.Skeleton, { className: "h-4 w-3/4" })
3004
+ return /* @__PURE__ */ (0, import_jsx_runtime35.jsxs)("div", { className: "flex flex-col gap-3 p-4", "aria-busy": "true", "data-testid": "genre-detail-panel-skeleton", children: [
3005
+ /* @__PURE__ */ (0, import_jsx_runtime35.jsx)(import_ui16.Skeleton, { className: "h-6 w-2/3" }),
3006
+ /* @__PURE__ */ (0, import_jsx_runtime35.jsx)(import_ui16.Skeleton, { className: "h-4 w-full" }),
3007
+ /* @__PURE__ */ (0, import_jsx_runtime35.jsx)(import_ui16.Skeleton, { className: "h-4 w-3/4" })
3100
3008
  ] });
3101
3009
  }
3102
- const renderBody = (node, sections) => ({
3103
- primary: /* @__PURE__ */ (0, import_jsx_runtime36.jsx)(
3104
- GenreInfoPanelBody,
3105
- {
3106
- toolbar: sections.toolbar,
3107
- family: sections.family,
3108
- genreUuid: detail.uuid,
3109
- genrePlaylist: { ...detail.criteriaPlaylist, tracksCount: detail.tracksCount },
3110
- wikidataId: detail.wikidataId,
3111
- scope,
3112
- getBackendBaseUrl,
3113
- playback,
3114
- band: { node, onZoomIn, shareUrl, shareTarget: shareUrl ? shareTarget : void 0 },
3115
- extras
3116
- }
3117
- ),
3118
- aside: null
3119
- });
3120
- return /* @__PURE__ */ (0, import_jsx_runtime36.jsx)(
3121
- import_genre_tree_view5.InfoPanel,
3010
+ const selectGenre = (nodeId) => onSelectGenre(nodeId);
3011
+ const family = /* @__PURE__ */ (0, import_jsx_runtime35.jsx)(
3012
+ import_genre_tree_view5.GenreNodeFamily,
3122
3013
  {
3123
3014
  node: view.node,
3124
3015
  fill: view.fill,
3125
3016
  textColor: import_genre_tree_view5.ACCENT_TEXT_COLOR,
3126
3017
  parentNode: view.parentNode,
3127
3018
  childNodes: view.childNodes,
3128
- ancestorNodes: view.ancestorNodes,
3019
+ onSelectNode: selectGenre
3020
+ }
3021
+ );
3022
+ const ancestors = /* @__PURE__ */ (0, import_jsx_runtime35.jsx)(import_genre_tree_view5.GenreNodeAncestors, { ancestorNodes: view.ancestorNodes, onSelectNode: selectGenre });
3023
+ return /* @__PURE__ */ (0, import_jsx_runtime35.jsxs)(
3024
+ import_ui16.DetailPanel,
3025
+ {
3129
3026
  side: "left",
3027
+ title: view.node.name,
3028
+ headerAccent: { background: view.fill, color: import_genre_tree_view5.ACCENT_TEXT_COLOR },
3029
+ headerActions: onExpandedChange ? /* @__PURE__ */ (0, import_jsx_runtime35.jsx)(
3030
+ import_ui16.IconButton,
3031
+ {
3032
+ "aria-label": expanded ? "Shrink panel" : "Extend panel",
3033
+ style: { color: "inherit" },
3034
+ onClick: () => onExpandedChange(!expanded),
3035
+ children: expanded ? /* @__PURE__ */ (0, import_jsx_runtime35.jsx)(import_lucide_react13.Minimize2, { "aria-hidden": "true", className: "h-4 w-4" }) : /* @__PURE__ */ (0, import_jsx_runtime35.jsx)(import_lucide_react13.Maximize2, { "aria-hidden": "true", className: "h-4 w-4" })
3036
+ }
3037
+ ) : void 0,
3130
3038
  onClose,
3131
- onSelectNode: (nodeId) => onSelectGenre(nodeId),
3132
- itemCountLabel: ITEM_COUNT_LABEL,
3133
- renderInfoPanelBody: renderBody,
3039
+ collapsed,
3134
3040
  expanded,
3135
3041
  onExpandedChange,
3136
- collapsed,
3137
- hideExpandButton: onExpandedChange === void 0
3042
+ edgeTab,
3043
+ aside: ancestors,
3044
+ children: [
3045
+ /* @__PURE__ */ (0, import_jsx_runtime35.jsx)(
3046
+ GenreInfoPanelBody,
3047
+ {
3048
+ family,
3049
+ genreUuid: detail.uuid,
3050
+ genrePlaylist: { ...detail.criteriaPlaylist, tracksCount: detail.tracksCount },
3051
+ wikidataId: detail.wikidataId,
3052
+ scope,
3053
+ getBackendBaseUrl,
3054
+ playback,
3055
+ band: { node: view.node, onZoomIn, shareUrl, shareTarget: shareUrl ? shareTarget : void 0 },
3056
+ extras
3057
+ }
3058
+ ),
3059
+ !expanded && ancestors
3060
+ ]
3138
3061
  }
3139
3062
  );
3140
3063
  }
@@ -3143,13 +3066,13 @@ function GenreDetailPanel({
3143
3066
  var import_genre_tree_view6 = require("@behindthemusictree/genre-tree-view");
3144
3067
 
3145
3068
  // src/genre-tree/track-list-sidebar/TrackListSidebar.tsx
3146
- var import_react19 = require("react");
3147
- var import_lucide_react13 = require("lucide-react");
3069
+ var import_react18 = require("react");
3070
+ var import_lucide_react14 = require("lucide-react");
3148
3071
  var import_genre_tree21 = require("@behindthemusictree/app-kit/genre-tree");
3149
3072
  var import_genre_tree22 = require("@behindthemusictree/app-kit/genre-tree");
3150
- var import_track_noun6 = require("@behindthemusictree/app-kit/track-noun");
3151
- var import_jsx_runtime37 = require("react/jsx-runtime");
3152
- var useBrowserLayoutEffect = typeof window === "undefined" ? import_react19.useEffect : import_react19.useLayoutEffect;
3073
+ var import_track_noun3 = require("@behindthemusictree/app-kit/track-noun");
3074
+ var import_jsx_runtime36 = require("react/jsx-runtime");
3075
+ var useBrowserLayoutEffect = typeof window === "undefined" ? import_react18.useEffect : import_react18.useLayoutEffect;
3153
3076
  function TrackListSidebar({
3154
3077
  className: className2 = "",
3155
3078
  renderDuration,
@@ -3162,17 +3085,17 @@ function TrackListSidebar({
3162
3085
  renderSongLink
3163
3086
  }) {
3164
3087
  const { trackList, loadMore, autoAdvanceScrollRequest } = (0, import_genre_tree21.useTrackList)();
3165
- const trackNoun = (0, import_track_noun6.useTrackNoun)();
3166
- const trackListRef = (0, import_react19.useRef)(null);
3167
- const previousOriginRef = (0, import_react19.useRef)(trackList?.origin ?? null);
3168
- const handledAutoAdvanceRequestIdRef = (0, import_react19.useRef)(null);
3169
- const shouldFollowPlaybackRef = (0, import_react19.useRef)(true);
3170
- const programmaticScrollTopRef = (0, import_react19.useRef)(null);
3171
- const userScrollIntentRef = (0, import_react19.useRef)(false);
3172
- const userScrollIntentTimerRef = (0, import_react19.useRef)(null);
3173
- const loadMoreSentinelRef = (0, import_react19.useRef)(null);
3088
+ const trackNoun = (0, import_track_noun3.useTrackNoun)();
3089
+ const trackListRef = (0, import_react18.useRef)(null);
3090
+ const previousOriginRef = (0, import_react18.useRef)(trackList?.origin ?? null);
3091
+ const handledAutoAdvanceRequestIdRef = (0, import_react18.useRef)(null);
3092
+ const shouldFollowPlaybackRef = (0, import_react18.useRef)(true);
3093
+ const programmaticScrollTopRef = (0, import_react18.useRef)(null);
3094
+ const userScrollIntentRef = (0, import_react18.useRef)(false);
3095
+ const userScrollIntentTimerRef = (0, import_react18.useRef)(null);
3096
+ const loadMoreSentinelRef = (0, import_react18.useRef)(null);
3174
3097
  const hasMore = !!trackList && trackList.nextPage !== null;
3175
- (0, import_react19.useEffect)(() => {
3098
+ (0, import_react18.useEffect)(() => {
3176
3099
  const sentinel = loadMoreSentinelRef.current;
3177
3100
  if (!hasMore || !sentinel) return;
3178
3101
  const observer = new IntersectionObserver((entries) => {
@@ -3192,11 +3115,11 @@ function TrackListSidebar({
3192
3115
  programmaticScrollTopRef.current = null;
3193
3116
  if (trackListRef.current) trackListRef.current.scrollTop = 0;
3194
3117
  }, [trackList?.origin]);
3195
- (0, import_react19.useEffect)(() => {
3118
+ (0, import_react18.useEffect)(() => {
3196
3119
  const list = trackListRef.current;
3197
3120
  if (!isCollapsed && list?.scrollTop === 0) shouldFollowPlaybackRef.current = true;
3198
3121
  }, [isCollapsed]);
3199
- (0, import_react19.useEffect)(
3122
+ (0, import_react18.useEffect)(
3200
3123
  () => () => {
3201
3124
  if (userScrollIntentTimerRef.current !== null) clearTimeout(userScrollIntentTimerRef.current);
3202
3125
  },
@@ -3250,25 +3173,25 @@ function TrackListSidebar({
3250
3173
  }
3251
3174
  };
3252
3175
  const positionClasses = layout === "inline" ? "relative w-full flex h-full flex-col" : "fixed right-0 w-[50rem] flex flex-col";
3253
- return trackList ? /* @__PURE__ */ (0, import_jsx_runtime37.jsxs)(
3176
+ return trackList ? /* @__PURE__ */ (0, import_jsx_runtime36.jsxs)(
3254
3177
  "div",
3255
3178
  {
3256
3179
  "data-theme": "dark",
3257
3180
  className: `track-list-sidebar ${positionClasses} rounded-2xl bg-[var(--color-surface)] pb-1 ${className2}`,
3258
3181
  style: layout === "fixed" ? { top: "63.5px", maxHeight: "calc(100vh - 63.5px - 79px)" } : void 0,
3259
3182
  children: [
3260
- /* @__PURE__ */ (0, import_jsx_runtime37.jsxs)("div", { className: "header flex h-16 items-center px-4 py-2 text-[var(--color-text)]", children: [
3261
- /* @__PURE__ */ (0, import_jsx_runtime37.jsxs)("div", { className: "min-w-0 flex flex-1 flex-col justify-center", children: [
3262
- /* @__PURE__ */ (0, import_jsx_runtime37.jsxs)("div", { className: "origin flex text-xl", children: [
3263
- /* @__PURE__ */ (0, import_jsx_runtime37.jsx)("div", { className: "from h-auto flex flex-col justify-center items-center mr-2", children: "From" }),
3264
- /* @__PURE__ */ (0, import_jsx_runtime37.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_runtime37.jsx)("div", { className: "name max-w-full truncate", children: trackList.origin.label }) })
3183
+ /* @__PURE__ */ (0, import_jsx_runtime36.jsxs)("div", { className: "header flex h-16 items-center px-4 py-2 text-[var(--color-text)]", children: [
3184
+ /* @__PURE__ */ (0, import_jsx_runtime36.jsxs)("div", { className: "min-w-0 flex flex-1 flex-col justify-center", children: [
3185
+ /* @__PURE__ */ (0, import_jsx_runtime36.jsxs)("div", { className: "origin flex text-xl", children: [
3186
+ /* @__PURE__ */ (0, import_jsx_runtime36.jsx)("div", { className: "from h-auto flex flex-col justify-center items-center mr-2", children: "From" }),
3187
+ /* @__PURE__ */ (0, import_jsx_runtime36.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_runtime36.jsx)("div", { className: "name max-w-full truncate", children: trackList.origin.label }) })
3265
3188
  ] }),
3266
- /* @__PURE__ */ (0, import_jsx_runtime37.jsxs)("div", { className: "info flex items-center text-sm text-[var(--color-text-muted)]", children: [
3189
+ /* @__PURE__ */ (0, import_jsx_runtime36.jsxs)("div", { className: "info flex items-center text-sm text-[var(--color-text-muted)]", children: [
3267
3190
  trackList.origin.type === import_genre_tree22.TrackListOriginType.GENRE_PLAYLIST ? "Genre \u2022 " : trackList.origin.type === import_genre_tree22.TrackListOriginType.ARTIST ? "Artist \u2022 " : `${trackNoun.singular} playlist \u2022 `,
3268
3191
  `${trackList.total} ${trackList.total > 1 ? trackNoun.plural : trackNoun.singular}`
3269
3192
  ] })
3270
3193
  ] }),
3271
- (isCollapsed && onExpand || !isCollapsed && onMinimize) && /* @__PURE__ */ (0, import_jsx_runtime37.jsx)(
3194
+ (isCollapsed && onExpand || !isCollapsed && onMinimize) && /* @__PURE__ */ (0, import_jsx_runtime36.jsx)(
3272
3195
  "button",
3273
3196
  {
3274
3197
  type: "button",
@@ -3277,11 +3200,11 @@ function TrackListSidebar({
3277
3200
  "aria-expanded": !isCollapsed,
3278
3201
  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"}`,
3279
3202
  onClick: isCollapsed ? onExpand : onMinimize,
3280
- children: /* @__PURE__ */ (0, import_jsx_runtime37.jsx)(import_lucide_react13.ListMusic, { "aria-hidden": "true", className: "h-5 w-5" })
3203
+ children: /* @__PURE__ */ (0, import_jsx_runtime36.jsx)(import_lucide_react14.ListMusic, { "aria-hidden": "true", className: "h-5 w-5" })
3281
3204
  }
3282
3205
  )
3283
3206
  ] }),
3284
- !isCollapsed && /* @__PURE__ */ (0, import_jsx_runtime37.jsxs)(
3207
+ !isCollapsed && /* @__PURE__ */ (0, import_jsx_runtime36.jsxs)(
3285
3208
  "ul",
3286
3209
  {
3287
3210
  ref: trackListRef,
@@ -3291,7 +3214,7 @@ function TrackListSidebar({
3291
3214
  onPointerDown: markUserScrollIntent,
3292
3215
  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",
3293
3216
  children: [
3294
- trackList.tracks.map((track, index) => /* @__PURE__ */ (0, import_jsx_runtime37.jsx)("li", { "data-track-uuid": track.uuid, children: /* @__PURE__ */ (0, import_jsx_runtime37.jsx)(
3217
+ trackList.tracks.map((track, index) => /* @__PURE__ */ (0, import_jsx_runtime36.jsx)("li", { "data-track-uuid": track.uuid, children: /* @__PURE__ */ (0, import_jsx_runtime36.jsx)(
3295
3218
  TrackItem,
3296
3219
  {
3297
3220
  track,
@@ -3302,7 +3225,7 @@ function TrackListSidebar({
3302
3225
  renderSongLink
3303
3226
  }
3304
3227
  ) }, track.uuid)),
3305
- hasMore && /* @__PURE__ */ (0, import_jsx_runtime37.jsx)("li", { ref: loadMoreSentinelRef, "aria-hidden": "true", className: "h-px" })
3228
+ hasMore && /* @__PURE__ */ (0, import_jsx_runtime36.jsx)("li", { ref: loadMoreSentinelRef, "aria-hidden": "true", className: "h-px" })
3306
3229
  ]
3307
3230
  }
3308
3231
  )
@@ -3312,16 +3235,17 @@ function TrackListSidebar({
3312
3235
  }
3313
3236
 
3314
3237
  // src/genre-tree/PlayerPanel.tsx
3315
- var import_react20 = require("react");
3316
- var import_tailwind_merge2 = require("tailwind-merge");
3238
+ var import_react19 = require("react");
3239
+ var import_tailwind_merge = require("tailwind-merge");
3317
3240
  var import_player13 = require("@behindthemusictree/app-kit/player");
3318
3241
  var import_genre_tree24 = require("@behindthemusictree/app-kit/genre-tree");
3242
+ var import_ui17 = require("@behindthemusictree/ui");
3319
3243
 
3320
3244
  // src/genre-tree/PlayerControls.tsx
3321
- var import_lucide_react14 = require("lucide-react");
3245
+ var import_lucide_react15 = require("lucide-react");
3322
3246
  var import_genre_tree23 = require("@behindthemusictree/app-kit/genre-tree");
3323
3247
  var import_player12 = require("@behindthemusictree/app-kit/player");
3324
- var import_jsx_runtime38 = require("react/jsx-runtime");
3248
+ var import_jsx_runtime37 = require("react/jsx-runtime");
3325
3249
  function PlayerControls() {
3326
3250
  const {
3327
3251
  duration,
@@ -3340,15 +3264,15 @@ function PlayerControls() {
3340
3264
  const safeDuration = Number.isFinite(duration) && duration > 0 ? duration : 0;
3341
3265
  const safeCurrentTime = Number.isFinite(currentTime) ? Math.max(0, Math.min(currentTime, safeDuration)) : 0;
3342
3266
  const isPlaying = playState === import_player12.PlayStates.PLAYING;
3343
- return /* @__PURE__ */ (0, import_jsx_runtime38.jsxs)(
3267
+ return /* @__PURE__ */ (0, import_jsx_runtime37.jsxs)(
3344
3268
  "section",
3345
3269
  {
3346
3270
  "aria-label": "Player controls",
3347
3271
  className: "player-controls flex shrink-0 flex-col gap-2 border-b border-zinc-800 px-3 py-2 text-zinc-200",
3348
3272
  children: [
3349
- /* @__PURE__ */ (0, import_jsx_runtime38.jsxs)("div", { className: "flex min-w-0 items-center gap-2 text-xs tabular-nums text-zinc-400", children: [
3350
- /* @__PURE__ */ (0, import_jsx_runtime38.jsx)("span", { "aria-label": "Elapsed time", children: (0, import_genre_tree23.formatTime)(safeCurrentTime) }),
3351
- /* @__PURE__ */ (0, import_jsx_runtime38.jsx)(
3273
+ /* @__PURE__ */ (0, import_jsx_runtime37.jsxs)("div", { className: "flex min-w-0 items-center gap-2 text-xs tabular-nums text-zinc-400", children: [
3274
+ /* @__PURE__ */ (0, import_jsx_runtime37.jsx)("span", { "aria-label": "Elapsed time", children: (0, import_genre_tree23.formatTime)(safeCurrentTime) }),
3275
+ /* @__PURE__ */ (0, import_jsx_runtime37.jsx)(
3352
3276
  "input",
3353
3277
  {
3354
3278
  "aria-label": "Seek",
@@ -3362,11 +3286,11 @@ function PlayerControls() {
3362
3286
  onChange: (event) => seekTo(Number(event.currentTarget.value))
3363
3287
  }
3364
3288
  ),
3365
- /* @__PURE__ */ (0, import_jsx_runtime38.jsx)("span", { "aria-label": "Duration", children: (0, import_genre_tree23.formatTime)(safeDuration) })
3289
+ /* @__PURE__ */ (0, import_jsx_runtime37.jsx)("span", { "aria-label": "Duration", children: (0, import_genre_tree23.formatTime)(safeDuration) })
3366
3290
  ] }),
3367
- /* @__PURE__ */ (0, import_jsx_runtime38.jsxs)("div", { className: "grid min-h-9 grid-cols-[1fr_auto_1fr] items-center gap-3", children: [
3368
- /* @__PURE__ */ (0, import_jsx_runtime38.jsxs)("div", { className: "col-start-2 flex items-center justify-center gap-2", children: [
3369
- /* @__PURE__ */ (0, import_jsx_runtime38.jsx)(
3291
+ /* @__PURE__ */ (0, import_jsx_runtime37.jsxs)("div", { className: "grid min-h-9 grid-cols-[1fr_auto_1fr] items-center gap-3", children: [
3292
+ /* @__PURE__ */ (0, import_jsx_runtime37.jsxs)("div", { className: "col-start-2 flex items-center justify-center gap-2", children: [
3293
+ /* @__PURE__ */ (0, import_jsx_runtime37.jsx)(
3370
3294
  "button",
3371
3295
  {
3372
3296
  type: "button",
@@ -3375,10 +3299,10 @@ function PlayerControls() {
3375
3299
  disabled: !isReady || !hasPreviousTrack,
3376
3300
  onClick: playPreviousTrack,
3377
3301
  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",
3378
- children: /* @__PURE__ */ (0, import_jsx_runtime38.jsx)(import_lucide_react14.SkipBack, { "aria-hidden": "true", className: "h-4 w-4" })
3302
+ children: /* @__PURE__ */ (0, import_jsx_runtime37.jsx)(import_lucide_react15.SkipBack, { "aria-hidden": "true", className: "h-4 w-4" })
3379
3303
  }
3380
3304
  ),
3381
- /* @__PURE__ */ (0, import_jsx_runtime38.jsx)(
3305
+ /* @__PURE__ */ (0, import_jsx_runtime37.jsx)(
3382
3306
  "button",
3383
3307
  {
3384
3308
  type: "button",
@@ -3387,10 +3311,10 @@ function PlayerControls() {
3387
3311
  disabled: !isReady,
3388
3312
  onClick: handlePlayPauseAction,
3389
3313
  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",
3390
- children: isPlaying ? /* @__PURE__ */ (0, import_jsx_runtime38.jsx)(import_lucide_react14.Pause, { "aria-hidden": "true", className: "h-4 w-4", fill: "currentColor" }) : /* @__PURE__ */ (0, import_jsx_runtime38.jsx)(import_lucide_react14.Play, { "aria-hidden": "true", className: "h-4 w-4", fill: "currentColor" })
3314
+ children: isPlaying ? /* @__PURE__ */ (0, import_jsx_runtime37.jsx)(import_lucide_react15.Pause, { "aria-hidden": "true", className: "h-4 w-4", fill: "currentColor" }) : /* @__PURE__ */ (0, import_jsx_runtime37.jsx)(import_lucide_react15.Play, { "aria-hidden": "true", className: "h-4 w-4", fill: "currentColor" })
3391
3315
  }
3392
3316
  ),
3393
- /* @__PURE__ */ (0, import_jsx_runtime38.jsx)(
3317
+ /* @__PURE__ */ (0, import_jsx_runtime37.jsx)(
3394
3318
  "button",
3395
3319
  {
3396
3320
  type: "button",
@@ -3399,14 +3323,14 @@ function PlayerControls() {
3399
3323
  disabled: !isReady || !hasNextTrack,
3400
3324
  onClick: playNextTrack,
3401
3325
  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",
3402
- children: /* @__PURE__ */ (0, import_jsx_runtime38.jsx)(import_lucide_react14.SkipForward, { "aria-hidden": "true", className: "h-4 w-4" })
3326
+ children: /* @__PURE__ */ (0, import_jsx_runtime37.jsx)(import_lucide_react15.SkipForward, { "aria-hidden": "true", className: "h-4 w-4" })
3403
3327
  }
3404
3328
  )
3405
3329
  ] }),
3406
- /* @__PURE__ */ (0, import_jsx_runtime38.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: [
3407
- volume === 0 ? /* @__PURE__ */ (0, import_jsx_runtime38.jsx)(import_lucide_react14.VolumeX, { "aria-hidden": "true", className: "h-[0.7rem] w-[0.7rem] shrink-0" }) : /* @__PURE__ */ (0, import_jsx_runtime38.jsx)(import_lucide_react14.Volume2, { "aria-hidden": "true", className: "h-[0.7rem] w-[0.7rem] shrink-0" }),
3408
- /* @__PURE__ */ (0, import_jsx_runtime38.jsx)("span", { className: "sr-only", children: "Volume" }),
3409
- /* @__PURE__ */ (0, import_jsx_runtime38.jsx)(
3330
+ /* @__PURE__ */ (0, import_jsx_runtime37.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: [
3331
+ volume === 0 ? /* @__PURE__ */ (0, import_jsx_runtime37.jsx)(import_lucide_react15.VolumeX, { "aria-hidden": "true", className: "h-[0.7rem] w-[0.7rem] shrink-0" }) : /* @__PURE__ */ (0, import_jsx_runtime37.jsx)(import_lucide_react15.Volume2, { "aria-hidden": "true", className: "h-[0.7rem] w-[0.7rem] shrink-0" }),
3332
+ /* @__PURE__ */ (0, import_jsx_runtime37.jsx)("span", { className: "sr-only", children: "Volume" }),
3333
+ /* @__PURE__ */ (0, import_jsx_runtime37.jsx)(
3410
3334
  "input",
3411
3335
  {
3412
3336
  "aria-label": "Volume",
@@ -3428,7 +3352,7 @@ function PlayerControls() {
3428
3352
  }
3429
3353
 
3430
3354
  // src/genre-tree/PlayerPanel.tsx
3431
- var import_jsx_runtime39 = require("react/jsx-runtime");
3355
+ var import_jsx_runtime38 = require("react/jsx-runtime");
3432
3356
  var PANEL_ID = "player-panel";
3433
3357
  var DEFAULT_EXTENDED_WIDTH = "calc(100vw - var(--layout-center-left) - var(--layout-gutter))";
3434
3358
  function PlayerPanel({
@@ -3442,12 +3366,12 @@ function PlayerPanel({
3442
3366
  }) {
3443
3367
  const { playerTrackObject } = (0, import_player13.usePlayer)();
3444
3368
  const { toggleTrackListSidebar, isTrackListSidebarVisible } = (0, import_genre_tree24.useTrackListSidebarVisibility)();
3445
- const [lastLoadedTrackTitle, setLastLoadedTrackTitle] = (0, import_react20.useState)("");
3369
+ const [lastLoadedTrackTitle, setLastLoadedTrackTitle] = (0, import_react19.useState)("");
3446
3370
  const loadedTrackTitle = playerTrackObject?.track.title;
3447
- (0, import_react20.useEffect)(() => {
3371
+ (0, import_react19.useEffect)(() => {
3448
3372
  if (loadedTrackTitle) setLastLoadedTrackTitle(loadedTrackTitle);
3449
3373
  }, [loadedTrackTitle]);
3450
- const [uncontrolledExtended, setUncontrolledExtended] = (0, import_react20.useState)(false);
3374
+ const [uncontrolledExtended, setUncontrolledExtended] = (0, import_react19.useState)(false);
3451
3375
  const isExtendRequested = expanded ?? uncontrolledExtended;
3452
3376
  const setIsExtendRequested = (next) => {
3453
3377
  setUncontrolledExtended(next);
@@ -3468,11 +3392,11 @@ function PlayerPanel({
3468
3392
  onClick: () => setIsExtendRequested(true),
3469
3393
  expanded: false
3470
3394
  } : null;
3471
- return /* @__PURE__ */ (0, import_jsx_runtime39.jsxs)(
3395
+ return /* @__PURE__ */ (0, import_jsx_runtime38.jsxs)(
3472
3396
  "div",
3473
3397
  {
3474
3398
  id: PANEL_ID,
3475
- className: (0, import_tailwind_merge2.twMerge)(
3399
+ className: (0, import_tailwind_merge.twMerge)(
3476
3400
  "fixed right-[var(--layout-gutter)] top-[var(--layout-band-top)] z-40 max-h-[calc(100vh-var(--layout-band-top)-3.5rem)]",
3477
3401
  !isExtended && "w-[var(--layout-right-w)]",
3478
3402
  isTrackListSidebarVisible && "h-[calc(100vh-var(--layout-band-top)-3.5rem)]",
@@ -3481,8 +3405,8 @@ function PlayerPanel({
3481
3405
  ),
3482
3406
  style: isExtended ? { width: extendedWidth } : void 0,
3483
3407
  children: [
3484
- /* @__PURE__ */ (0, import_jsx_runtime39.jsx)(
3485
- PanelEdgeTab,
3408
+ /* @__PURE__ */ (0, import_jsx_runtime38.jsx)(
3409
+ import_ui17.PanelEdgeTab,
3486
3410
  {
3487
3411
  side: "right",
3488
3412
  edge: tabEdge,
@@ -3491,30 +3415,30 @@ function PlayerPanel({
3491
3415
  className: "absolute right-full top-1/2 -translate-y-1/2"
3492
3416
  }
3493
3417
  ),
3494
- /* @__PURE__ */ (0, import_jsx_runtime39.jsxs)(
3418
+ /* @__PURE__ */ (0, import_jsx_runtime38.jsxs)(
3495
3419
  "div",
3496
3420
  {
3497
- className: (0, import_tailwind_merge2.twMerge)(
3421
+ className: (0, import_tailwind_merge.twMerge)(
3498
3422
  "flex h-full max-h-[inherit] overflow-hidden rounded-[var(--radius-panel,1rem)] bg-zinc-950 text-zinc-100 shadow-[0_1px_2px_rgba(0,0,0,0.5),0_2px_6px_2px_rgba(0,0,0,0.3)]",
3499
3423
  isExtended ? "flex-row" : "flex-col"
3500
3424
  ),
3501
3425
  children: [
3502
- /* @__PURE__ */ (0, import_jsx_runtime39.jsxs)("div", { className: "flex w-[var(--layout-right-w)] shrink-0 flex-col", children: [
3503
- /* @__PURE__ */ (0, import_jsx_runtime39.jsx)("div", { className: "flex shrink-0 items-center justify-between gap-2 border-b border-zinc-800 p-2", children: /* @__PURE__ */ (0, import_jsx_runtime39.jsx)("span", { className: "min-w-0 truncate text-sm font-semibold", children: loadedTrackTitle || lastLoadedTrackTitle }) }),
3504
- /* @__PURE__ */ (0, import_jsx_runtime39.jsxs)("div", { className: "relative w-full shrink-0", children: [
3505
- /* @__PURE__ */ (0, import_jsx_runtime39.jsx)(import_player13.PlayerVideoSurface, { className: "aspect-video w-full bg-black" }),
3506
- playerTrackObject?.loadError && /* @__PURE__ */ (0, import_jsx_runtime39.jsx)("span", { className: "absolute bottom-2 left-2 right-2 text-xs text-red-400", children: playerTrackObject.loadError })
3426
+ /* @__PURE__ */ (0, import_jsx_runtime38.jsxs)("div", { className: "flex w-[var(--layout-right-w)] shrink-0 flex-col", children: [
3427
+ /* @__PURE__ */ (0, import_jsx_runtime38.jsx)("div", { className: "flex shrink-0 items-center justify-between gap-2 border-b border-zinc-800 p-2", children: /* @__PURE__ */ (0, import_jsx_runtime38.jsx)("span", { className: "min-w-0 truncate text-sm font-semibold", children: loadedTrackTitle || lastLoadedTrackTitle }) }),
3428
+ /* @__PURE__ */ (0, import_jsx_runtime38.jsxs)("div", { className: "relative w-full shrink-0", children: [
3429
+ /* @__PURE__ */ (0, import_jsx_runtime38.jsx)(import_player13.PlayerVideoSurface, { className: "aspect-video w-full bg-black" }),
3430
+ playerTrackObject?.loadError && /* @__PURE__ */ (0, import_jsx_runtime38.jsx)("span", { className: "absolute bottom-2 left-2 right-2 text-xs text-red-400", children: playerTrackObject.loadError })
3507
3431
  ] }),
3508
- /* @__PURE__ */ (0, import_jsx_runtime39.jsx)(PlayerControls, {})
3432
+ /* @__PURE__ */ (0, import_jsx_runtime38.jsx)(PlayerControls, {})
3509
3433
  ] }),
3510
- /* @__PURE__ */ (0, import_jsx_runtime39.jsx)(
3434
+ /* @__PURE__ */ (0, import_jsx_runtime38.jsx)(
3511
3435
  TrackListSidebar,
3512
3436
  {
3513
3437
  layout: "inline",
3514
3438
  isCollapsed: !isTrackListSidebarVisible,
3515
3439
  onExpand: toggleTrackListSidebar,
3516
3440
  onMinimize: hideSongList,
3517
- className: (0, import_tailwind_merge2.twMerge)(
3441
+ className: (0, import_tailwind_merge.twMerge)(
3518
3442
  "min-h-0 min-w-0 flex-1",
3519
3443
  isExtended && "border-l border-zinc-800"
3520
3444
  ),
@@ -3534,11 +3458,11 @@ function PlayerPanel({
3534
3458
  var import_uuid = require("uuid");
3535
3459
  var import_ai = require("react-icons/ai");
3536
3460
  var import_genre_tree25 = require("@behindthemusictree/app-kit/genre-tree");
3537
- var import_jsx_runtime40 = require("react/jsx-runtime");
3461
+ var import_jsx_runtime39 = require("react/jsx-runtime");
3538
3462
  function Rating({ rating, handleChange }) {
3539
3463
  const instanceId = (0, import_uuid.v4)();
3540
- return /* @__PURE__ */ (0, import_jsx_runtime40.jsx)("div", { className: "flex items-center", children: [...Array(6)].map((_, i) => /* @__PURE__ */ (0, import_jsx_runtime40.jsxs)("div", { className: `relative ${i === 0 ? "mr-4" : "mr-1"}`, children: [
3541
- /* @__PURE__ */ (0, import_jsx_runtime40.jsx)(
3464
+ return /* @__PURE__ */ (0, import_jsx_runtime39.jsx)("div", { className: "flex items-center", children: [...Array(6)].map((_, i) => /* @__PURE__ */ (0, import_jsx_runtime39.jsxs)("div", { className: `relative ${i === 0 ? "mr-4" : "mr-1"}`, children: [
3465
+ /* @__PURE__ */ (0, import_jsx_runtime39.jsx)(
3542
3466
  "input",
3543
3467
  {
3544
3468
  id: `star-${instanceId}-${i}`,
@@ -3550,16 +3474,16 @@ function Rating({ rating, handleChange }) {
3550
3474
  onChange: handleChange
3551
3475
  }
3552
3476
  ),
3553
- /* @__PURE__ */ (0, import_jsx_runtime40.jsx)("label", { htmlFor: `star-${instanceId}-${i}`, className: "cursor-pointer", children: i === 0 ? /* @__PURE__ */ (0, import_jsx_runtime40.jsxs)("div", { className: "flex", children: [
3554
- /* @__PURE__ */ (0, import_jsx_runtime40.jsx)(
3477
+ /* @__PURE__ */ (0, import_jsx_runtime39.jsx)("label", { htmlFor: `star-${instanceId}-${i}`, className: "cursor-pointer", children: i === 0 ? /* @__PURE__ */ (0, import_jsx_runtime39.jsxs)("div", { className: "flex", children: [
3478
+ /* @__PURE__ */ (0, import_jsx_runtime39.jsx)(
3555
3479
  import_ai.AiOutlineStar,
3556
3480
  {
3557
3481
  className: `w-7 h-7 absolute top-0 left-0 ${rating === import_genre_tree25.FORM_RATING_NULL_VALUE ? "text-[var(--color-accent-rating)]" : "text-[var(--color-text-muted)]"}`
3558
3482
  }
3559
3483
  ),
3560
- /* @__PURE__ */ (0, import_jsx_runtime40.jsx)("span", { className: "flex-grow w-8" }),
3484
+ /* @__PURE__ */ (0, import_jsx_runtime39.jsx)("span", { className: "flex-grow w-8" }),
3561
3485
  " "
3562
- ] }) : /* @__PURE__ */ (0, import_jsx_runtime40.jsx)(
3486
+ ] }) : /* @__PURE__ */ (0, import_jsx_runtime39.jsx)(
3563
3487
  import_ai.AiFillStar,
3564
3488
  {
3565
3489
  className: `w-7 h-7 absolute top-0 left-0 ${(rating ?? -1) >= i ? "text-[var(--color-accent-rating)]" : "text-[var(--color-text-muted)]"}`
@@ -3579,13 +3503,11 @@ function Rating({ rating, handleChange }) {
3579
3503
  GenreTreeView,
3580
3504
  GenreTreeViewSkeleton,
3581
3505
  GenreTreeWheelSkeleton,
3582
- PanelEdgeTab,
3583
3506
  PlayerPanel,
3584
3507
  Rating,
3585
3508
  TrackItem,
3586
3509
  TrackListSidebar,
3587
3510
  TrackPositionPlayPause,
3588
- useGenrePlaylistPlayback,
3589
- useLeftDetailPanelLayout
3511
+ useGenrePlaylistPlayback
3590
3512
  });
3591
3513
  //# sourceMappingURL=index.cjs.map