@behindthemusictree/app-kit-ui 29.1.0 → 31.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,27 +30,52 @@ __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_react15 = require("react");
43
+ var import_react16 = require("react");
46
44
  var import_react_query3 = require("@tanstack/react-query");
47
45
  var import_lucide_react11 = require("lucide-react");
48
- var import_ui13 = require("@behindthemusictree/ui");
46
+ var import_lucide_react12 = require("lucide-react");
47
+ var import_ui15 = require("@behindthemusictree/ui");
49
48
  var import_genre_tree_view4 = require("@behindthemusictree/genre-tree-view");
50
- var import_genre_tree16 = require("@behindthemusictree/app-kit/genre-tree");
51
- var import_genre_tree17 = require("@behindthemusictree/app-kit/genre-tree");
49
+ var import_genre_tree18 = require("@behindthemusictree/app-kit/genre-tree");
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,
@@ -736,6 +695,7 @@ function GenreTreeWheelHandoff({ skeleton, children, onRevealedChange }) {
736
695
  }
737
696
 
738
697
  // src/genre-tree/GenreSearch.tsx
698
+ var import_react7 = require("react");
739
699
  var import_lucide_react3 = require("lucide-react");
740
700
  var import_ui3 = require("@behindthemusictree/ui");
741
701
  var import_genre_tree2 = require("@behindthemusictree/app-kit/genre-tree");
@@ -748,7 +708,7 @@ function GenreSearch({
748
708
  onPrevious,
749
709
  placeholder,
750
710
  onQueryChange,
751
- extraResults
711
+ extraGroups = []
752
712
  }) {
753
713
  const { query, setQuery, results, canClear, clear, select } = (0, import_genre_tree2.useGenreSearch)({
754
714
  genrePlaylists,
@@ -756,11 +716,45 @@ function GenreSearch({
756
716
  selectedName,
757
717
  onClear
758
718
  });
759
- const extraItems = query !== "" && query !== selectedName ? extraResults?.items ?? [] : [];
760
- const label = extraResults ? "Search" : "Search a genre";
761
- const loading = query !== "" && query !== selectedName && (extraResults?.loading ?? false);
762
- const showList = results.length > 0 || extraItems.length > 0;
763
- const heading = "px-3 pt-2 pb-1 text-xs font-medium uppercase text-[var(--color-text-muted)]";
719
+ const [open, setOpen] = (0, import_react7.useState)(false);
720
+ const active = query !== "" && query !== selectedName;
721
+ const hasExtras = extraGroups.length > 0;
722
+ const label = hasExtras ? "Search" : "Search a genre";
723
+ const loading = active && extraGroups.some((group) => group.loading);
724
+ const status = active ? extraGroups.map((group) => group.status).filter(Boolean).join(". ") : "";
725
+ const groups = [
726
+ {
727
+ key: "genres",
728
+ label: hasExtras ? "Genres" : void 0,
729
+ options: results.map((genrePlaylist) => ({
730
+ key: genrePlaylist.uuid,
731
+ content: /* @__PURE__ */ (0, import_jsx_runtime9.jsxs)(import_jsx_runtime9.Fragment, { children: [
732
+ genrePlaylist.name,
733
+ genrePlaylist.isUmbrella && /* @__PURE__ */ (0, import_jsx_runtime9.jsx)("span", { className: "ml-2 text-xs italic text-[var(--color-text-muted)]", children: "umbrella" })
734
+ ] })
735
+ }))
736
+ },
737
+ ...extraGroups.map((group) => ({
738
+ key: group.key,
739
+ label: group.label,
740
+ options: active ? group.items.map((item) => ({ key: item.key, content: item.content })) : []
741
+ }))
742
+ ];
743
+ const handlePick = (groupKey, optionKey) => {
744
+ setOpen(false);
745
+ if (groupKey === "genres") {
746
+ const genrePlaylist = results.find((result) => result.uuid === optionKey);
747
+ if (!genrePlaylist) return;
748
+ select(genrePlaylist);
749
+ onQueryChange?.("");
750
+ return;
751
+ }
752
+ const item = extraGroups.find((group) => group.key === groupKey)?.items.find((entry) => entry.key === optionKey);
753
+ if (!item) return;
754
+ setQuery("");
755
+ onQueryChange?.("");
756
+ item.onSelect();
757
+ };
764
758
  return /* @__PURE__ */ (0, import_jsx_runtime9.jsxs)("div", { className: "genre-search relative w-[calc(var(--layout-detail-panel-w,25.5rem)-2*var(--layout-panel-inset,1rem))] shrink-0 max-md:w-full", children: [
765
759
  onPrevious && /* @__PURE__ */ (0, import_jsx_runtime9.jsx)(
766
760
  "button",
@@ -768,53 +762,51 @@ function GenreSearch({
768
762
  type: "button",
769
763
  "aria-label": "Previous",
770
764
  title: "Previous",
771
- 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)]",
772
766
  onClick: onPrevious,
773
767
  children: /* @__PURE__ */ (0, import_jsx_runtime9.jsx)(import_lucide_react3.ArrowLeft, { "aria-hidden": "true", className: "w-4 h-4" })
774
768
  }
775
769
  ),
776
770
  /* @__PURE__ */ (0, import_jsx_runtime9.jsx)(
777
- import_ui3.Input,
771
+ import_ui3.Combobox,
778
772
  {
779
- type: "text",
773
+ ariaLabel: label,
774
+ listLabel: `${label} results`,
780
775
  value: query,
781
- onChange: (event) => {
782
- setQuery(event.target.value);
783
- onQueryChange?.(event.target.value);
784
- },
785
776
  placeholder: placeholder ?? `${label}\u2026`,
786
- "aria-label": label,
787
- "aria-busy": loading,
788
- inputMode: "search",
789
- enterKeyHint: "search",
790
- autoComplete: "off",
791
- className: "h-[var(--layout-search-h,3rem)] pl-6 pr-20 text-base transition-[padding] duration-200 ease-out motion-reduce:transition-none bg-[var(--color-surface)] border-[var(--color-border)] rounded-full shadow-md"
777
+ groups,
778
+ open,
779
+ onOpenChange: setOpen,
780
+ onValueChange: (value) => {
781
+ setQuery(value);
782
+ onQueryChange?.(value);
783
+ },
784
+ onPick: handlePick,
785
+ status,
786
+ loading: loading ? "Searching\u2026" : void 0,
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`,
788
+ className: "genre-search-results z-40 rounded-xl shadow-md",
789
+ listClassName: "max-h-[min(20rem,50dvh)]",
790
+ optionClassName: "[@media(pointer:coarse)]:min-h-[var(--layout-touch-target)]"
792
791
  }
793
792
  ),
794
793
  /* @__PURE__ */ (0, import_jsx_runtime9.jsxs)("div", { className: "absolute right-3 top-1/2 flex items-center -translate-y-1/2", children: [
795
- /* @__PURE__ */ (0, import_jsx_runtime9.jsxs)(
796
- "span",
797
- {
798
- "data-loading": loading,
799
- className: "relative p-2 text-[var(--color-text-muted)] pointer-events-none",
800
- children: [
801
- /* @__PURE__ */ (0, import_jsx_runtime9.jsx)(
802
- import_lucide_react3.Search,
803
- {
804
- "aria-hidden": "true",
805
- className: `w-6 h-6 transition-opacity duration-200 motion-reduce:transition-none ${loading ? "opacity-0" : ""}`
806
- }
807
- ),
808
- /* @__PURE__ */ (0, import_jsx_runtime9.jsx)(
809
- import_lucide_react3.LoaderCircle,
810
- {
811
- "aria-hidden": "true",
812
- className: `absolute inset-2 w-6 h-6 transition-opacity duration-200 motion-reduce:transition-none ${loading ? "opacity-100 delay-300 animate-spin motion-reduce:animate-none" : "opacity-0"}`
813
- }
814
- )
815
- ]
816
- }
817
- ),
794
+ /* @__PURE__ */ (0, import_jsx_runtime9.jsxs)("span", { "data-loading": loading, className: "relative p-2 text-[var(--color-text-muted)] pointer-events-none", children: [
795
+ /* @__PURE__ */ (0, import_jsx_runtime9.jsx)(
796
+ import_lucide_react3.Search,
797
+ {
798
+ "aria-hidden": "true",
799
+ className: `w-6 h-6 transition-opacity duration-200 motion-reduce:transition-none ${loading ? "opacity-0" : ""}`
800
+ }
801
+ ),
802
+ /* @__PURE__ */ (0, import_jsx_runtime9.jsx)(
803
+ import_lucide_react3.LoaderCircle,
804
+ {
805
+ "aria-hidden": "true",
806
+ className: `absolute inset-2 w-6 h-6 transition-opacity duration-200 motion-reduce:transition-none ${loading ? "opacity-100 delay-300 animate-spin motion-reduce:animate-none" : "opacity-0"}`
807
+ }
808
+ )
809
+ ] }),
818
810
  canClear && /* @__PURE__ */ (0, import_jsx_runtime9.jsx)(
819
811
  "button",
820
812
  {
@@ -828,39 +820,6 @@ function GenreSearch({
828
820
  children: /* @__PURE__ */ (0, import_jsx_runtime9.jsx)(import_lucide_react3.X, { "aria-hidden": "true", className: "w-6 h-6" })
829
821
  }
830
822
  )
831
- ] }),
832
- extraResults?.status && /* @__PURE__ */ (0, import_jsx_runtime9.jsx)("p", { role: "status", "aria-live": "polite", className: "sr-only", children: extraResults.status }),
833
- showList && /* @__PURE__ */ (0, import_jsx_runtime9.jsxs)("ul", { className: "genre-search-results absolute z-40 w-full max-h-[min(16rem,50dvh)] mt-1 overflow-y-auto bg-[var(--color-surface)] border border-[var(--color-border)] rounded-xl shadow-md", children: [
834
- extraResults && results.length > 0 && /* @__PURE__ */ (0, import_jsx_runtime9.jsx)("li", { className: heading, children: "Genres" }),
835
- results.map((genrePlaylist) => /* @__PURE__ */ (0, import_jsx_runtime9.jsx)("li", { children: /* @__PURE__ */ (0, import_jsx_runtime9.jsxs)(
836
- "button",
837
- {
838
- type: "button",
839
- className: "w-full px-3 py-1.5 [@media(pointer:coarse)]:min-h-[var(--layout-touch-target)] text-left hover:bg-[var(--color-surface-muted)]",
840
- onClick: () => {
841
- select(genrePlaylist);
842
- onQueryChange?.("");
843
- },
844
- children: [
845
- genrePlaylist.name,
846
- genrePlaylist.isUmbrella && /* @__PURE__ */ (0, import_jsx_runtime9.jsx)("span", { className: "ml-2 text-xs italic text-[var(--color-text-muted)]", children: "umbrella" })
847
- ]
848
- }
849
- ) }, genrePlaylist.uuid)),
850
- extraResults && extraItems.length > 0 && /* @__PURE__ */ (0, import_jsx_runtime9.jsx)("li", { className: heading, children: extraResults.label }),
851
- extraItems.map((item) => /* @__PURE__ */ (0, import_jsx_runtime9.jsx)("li", { children: /* @__PURE__ */ (0, import_jsx_runtime9.jsx)(
852
- "button",
853
- {
854
- type: "button",
855
- className: "w-full px-3 py-1.5 [@media(pointer:coarse)]:min-h-[var(--layout-touch-target)] text-left hover:bg-[var(--color-surface-muted)]",
856
- onClick: () => {
857
- setQuery("");
858
- onQueryChange?.("");
859
- item.onSelect();
860
- },
861
- children: item.content
862
- }
863
- ) }, item.key))
864
823
  ] })
865
824
  ] });
866
825
  }
@@ -876,18 +835,18 @@ var import_lucide_react6 = require("lucide-react");
876
835
  var import_popup3 = require("@behindthemusictree/app-kit/popup");
877
836
 
878
837
  // src/genre-tree/SharePopup.tsx
879
- var import_react8 = require("react");
838
+ var import_react9 = require("react");
880
839
  var import_lucide_react5 = require("lucide-react");
881
840
  var import_fa = require("react-icons/fa");
882
841
  var import_si = require("react-icons/si");
883
842
 
884
843
  // src/popup/Toast.tsx
885
- var import_react7 = require("react");
844
+ var import_react8 = require("react");
886
845
  var import_react_dom2 = require("react-dom");
887
846
  var import_jsx_runtime10 = require("react/jsx-runtime");
888
847
  function Toast({ message, onClose, durationMs = 2200 }) {
889
- const [mounted, setMounted] = (0, import_react7.useState)(false);
890
- (0, import_react7.useEffect)(() => {
848
+ const [mounted, setMounted] = (0, import_react8.useState)(false);
849
+ (0, import_react8.useEffect)(() => {
891
850
  setMounted(true);
892
851
  const timeout = window.setTimeout(onClose, durationMs);
893
852
  return () => window.clearTimeout(timeout);
@@ -935,8 +894,8 @@ function ShareDestinationButton({
935
894
  var import_lucide_react4 = require("lucide-react");
936
895
  var import_jsx_runtime12 = require("react/jsx-runtime");
937
896
  function SharePreviewCard({ target, url }) {
938
- const Icon = target.kind === "genre" ? import_lucide_react4.Music2 : import_lucide_react4.UserRound;
939
- const typeLabel = target.kind === "genre" ? "Genre" : "Artist";
897
+ const Icon = { genre: import_lucide_react4.Music2, artist: import_lucide_react4.UserRound, song: import_lucide_react4.Music }[target.kind];
898
+ const typeLabel = { genre: "Genre", artist: "Artist", song: "Song" }[target.kind];
940
899
  return /* @__PURE__ */ (0, import_jsx_runtime12.jsx)("div", { className: "rounded-3xl bg-[var(--color-surface-muted)] p-4", children: /* @__PURE__ */ (0, import_jsx_runtime12.jsxs)("div", { className: "flex min-h-32 overflow-hidden rounded-2xl bg-[var(--color-surface)] shadow-lg", children: [
941
900
  /* @__PURE__ */ (0, import_jsx_runtime12.jsx)("div", { className: "flex w-28 shrink-0 items-center justify-center bg-[var(--color-primary)]/15 text-[var(--color-primary)]", children: /* @__PURE__ */ (0, import_jsx_runtime12.jsx)(Icon, { "aria-hidden": "true", className: "h-10 w-10" }) }),
942
901
  /* @__PURE__ */ (0, import_jsx_runtime12.jsxs)("div", { className: "flex min-w-0 flex-1 flex-col justify-center gap-1 p-4", children: [
@@ -981,20 +940,20 @@ async function copyText(text) {
981
940
  }
982
941
  }
983
942
  function SharePopup({ target, url, onClose }) {
984
- const [absoluteUrl, setAbsoluteUrl] = (0, import_react8.useState)(url);
985
- const [copyStatus, setCopyStatus] = (0, import_react8.useState)("idle");
986
- const [canUseNativeShare, setCanUseNativeShare] = (0, import_react8.useState)(false);
987
- const [shareError, setShareError] = (0, import_react8.useState)(null);
988
- const [toastId, setToastId] = (0, import_react8.useState)(0);
989
- (0, import_react8.useEffect)(() => {
943
+ const [absoluteUrl, setAbsoluteUrl] = (0, import_react9.useState)(url);
944
+ const [copyStatus, setCopyStatus] = (0, import_react9.useState)("idle");
945
+ const [canUseNativeShare, setCanUseNativeShare] = (0, import_react9.useState)(false);
946
+ const [shareError, setShareError] = (0, import_react9.useState)(null);
947
+ const [toastId, setToastId] = (0, import_react9.useState)(0);
948
+ (0, import_react9.useEffect)(() => {
990
949
  setAbsoluteUrl(resolveAbsoluteUrl(url) ?? "");
991
950
  setCopyStatus("idle");
992
951
  setShareError(null);
993
952
  }, [url]);
994
- (0, import_react8.useEffect)(() => {
953
+ (0, import_react9.useEffect)(() => {
995
954
  setCanUseNativeShare(typeof navigator !== "undefined" && typeof navigator.share === "function");
996
955
  }, []);
997
- const destinationUrls = (0, import_react8.useMemo)(() => {
956
+ const destinationUrls = (0, import_react9.useMemo)(() => {
998
957
  if (!absoluteUrl) return null;
999
958
  const message = `${target.name} \u2014 ${absoluteUrl}`;
1000
959
  return {
@@ -1012,7 +971,7 @@ function SharePopup({ target, url, onClose }) {
1012
971
  setCopyStatus(copied ? "idle" : "failed");
1013
972
  if (copied) setToastId((id) => id + 1);
1014
973
  };
1015
- const dismissToast = (0, import_react8.useCallback)(() => setToastId(0), []);
974
+ const dismissToast = (0, import_react9.useCallback)(() => setToastId(0), []);
1016
975
  const handleNativeShare = async () => {
1017
976
  if (!absoluteUrl || !navigator.share) return;
1018
977
  setShareError(null);
@@ -1218,13 +1177,13 @@ var import_ui5 = require("@behindthemusictree/ui");
1218
1177
  var import_genre_tree9 = require("@behindthemusictree/app-kit/genre-tree");
1219
1178
 
1220
1179
  // src/genre-tree/ClampedText.tsx
1221
- var import_react9 = require("react");
1180
+ var import_react10 = require("react");
1222
1181
  var import_jsx_runtime16 = require("react/jsx-runtime");
1223
1182
  function ClampedText({ children, className: className2 = "" }) {
1224
- const ref = (0, import_react9.useRef)(null);
1225
- const [expanded, setExpanded] = (0, import_react9.useState)(false);
1226
- const [overflows, setOverflows] = (0, import_react9.useState)(false);
1227
- (0, import_react9.useLayoutEffect)(() => {
1183
+ const ref = (0, import_react10.useRef)(null);
1184
+ const [expanded, setExpanded] = (0, import_react10.useState)(false);
1185
+ const [overflows, setOverflows] = (0, import_react10.useState)(false);
1186
+ (0, import_react10.useLayoutEffect)(() => {
1228
1187
  const element = ref.current;
1229
1188
  if (!element || expanded) return;
1230
1189
  setOverflows(element.scrollHeight > element.clientHeight);
@@ -1245,7 +1204,7 @@ function ClampedText({ children, className: className2 = "" }) {
1245
1204
  }
1246
1205
 
1247
1206
  // src/genre-tree/GenrePlaylistTracks.tsx
1248
- var import_react10 = require("react");
1207
+ var import_react11 = require("react");
1249
1208
  var import_react_query = require("@tanstack/react-query");
1250
1209
  var import_zod = require("zod");
1251
1210
  var import_fa2 = require("react-icons/fa");
@@ -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({
@@ -1313,6 +1272,7 @@ function GenrePlaylistTracks({
1313
1272
  scope,
1314
1273
  getBackendBaseUrl,
1315
1274
  renderArtistLink,
1275
+ renderSongLink,
1316
1276
  renderSongLeading,
1317
1277
  renderSongActions,
1318
1278
  renderTracksToolbar,
@@ -1322,7 +1282,7 @@ function GenrePlaylistTracks({
1322
1282
  const { session, sessionRestored } = (0, import_auth.useSession)();
1323
1283
  const { playerTrackObject, playState, handlePlayPauseAction } = (0, import_player4.usePlayer)();
1324
1284
  const { showPopup } = (0, import_popup4.usePopup)();
1325
- const trackNoun = (0, import_track_noun4.useTrackNoun)();
1285
+ const trackNoun = (0, import_track_noun.useTrackNoun)();
1326
1286
  const { uuid } = genrePlaylist;
1327
1287
  const { fetch } = (0, import_transport3.useFetchWrapper)(getBackendBaseUrl);
1328
1288
  const { data, isPending, isError, refetch, hasNextPage, isFetchingNextPage, fetchNextPage } = (0, import_react_query.useInfiniteQuery)({
@@ -1343,8 +1303,8 @@ function GenrePlaylistTracks({
1343
1303
  getNextPageParam: (lastPage) => lastPage.nextPage ?? void 0,
1344
1304
  enabled: scope !== "me" || sessionRestored && !!session?.accessToken
1345
1305
  });
1346
- const sentinelRef = (0, import_react10.useRef)(null);
1347
- (0, import_react10.useEffect)(() => {
1306
+ const sentinelRef = (0, import_react11.useRef)(null);
1307
+ (0, import_react11.useEffect)(() => {
1348
1308
  const sentinel = sentinelRef.current;
1349
1309
  if (!hasNextPage || isFetchingNextPage || !sentinel) return;
1350
1310
  const observer = new IntersectionObserver((entries) => {
@@ -1367,7 +1327,7 @@ function GenrePlaylistTracks({
1367
1327
  };
1368
1328
  return /* @__PURE__ */ (0, import_jsx_runtime18.jsxs)("div", { className: "gtv-info-panel-children", children: [
1369
1329
  /* @__PURE__ */ (0, import_jsx_runtime18.jsxs)("span", { className: "gtv-info-panel-children-title", children: [
1370
- (0, import_track_noun4.capitalize)(trackNoun.plural),
1330
+ (0, import_track_noun.capitalize)(trackNoun.plural),
1371
1331
  total !== void 0 && ` (${total})`
1372
1332
  ] }),
1373
1333
  renderTracksToolbar?.(),
@@ -1383,7 +1343,7 @@ function GenrePlaylistTracks({
1383
1343
  const isCurrent = playerTrackObject?.track.id === track.uuid;
1384
1344
  const isPlaying = isCurrent && playState === import_player5.PlayStates.PLAYING;
1385
1345
  const text = /* @__PURE__ */ (0, import_jsx_runtime18.jsxs)("span", { className: TEXT_CELL, children: [
1386
- /* @__PURE__ */ (0, import_jsx_runtime18.jsx)("span", { className: TITLE_CELL, children: (0, import_genre_tree6.isSong)(track) ? /* @__PURE__ */ (0, import_jsx_runtime18.jsx)(MusicBrainzRecordingLink, { song: track, className: "hover:underline focus-visible:underline", children: track.title }) : track.title }),
1346
+ /* @__PURE__ */ (0, import_jsx_runtime18.jsx)("span", { className: TITLE_CELL, children: renderSongLink ? renderSongLink(track) : (0, import_genre_tree6.isSong)(track) ? /* @__PURE__ */ (0, import_jsx_runtime18.jsx)(MusicBrainzRecordingLink, { song: track, className: "hover:underline focus-visible:underline", children: track.title }) : track.title }),
1387
1347
  track.artists?.length ? /* @__PURE__ */ (0, import_jsx_runtime18.jsx)("span", { className: SEPARATOR, children: " \u2014 " }) : null,
1388
1348
  /* @__PURE__ */ (0, import_jsx_runtime18.jsx)("span", { className: COLUMN_CELL, children: track.artists?.length ? /* @__PURE__ */ (0, import_jsx_runtime18.jsx)(import_genre_tree8.ArtistNames, { artists: track.artists, renderArtistLink }) : null }),
1389
1349
  track.album && /* @__PURE__ */ (0, import_jsx_runtime18.jsx)("span", { className: ALBUM_CELL, children: track.album.name })
@@ -1438,7 +1398,7 @@ function GenrePlaylistTracks({
1438
1398
  }
1439
1399
 
1440
1400
  // src/genre-tree/GenreDetailExtras.tsx
1441
- var import_track_noun5 = require("@behindthemusictree/app-kit/track-noun");
1401
+ var import_track_noun2 = require("@behindthemusictree/app-kit/track-noun");
1442
1402
  var import_si2 = require("react-icons/si");
1443
1403
  var import_jsx_runtime19 = require("react/jsx-runtime");
1444
1404
  function GenreDetailExtras({
@@ -1450,12 +1410,13 @@ function GenreDetailExtras({
1450
1410
  criteriaOverviewSchema,
1451
1411
  renderGenreDetailExtras,
1452
1412
  renderArtistLink,
1413
+ renderSongLink,
1453
1414
  renderSummary,
1454
1415
  renderEssentialTracks,
1455
1416
  family,
1456
1417
  ...trackSlots
1457
1418
  }) {
1458
- const trackNoun = (0, import_track_noun5.useTrackNoun)();
1419
+ const trackNoun = (0, import_track_noun2.useTrackNoun)();
1459
1420
  const { data: overview, isPending } = (0, import_genre_tree9.useFetchGenreOverview)(
1460
1421
  genreUuid,
1461
1422
  scope,
@@ -1527,6 +1488,7 @@ function GenreDetailExtras({
1527
1488
  scope,
1528
1489
  getBackendBaseUrl,
1529
1490
  renderArtistLink,
1491
+ renderSongLink,
1530
1492
  ...trackSlots
1531
1493
  }
1532
1494
  )
@@ -1535,7 +1497,7 @@ function GenreDetailExtras({
1535
1497
 
1536
1498
  // src/genre-tree/GenreInfoPanelBody.tsx
1537
1499
  var import_jsx_runtime20 = require("react/jsx-runtime");
1538
- var import_react11 = require("react");
1500
+ var import_react12 = require("react");
1539
1501
  function GenreNodeActionBand({ genrePlaylist, playback, ...bandProps }) {
1540
1502
  const { isPlaying, setIsPlaying } = (0, import_player6.usePlayer)();
1541
1503
  const isCurrentTrackList = playback.playingNodeId === genrePlaylist.uuid;
@@ -1572,7 +1534,7 @@ function GenreInfoPanelBody({
1572
1534
  return /* @__PURE__ */ (0, import_jsx_runtime20.jsxs)(import_jsx_runtime20.Fragment, { children: [
1573
1535
  toolbar,
1574
1536
  /* @__PURE__ */ (0, import_jsx_runtime20.jsx)(GenreNodeActionBand, { ...band, genrePlaylist, playback }),
1575
- /* @__PURE__ */ (0, import_react11.createElement)(
1537
+ /* @__PURE__ */ (0, import_react12.createElement)(
1576
1538
  GenreDetailExtras,
1577
1539
  {
1578
1540
  ...extras,
@@ -1588,66 +1550,10 @@ function GenreInfoPanelBody({
1588
1550
  ] });
1589
1551
  }
1590
1552
 
1591
- // src/genre-tree/PanelEdgeTab.tsx
1592
- var import_lucide_react8 = require("lucide-react");
1593
- var import_tailwind_merge = require("tailwind-merge");
1594
- var import_jsx_runtime21 = require("react/jsx-runtime");
1595
- 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)]";
1596
- function PanelEdgeTab({ side, edge, center, controls, className: className2 }) {
1597
- if (!edge && !center) return null;
1598
- const isLeft = side === "left";
1599
- const EdgeIcon = isLeft ? import_lucide_react8.ChevronLeft : import_lucide_react8.ChevronRight;
1600
- const CenterIcon = isLeft ? import_lucide_react8.ChevronRight : import_lucide_react8.ChevronLeft;
1601
- const renderButton = (action, icon) => /* @__PURE__ */ (0, import_jsx_runtime21.jsx)(
1602
- "button",
1603
- {
1604
- type: "button",
1605
- "aria-label": action.label,
1606
- title: action.label,
1607
- "aria-controls": controls,
1608
- "aria-expanded": action.expanded,
1609
- className: buttonClassName,
1610
- onClick: action.onClick,
1611
- children: icon
1612
- }
1613
- );
1614
- const edgeButton = edge && renderButton(edge, /* @__PURE__ */ (0, import_jsx_runtime21.jsx)(EdgeIcon, { "aria-hidden": "true", className: "h-5 w-5" }));
1615
- const centerButton = center && renderButton(center, /* @__PURE__ */ (0, import_jsx_runtime21.jsx)(CenterIcon, { "aria-hidden": "true", className: "h-5 w-5" }));
1616
- const buttons = isLeft ? [edgeButton, centerButton] : [centerButton, edgeButton];
1617
- return /* @__PURE__ */ (0, import_jsx_runtime21.jsxs)(
1618
- "div",
1619
- {
1620
- className: (0, import_tailwind_merge.twMerge)(
1621
- "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)]",
1622
- isLeft ? "rounded-r-lg" : "rounded-l-lg [clip-path:inset(-0.5rem_0_-0.5rem_-0.5rem)]",
1623
- className2
1624
- ),
1625
- children: [
1626
- buttons[0],
1627
- buttons[0] && buttons[1] && /* @__PURE__ */ (0, import_jsx_runtime21.jsx)("div", { role: "presentation", className: "h-px shrink-0 bg-[var(--color-border)]" }),
1628
- buttons[1]
1629
- ]
1630
- }
1631
- );
1632
- }
1633
-
1634
- // src/genre-tree/useLeftDetailPanelLayout.ts
1635
- var import_react12 = require("react");
1636
- function useLeftDetailPanelLayout(isPanelShown, enabled = true) {
1637
- (0, import_react12.useEffect)(() => {
1638
- if (!enabled || isPanelShown) return void 0;
1639
- const root = document.documentElement;
1640
- root.style.setProperty("--layout-left-w", "0px");
1641
- return () => {
1642
- root.style.removeProperty("--layout-left-w");
1643
- };
1644
- }, [enabled, isPanelShown]);
1645
- }
1646
-
1647
1553
  // src/genre-tree/ChangeParentPopup.tsx
1648
1554
  var import_react13 = require("react");
1649
1555
  var import_genre_tree10 = require("@behindthemusictree/app-kit/genre-tree");
1650
- var import_jsx_runtime22 = require("react/jsx-runtime");
1556
+ var import_jsx_runtime21 = require("react/jsx-runtime");
1651
1557
  function ChangeParentPopup({
1652
1558
  genrePlaylist,
1653
1559
  genrePlaylists,
@@ -1682,12 +1588,12 @@ function ChangeParentPopup({
1682
1588
  window.addEventListener("keydown", handleKeyDown);
1683
1589
  return () => window.removeEventListener("keydown", handleKeyDown);
1684
1590
  }, [onClose]);
1685
- return /* @__PURE__ */ (0, import_jsx_runtime22.jsxs)(BasePopup_default, { title: `Change parent of ${genrePlaylist.name}`, onClose, showCancelButton: true, fitContent: true, children: [
1686
- /* @__PURE__ */ (0, import_jsx_runtime22.jsxs)("p", { className: "mb-3 text-sm text-[var(--color-text-muted)]", children: [
1591
+ return /* @__PURE__ */ (0, import_jsx_runtime21.jsxs)(BasePopup_default, { title: `Change parent of ${genrePlaylist.name}`, onClose, showCancelButton: true, fitContent: true, children: [
1592
+ /* @__PURE__ */ (0, import_jsx_runtime21.jsxs)("p", { className: "mb-3 text-sm text-[var(--color-text-muted)]", children: [
1687
1593
  "Current parent: ",
1688
1594
  genrePlaylist.parent?.name ?? "none"
1689
1595
  ] }),
1690
- /* @__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)(
1596
+ /* @__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)(
1691
1597
  GenreSearch,
1692
1598
  {
1693
1599
  genrePlaylists: candidates,
@@ -1695,7 +1601,7 @@ function ChangeParentPopup({
1695
1601
  placeholder: "Search a new parent\u2026"
1696
1602
  }
1697
1603
  ) }),
1698
- 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(" ") })
1604
+ 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(" ") })
1699
1605
  ] });
1700
1606
  }
1701
1607
 
@@ -1707,12 +1613,12 @@ var import_genre_tree15 = require("@behindthemusictree/app-kit/genre-tree");
1707
1613
  var import_ui7 = require("@behindthemusictree/ui");
1708
1614
 
1709
1615
  // src/genre-tree/artist/ArtistLinks.tsx
1710
- var import_lucide_react9 = require("lucide-react");
1616
+ var import_lucide_react8 = require("lucide-react");
1711
1617
  var import_si3 = require("react-icons/si");
1712
1618
  var import_ui6 = require("@behindthemusictree/ui");
1713
- var import_jsx_runtime23 = require("react/jsx-runtime");
1619
+ var import_jsx_runtime22 = require("react/jsx-runtime");
1714
1620
  var LINK_KINDS = {
1715
- official: { label: "Official website", Icon: import_lucide_react9.Globe },
1621
+ official: { label: "Official website", Icon: import_lucide_react8.Globe },
1716
1622
  wikipedia: { label: "Wikipedia", Icon: import_si3.SiWikipedia },
1717
1623
  wikidata: { label: "Wikidata", Icon: import_si3.SiWikidata },
1718
1624
  spotify: { label: "Spotify", Icon: import_si3.SiSpotify },
@@ -1720,7 +1626,7 @@ var LINK_KINDS = {
1720
1626
  bandcamp: { label: "Bandcamp", Icon: import_si3.SiBandcamp },
1721
1627
  soundcloud: { label: "SoundCloud", Icon: import_si3.SiSoundcloud },
1722
1628
  discogs: { label: "Discogs", Icon: import_si3.SiDiscogs },
1723
- social: { label: "Social", Icon: import_lucide_react9.Share2 }
1629
+ social: { label: "Social", Icon: import_lucide_react8.Share2 }
1724
1630
  };
1725
1631
  function socialLabel(url) {
1726
1632
  try {
@@ -1747,7 +1653,7 @@ function artistLinks(artist) {
1747
1653
  return links;
1748
1654
  }
1749
1655
  function ArtistLinks({ links }) {
1750
- 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)) });
1656
+ 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)) });
1751
1657
  }
1752
1658
 
1753
1659
  // src/genre-tree/artist/formatYears.ts
@@ -1758,7 +1664,7 @@ function formatYears(beginYear, endYear, ended) {
1758
1664
  }
1759
1665
 
1760
1666
  // src/genre-tree/artist/ArtistHeader.tsx
1761
- var import_jsx_runtime24 = require("react/jsx-runtime");
1667
+ var import_jsx_runtime23 = require("react/jsx-runtime");
1762
1668
  var countryNames = new Intl.DisplayNames(["en"], { type: "region" });
1763
1669
  function ArtistHeader({ artist }) {
1764
1670
  const fields = [];
@@ -1768,35 +1674,35 @@ function ArtistHeader({ artist }) {
1768
1674
  const years = formatYears(artist.beginYear, artist.endYear, artist.ended);
1769
1675
  if (years) fields.push({ label: "Active", value: years });
1770
1676
  const links = artistLinks(artist);
1771
- if (links.length > 0) fields.push({ label: "Links", value: /* @__PURE__ */ (0, import_jsx_runtime24.jsx)(ArtistLinks, { links }) });
1677
+ if (links.length > 0) fields.push({ label: "Links", value: /* @__PURE__ */ (0, import_jsx_runtime23.jsx)(ArtistLinks, { links }) });
1772
1678
  if (fields.length === 0) return null;
1773
- return /* @__PURE__ */ (0, import_jsx_runtime24.jsx)(import_ui7.FieldList, { items: fields });
1679
+ return /* @__PURE__ */ (0, import_jsx_runtime23.jsx)(import_ui7.FieldList, { items: fields });
1774
1680
  }
1775
1681
 
1776
1682
  // src/genre-tree/artist/ArtistPhoto.tsx
1777
- var import_jsx_runtime25 = require("react/jsx-runtime");
1683
+ var import_jsx_runtime24 = require("react/jsx-runtime");
1778
1684
  function ExternalLink({ href, children }) {
1779
- return /* @__PURE__ */ (0, import_jsx_runtime25.jsx)("a", { href, target: "_blank", rel: "noopener noreferrer", className: "underline-offset-2 hover:underline", children });
1685
+ return /* @__PURE__ */ (0, import_jsx_runtime24.jsx)("a", { href, target: "_blank", rel: "noopener noreferrer", className: "underline-offset-2 hover:underline", children });
1780
1686
  }
1781
1687
  function ArtistPhoto({ image, name }) {
1782
- return /* @__PURE__ */ (0, import_jsx_runtime25.jsxs)("figure", { className: "flex flex-col gap-1.5 px-6 py-2.5", children: [
1783
- /* @__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" }),
1784
- /* @__PURE__ */ (0, import_jsx_runtime25.jsxs)("figcaption", { className: "text-xs text-[var(--color-text-muted)]", children: [
1688
+ return /* @__PURE__ */ (0, import_jsx_runtime24.jsxs)("figure", { className: "flex flex-col gap-1.5 px-6 py-2.5", children: [
1689
+ /* @__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" }),
1690
+ /* @__PURE__ */ (0, import_jsx_runtime24.jsxs)("figcaption", { className: "text-xs text-[var(--color-text-muted)]", children: [
1785
1691
  "Photo: ",
1786
- /* @__PURE__ */ (0, import_jsx_runtime25.jsx)(ExternalLink, { href: image.pageUrl, children: image.author ?? "Wikimedia Commons" }),
1692
+ /* @__PURE__ */ (0, import_jsx_runtime24.jsx)(ExternalLink, { href: image.pageUrl, children: image.author ?? "Wikimedia Commons" }),
1787
1693
  ",",
1788
1694
  " ",
1789
- image.licenseUrl ? /* @__PURE__ */ (0, import_jsx_runtime25.jsx)(ExternalLink, { href: image.licenseUrl, children: image.license }) : image.license
1695
+ image.licenseUrl ? /* @__PURE__ */ (0, import_jsx_runtime24.jsx)(ExternalLink, { href: image.licenseUrl, children: image.license }) : image.license
1790
1696
  ] })
1791
1697
  ] });
1792
1698
  }
1793
1699
 
1794
1700
  // src/genre-tree/artist/ArtistMembers.tsx
1795
1701
  var import_ui8 = require("@behindthemusictree/ui");
1796
- var import_jsx_runtime26 = require("react/jsx-runtime");
1702
+ var import_jsx_runtime25 = require("react/jsx-runtime");
1797
1703
  function MembershipList({ title, memberships, renderArtistLink }) {
1798
1704
  if (memberships.length === 0) return null;
1799
- return /* @__PURE__ */ (0, import_jsx_runtime26.jsx)(import_ui8.PanelGroup, { title, children: /* @__PURE__ */ (0, import_jsx_runtime26.jsx)("ul", { children: memberships.map((membership, index) => {
1705
+ return /* @__PURE__ */ (0, import_jsx_runtime25.jsx)(import_ui8.PanelGroup, { title, children: /* @__PURE__ */ (0, import_jsx_runtime25.jsx)("ul", { children: memberships.map((membership, index) => {
1800
1706
  const details = [
1801
1707
  formatYears(membership.beginYear, membership.endYear, membership.ended),
1802
1708
  ...membership.roles
@@ -1804,9 +1710,9 @@ function MembershipList({ title, memberships, renderArtistLink }) {
1804
1710
  const detailsText = details.filter(Boolean).join(" \xB7 ");
1805
1711
  return (
1806
1712
  // A member can have several periods in the same group.
1807
- /* @__PURE__ */ (0, import_jsx_runtime26.jsxs)("li", { className: "truncate py-0.5", title: detailsText || void 0, children: [
1808
- /* @__PURE__ */ (0, import_jsx_runtime26.jsx)("span", { className: "font-medium text-[var(--color-text)]", children: renderArtistLink(membership.artist) }),
1809
- detailsText && /* @__PURE__ */ (0, import_jsx_runtime26.jsxs)("span", { className: "text-[var(--color-text-muted)]", children: [
1713
+ /* @__PURE__ */ (0, import_jsx_runtime25.jsxs)("li", { className: "truncate py-0.5", title: detailsText || void 0, children: [
1714
+ /* @__PURE__ */ (0, import_jsx_runtime25.jsx)("span", { className: "font-medium text-[var(--color-text)]", children: renderArtistLink(membership.artist) }),
1715
+ detailsText && /* @__PURE__ */ (0, import_jsx_runtime25.jsxs)("span", { className: "text-[var(--color-text-muted)]", children: [
1810
1716
  " \u2014 ",
1811
1717
  detailsText
1812
1718
  ] })
@@ -1817,9 +1723,9 @@ function MembershipList({ title, memberships, renderArtistLink }) {
1817
1723
  function ArtistMembers({ title, memberships, renderArtistLink }) {
1818
1724
  if (memberships.length === 0) return null;
1819
1725
  const isCurrent = (membership) => !membership.ended && membership.endYear === null;
1820
- return /* @__PURE__ */ (0, import_jsx_runtime26.jsxs)(import_ui8.PanelSection, { title, children: [
1821
- /* @__PURE__ */ (0, import_jsx_runtime26.jsx)(MembershipList, { title: "Current", memberships: memberships.filter(isCurrent), renderArtistLink }),
1822
- /* @__PURE__ */ (0, import_jsx_runtime26.jsx)(
1726
+ return /* @__PURE__ */ (0, import_jsx_runtime25.jsxs)(import_ui8.PanelSection, { title, children: [
1727
+ /* @__PURE__ */ (0, import_jsx_runtime25.jsx)(MembershipList, { title: "Current", memberships: memberships.filter(isCurrent), renderArtistLink }),
1728
+ /* @__PURE__ */ (0, import_jsx_runtime25.jsx)(
1823
1729
  MembershipList,
1824
1730
  {
1825
1731
  title: "Past",
@@ -1832,7 +1738,7 @@ function ArtistMembers({ title, memberships, renderArtistLink }) {
1832
1738
 
1833
1739
  // src/genre-tree/artist/ArtistDiscography.tsx
1834
1740
  var import_ui9 = require("@behindthemusictree/ui");
1835
- var import_jsx_runtime27 = require("react/jsx-runtime");
1741
+ var import_jsx_runtime26 = require("react/jsx-runtime");
1836
1742
  var GROUPS = ["Albums", "EPs", "Singles", "Live", "Compilations", "Other"];
1837
1743
  var PRIMARY_GROUPS = { album: "Albums", ep: "EPs", single: "Singles" };
1838
1744
  function discographyGroup({ primaryType, secondaryTypes }) {
@@ -1852,12 +1758,12 @@ function byYear(a, b) {
1852
1758
  function ArtistDiscography({ albums }) {
1853
1759
  if (albums.length === 0) return null;
1854
1760
  const sorted = [...albums].sort(byYear);
1855
- return /* @__PURE__ */ (0, import_jsx_runtime27.jsx)(import_ui9.PanelSection, { title: "Discography", children: GROUPS.map((group) => {
1761
+ return /* @__PURE__ */ (0, import_jsx_runtime26.jsx)(import_ui9.PanelSection, { title: "Discography", children: GROUPS.map((group) => {
1856
1762
  const groupAlbums = sorted.filter((album) => discographyGroup(album) === group);
1857
1763
  if (groupAlbums.length === 0) return null;
1858
- 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: [
1859
- /* @__PURE__ */ (0, import_jsx_runtime27.jsx)("span", { className: "tabular-nums text-[var(--color-text-muted)]", children: album.year ?? "\u2014" }),
1860
- /* @__PURE__ */ (0, import_jsx_runtime27.jsx)("span", { className: "truncate text-[var(--color-text)]", title: album.name, children: album.name })
1764
+ 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: [
1765
+ /* @__PURE__ */ (0, import_jsx_runtime26.jsx)("span", { className: "tabular-nums text-[var(--color-text-muted)]", children: album.year ?? "\u2014" }),
1766
+ /* @__PURE__ */ (0, import_jsx_runtime26.jsx)("span", { className: "truncate text-[var(--color-text)]", title: album.name, children: album.name })
1861
1767
  ] }, album.uuid)) }) }, group);
1862
1768
  }) });
1863
1769
  }
@@ -1865,7 +1771,7 @@ function ArtistDiscography({ albums }) {
1865
1771
  // src/genre-tree/artist/ArtistSongs.tsx
1866
1772
  var import_react14 = require("react");
1867
1773
  var import_react_query2 = require("@tanstack/react-query");
1868
- var import_lucide_react10 = require("lucide-react");
1774
+ var import_lucide_react9 = require("lucide-react");
1869
1775
  var import_ui10 = require("@behindthemusictree/ui");
1870
1776
  var import_player10 = require("@behindthemusictree/app-kit/player");
1871
1777
  var import_genre_tree14 = require("@behindthemusictree/app-kit/genre-tree");
@@ -1879,7 +1785,7 @@ var import_genre_tree11 = require("@behindthemusictree/app-kit/genre-tree");
1879
1785
  var import_fa3 = require("react-icons/fa");
1880
1786
  var import_player7 = require("@behindthemusictree/app-kit/player");
1881
1787
  var import_player8 = require("@behindthemusictree/app-kit/player");
1882
- var import_jsx_runtime28 = require("react/jsx-runtime");
1788
+ var import_jsx_runtime27 = require("react/jsx-runtime");
1883
1789
  function TrackPositionPlayPause({
1884
1790
  position,
1885
1791
  uuid,
@@ -1893,14 +1799,14 @@ function TrackPositionPlayPause({
1893
1799
  const isPlaying = isThisTrack && isConfirmedPlayable && playState === import_player8.PlayStates.PLAYING;
1894
1800
  const isPaused = isThisTrack && isConfirmedPlayable && playState === import_player8.PlayStates.PAUSED;
1895
1801
  const isCurrentTrackActive = isPlaying || isPaused;
1896
- return /* @__PURE__ */ (0, import_jsx_runtime28.jsxs)(
1802
+ return /* @__PURE__ */ (0, import_jsx_runtime27.jsxs)(
1897
1803
  "div",
1898
1804
  {
1899
1805
  className: "track-position-play-pause relative flex items-center justify-center gap-2 text-lg w-16 cursor-pointer",
1900
1806
  style: { minWidth: "4rem", maxWidth: "4rem" },
1901
1807
  onClick: handlePlayPauseClick,
1902
1808
  children: [
1903
- /* @__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)(
1809
+ /* @__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)(
1904
1810
  "span",
1905
1811
  {
1906
1812
  className: "track-position-play-pause__bars",
@@ -1908,18 +1814,18 @@ function TrackPositionPlayPause({
1908
1814
  role: "img",
1909
1815
  "aria-label": isPlaying ? "Playing" : "Paused",
1910
1816
  children: [
1911
- /* @__PURE__ */ (0, import_jsx_runtime28.jsx)("span", { className: "track-position-play-pause__bar" }),
1912
- /* @__PURE__ */ (0, import_jsx_runtime28.jsx)("span", { className: "track-position-play-pause__bar" }),
1913
- /* @__PURE__ */ (0, import_jsx_runtime28.jsx)("span", { className: "track-position-play-pause__bar" })
1817
+ /* @__PURE__ */ (0, import_jsx_runtime27.jsx)("span", { className: "track-position-play-pause__bar" }),
1818
+ /* @__PURE__ */ (0, import_jsx_runtime27.jsx)("span", { className: "track-position-play-pause__bar" }),
1819
+ /* @__PURE__ */ (0, import_jsx_runtime27.jsx)("span", { className: "track-position-play-pause__bar" })
1914
1820
  ]
1915
1821
  }
1916
1822
  ) : position }),
1917
- /* @__PURE__ */ (0, import_jsx_runtime28.jsx)(
1823
+ /* @__PURE__ */ (0, import_jsx_runtime27.jsx)(
1918
1824
  "div",
1919
1825
  {
1920
1826
  className: `hidden group-hover:flex items-center justify-center ${isCurrentTrackActive ? "" : "absolute inset-0"}`,
1921
1827
  "aria-hidden": "true",
1922
- 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, {})
1828
+ 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, {})
1923
1829
  }
1924
1830
  )
1925
1831
  ]
@@ -1930,13 +1836,14 @@ function TrackPositionPlayPause({
1930
1836
  // src/genre-tree/track-list-sidebar/TrackItem.tsx
1931
1837
  var import_genre_tree12 = require("@behindthemusictree/app-kit/genre-tree");
1932
1838
  var import_genre_tree13 = require("@behindthemusictree/app-kit/genre-tree");
1933
- var import_jsx_runtime29 = require("react/jsx-runtime");
1839
+ var import_jsx_runtime28 = require("react/jsx-runtime");
1934
1840
  function TrackItem({
1935
1841
  track,
1936
1842
  position,
1937
1843
  renderDuration,
1938
1844
  renderActions,
1939
- renderArtistLink
1845
+ renderArtistLink,
1846
+ renderSongLink
1940
1847
  }) {
1941
1848
  const { handlePlayPauseAction, playerTrackObject, loadTrackForPlayer } = (0, import_player9.usePlayer)();
1942
1849
  const { trackList, selectedTrack, toTrackAtPosition, runtimeUnplayableTrackIds } = (0, import_genre_tree11.useTrackList)();
@@ -1953,12 +1860,12 @@ function TrackItem({
1953
1860
  loadTrackForPlayer(track.uuid);
1954
1861
  }
1955
1862
  };
1956
- return /* @__PURE__ */ (0, import_jsx_runtime29.jsxs)(
1863
+ return /* @__PURE__ */ (0, import_jsx_runtime28.jsxs)(
1957
1864
  "div",
1958
1865
  {
1959
1866
  className: `track-item flex h-14 text-[var(--color-text)] ${isUnplayable ? "opacity-50" : "hover:bg-[var(--color-surface-muted)] group"}`,
1960
1867
  children: [
1961
- isUnplayable ? /* @__PURE__ */ (0, import_jsx_runtime29.jsx)(
1868
+ isUnplayable ? /* @__PURE__ */ (0, import_jsx_runtime28.jsx)(
1962
1869
  "div",
1963
1870
  {
1964
1871
  className: "flex items-center justify-center text-lg w-16",
@@ -1966,7 +1873,7 @@ function TrackItem({
1966
1873
  "aria-disabled": "true",
1967
1874
  children: position
1968
1875
  }
1969
- ) : /* @__PURE__ */ (0, import_jsx_runtime29.jsx)(
1876
+ ) : /* @__PURE__ */ (0, import_jsx_runtime28.jsx)(
1970
1877
  TrackPositionPlayPause,
1971
1878
  {
1972
1879
  position,
@@ -1975,12 +1882,12 @@ function TrackItem({
1975
1882
  isSelected: selectedTrack?.uuid === track.uuid
1976
1883
  }
1977
1884
  ),
1978
- /* @__PURE__ */ (0, import_jsx_runtime29.jsxs)("div", { className: "title-artist-container flex flex-col items-start justify-center min-w-0 flex-1", children: [
1979
- /* @__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: (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 }) }),
1980
- 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 }) }) : ""
1885
+ /* @__PURE__ */ (0, import_jsx_runtime28.jsxs)("div", { className: "title-artist-container flex flex-col items-start justify-center min-w-0 flex-1", children: [
1886
+ /* @__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 }) }),
1887
+ 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 }) }) : ""
1981
1888
  ] }),
1982
- /* @__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 }) : "" }),
1983
- renderDuration && /* @__PURE__ */ (0, import_jsx_runtime29.jsx)(
1889
+ /* @__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 }) : "" }),
1890
+ renderDuration && /* @__PURE__ */ (0, import_jsx_runtime28.jsx)(
1984
1891
  "div",
1985
1892
  {
1986
1893
  className: "duration flex text-base w-16 items-center justify-center",
@@ -1988,20 +1895,20 @@ function TrackItem({
1988
1895
  children: renderDuration(track)
1989
1896
  }
1990
1897
  ),
1991
- 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) })
1898
+ 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) })
1992
1899
  ]
1993
1900
  }
1994
1901
  );
1995
1902
  }
1996
1903
 
1997
1904
  // src/genre-tree/artist/ArtistSongs.tsx
1998
- var import_jsx_runtime30 = require("react/jsx-runtime");
1905
+ var import_jsx_runtime29 = require("react/jsx-runtime");
1999
1906
  var SongPageSchema2 = (0, import_transport7.PaginatedResponseSchema)(import_genre_tree14.SongDetailedSchema);
2000
1907
  var SORTS = [
2001
1908
  { ordering: "title", label: "Title" },
2002
1909
  { ordering: "-albumYear", label: "Year" }
2003
1910
  ];
2004
- function ArtistSongs({ artist, getBackendBaseUrl, renderArtistLink }) {
1911
+ function ArtistSongs({ artist, getBackendBaseUrl, renderArtistLink, renderSongLink }) {
2005
1912
  const [page, setPage] = (0, import_react14.useState)(1);
2006
1913
  const [ordering, setOrdering] = (0, import_react14.useState)("title");
2007
1914
  const { fetch } = (0, import_transport7.useFetchWrapper)(getBackendBaseUrl);
@@ -2028,21 +1935,21 @@ function ArtistSongs({ artist, getBackendBaseUrl, renderArtistLink }) {
2028
1935
  event.stopPropagation();
2029
1936
  void play(song.uuid);
2030
1937
  };
2031
- 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: [
2032
- /* @__PURE__ */ (0, import_jsx_runtime30.jsxs)("div", { className: "flex flex-wrap items-center justify-between gap-2 mb-2", children: [
2033
- /* @__PURE__ */ (0, import_jsx_runtime30.jsxs)(
1938
+ 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: [
1939
+ /* @__PURE__ */ (0, import_jsx_runtime29.jsxs)("div", { className: "flex flex-wrap items-center justify-between gap-2 mb-2", children: [
1940
+ /* @__PURE__ */ (0, import_jsx_runtime29.jsxs)(
2034
1941
  "button",
2035
1942
  {
2036
1943
  type: "button",
2037
1944
  onClick: () => void play(),
2038
1945
  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",
2039
1946
  children: [
2040
- /* @__PURE__ */ (0, import_jsx_runtime30.jsx)(import_lucide_react10.Play, { className: "h-3.5 w-3.5", "aria-hidden": "true" }),
1947
+ /* @__PURE__ */ (0, import_jsx_runtime29.jsx)(import_lucide_react9.Play, { className: "h-3.5 w-3.5", "aria-hidden": "true" }),
2041
1948
  "Play all"
2042
1949
  ]
2043
1950
  }
2044
1951
  ),
2045
- /* @__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)(
1952
+ /* @__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)(
2046
1953
  "button",
2047
1954
  {
2048
1955
  type: "button",
@@ -2057,26 +1964,28 @@ function ArtistSongs({ artist, getBackendBaseUrl, renderArtistLink }) {
2057
1964
  sort.ordering
2058
1965
  )) })
2059
1966
  ] }),
2060
- /* @__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)(
1967
+ /* @__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)(
2061
1968
  TrackItem,
2062
1969
  {
2063
1970
  track: song,
2064
1971
  position: (page - 1) * data.pageSize + index + 1,
2065
- renderArtistLink
1972
+ renderArtistLink,
1973
+ renderSongLink
2066
1974
  }
2067
1975
  ) }, song.uuid)) }),
2068
- /* @__PURE__ */ (0, import_jsx_runtime30.jsx)(import_ui10.Pagination, { currentPage: page, totalPages: data.totalPages, onPageChange: setPage })
1976
+ /* @__PURE__ */ (0, import_jsx_runtime29.jsx)(import_ui10.Pagination, { currentPage: page, totalPages: data.totalPages, onPageChange: setPage })
2069
1977
  ] }) });
2070
1978
  }
2071
1979
 
2072
1980
  // src/genre-tree/artist/useArtistPanel.tsx
2073
- var import_jsx_runtime31 = require("react/jsx-runtime");
1981
+ var import_jsx_runtime30 = require("react/jsx-runtime");
2074
1982
  function useArtistPanel({
2075
1983
  uuid,
2076
1984
  scope,
2077
1985
  getBackendBaseUrl,
2078
1986
  getShareUrl,
2079
1987
  renderArtistLink,
1988
+ renderSongLink,
2080
1989
  expanded,
2081
1990
  onExpandedChange,
2082
1991
  onClose
@@ -2090,26 +1999,26 @@ function useArtistPanel({
2090
1999
  return {
2091
2000
  ...panel,
2092
2001
  title: "Artist",
2093
- content: /* @__PURE__ */ (0, import_jsx_runtime31.jsxs)("div", { "aria-busy": "true", className: "flex flex-col gap-2.5 px-6 py-2.5", children: [
2094
- /* @__PURE__ */ (0, import_jsx_runtime31.jsx)(import_ui11.Skeleton, { className: "h-3 w-16" }),
2095
- /* @__PURE__ */ (0, import_jsx_runtime31.jsx)(import_ui11.Skeleton, { className: "h-4 w-32" }),
2096
- /* @__PURE__ */ (0, import_jsx_runtime31.jsx)(import_ui11.Skeleton, { className: "h-3 w-16" }),
2097
- /* @__PURE__ */ (0, import_jsx_runtime31.jsx)(import_ui11.Skeleton, { className: "h-4 w-24" }),
2098
- /* @__PURE__ */ (0, import_jsx_runtime31.jsx)(import_ui11.Skeleton, { className: "h-16 mt-2.5" }),
2099
- /* @__PURE__ */ (0, import_jsx_runtime31.jsx)(import_ui11.Skeleton, { className: "h-16" })
2002
+ content: /* @__PURE__ */ (0, import_jsx_runtime30.jsxs)("div", { "aria-busy": "true", className: "flex flex-col gap-2.5 px-6 py-2.5", children: [
2003
+ /* @__PURE__ */ (0, import_jsx_runtime30.jsx)(import_ui11.Skeleton, { className: "h-3 w-16" }),
2004
+ /* @__PURE__ */ (0, import_jsx_runtime30.jsx)(import_ui11.Skeleton, { className: "h-4 w-32" }),
2005
+ /* @__PURE__ */ (0, import_jsx_runtime30.jsx)(import_ui11.Skeleton, { className: "h-3 w-16" }),
2006
+ /* @__PURE__ */ (0, import_jsx_runtime30.jsx)(import_ui11.Skeleton, { className: "h-4 w-24" }),
2007
+ /* @__PURE__ */ (0, import_jsx_runtime30.jsx)(import_ui11.Skeleton, { className: "h-16 mt-2.5" }),
2008
+ /* @__PURE__ */ (0, import_jsx_runtime30.jsx)(import_ui11.Skeleton, { className: "h-16" })
2100
2009
  ] })
2101
2010
  };
2102
2011
  case "not-found":
2103
2012
  return {
2104
2013
  ...panel,
2105
2014
  title: "Artist not found",
2106
- 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." })
2015
+ 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." })
2107
2016
  };
2108
2017
  case "error":
2109
2018
  return {
2110
2019
  ...panel,
2111
2020
  title: "Artist",
2112
- content: /* @__PURE__ */ (0, import_jsx_runtime31.jsx)("p", { className: "px-6 py-2.5 text-red-600", children: "Could not load this artist." })
2021
+ content: /* @__PURE__ */ (0, import_jsx_runtime30.jsx)("p", { className: "px-6 py-2.5 text-red-600", children: "Could not load this artist." })
2113
2022
  };
2114
2023
  case "success": {
2115
2024
  const { artist } = result;
@@ -2118,63 +2027,251 @@ function useArtistPanel({
2118
2027
  return {
2119
2028
  ...panel,
2120
2029
  title: artist.name,
2121
- content: /* @__PURE__ */ (0, import_jsx_runtime31.jsxs)(import_jsx_runtime31.Fragment, { children: [
2122
- 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 }) }) }),
2123
- artist.image && /* @__PURE__ */ (0, import_jsx_runtime31.jsx)(ArtistPhoto, { image: artist.image, name: artist.name }),
2124
- /* @__PURE__ */ (0, import_jsx_runtime31.jsx)(ArtistHeader, { artist }),
2125
- /* @__PURE__ */ (0, import_jsx_runtime31.jsx)(ArtistMembers, { title: "Members", memberships: artist.members, renderArtistLink }),
2126
- /* @__PURE__ */ (0, import_jsx_runtime31.jsx)(ArtistMembers, { title: "Member of", memberships: artist.memberOf, renderArtistLink }),
2127
- artist.tracksCount > 0 && /* @__PURE__ */ (0, import_jsx_runtime31.jsx)(
2030
+ content: /* @__PURE__ */ (0, import_jsx_runtime30.jsxs)(import_jsx_runtime30.Fragment, { children: [
2031
+ 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 }) }) }),
2032
+ artist.image && /* @__PURE__ */ (0, import_jsx_runtime30.jsx)(ArtistPhoto, { image: artist.image, name: artist.name }),
2033
+ /* @__PURE__ */ (0, import_jsx_runtime30.jsx)(ArtistHeader, { artist }),
2034
+ /* @__PURE__ */ (0, import_jsx_runtime30.jsx)(ArtistMembers, { title: "Members", memberships: artist.members, renderArtistLink }),
2035
+ /* @__PURE__ */ (0, import_jsx_runtime30.jsx)(ArtistMembers, { title: "Member of", memberships: artist.memberOf, renderArtistLink }),
2036
+ artist.tracksCount > 0 && /* @__PURE__ */ (0, import_jsx_runtime30.jsx)(
2128
2037
  ArtistSongs,
2129
2038
  {
2130
2039
  artist: { uuid: artist.uuid, name: artist.name },
2131
2040
  getBackendBaseUrl,
2132
- renderArtistLink
2041
+ renderArtistLink,
2042
+ renderSongLink
2133
2043
  }
2134
2044
  )
2135
2045
  ] }),
2136
- extraContent: artist.albums.length > 0 ? /* @__PURE__ */ (0, import_jsx_runtime31.jsx)(ArtistDiscography, { albums: artist.albums }) : void 0
2046
+ extraContent: artist.albums.length > 0 ? /* @__PURE__ */ (0, import_jsx_runtime30.jsx)(ArtistDiscography, { albums: artist.albums }) : void 0
2137
2047
  };
2138
2048
  }
2139
2049
  }
2140
2050
  }
2141
2051
 
2142
- // src/genre-tree/SizeMetricToggle.tsx
2143
- var import_md = require("react-icons/md");
2052
+ // src/genre-tree/song/useSongPanel.tsx
2053
+ var import_ui13 = require("@behindthemusictree/ui");
2054
+ var import_genre_tree17 = require("@behindthemusictree/app-kit/genre-tree");
2055
+
2056
+ // src/genre-tree/song/SongPanelContent.tsx
2057
+ var import_react15 = require("react");
2058
+ var import_lucide_react10 = require("lucide-react");
2144
2059
  var import_ui12 = require("@behindthemusictree/ui");
2060
+ var import_player11 = require("@behindthemusictree/app-kit/player");
2061
+ var import_genre_tree16 = require("@behindthemusictree/app-kit/genre-tree");
2062
+ var import_jsx_runtime31 = require("react/jsx-runtime");
2063
+ var numberFormat = new Intl.NumberFormat("en");
2064
+ var dateFormat = new Intl.DateTimeFormat("en", { dateStyle: "medium" });
2065
+ var externalLink = "text-[var(--color-primary)] hover:underline focus-visible:underline";
2066
+ function SongHistory({ uuid, getBackendBaseUrl }) {
2067
+ const [open, setOpen] = (0, import_react15.useState)(false);
2068
+ const history = (0, import_genre_tree16.useSongHistory)(uuid, getBackendBaseUrl, open);
2069
+ let body = null;
2070
+ if (history.status === "loading") body = /* @__PURE__ */ (0, import_jsx_runtime31.jsx)(import_ui12.Skeleton, { className: "h-10", "aria-busy": "true" });
2071
+ else if (history.status === "error") body = /* @__PURE__ */ (0, import_jsx_runtime31.jsx)("p", { className: "text-red-600", children: "Could not load the history." });
2072
+ else if (history.status === "success")
2073
+ 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: [
2074
+ /* @__PURE__ */ (0, import_jsx_runtime31.jsx)("span", { className: "font-medium", children: entry.action }),
2075
+ entry.oldValue !== null || entry.newValue !== null ? /* @__PURE__ */ (0, import_jsx_runtime31.jsxs)("span", { className: "text-[var(--color-text-muted)]", children: [
2076
+ " ",
2077
+ "\u2014 ",
2078
+ entry.oldValue ?? "none",
2079
+ " \u2192 ",
2080
+ entry.newValue ?? "none"
2081
+ ] }) : null,
2082
+ /* @__PURE__ */ (0, import_jsx_runtime31.jsxs)("span", { className: "block text-xs text-[var(--color-text-muted)]", children: [
2083
+ entry.actorPseudo ?? "Anonymous",
2084
+ " \xB7 ",
2085
+ dateFormat.format(new Date(entry.createdOn))
2086
+ ] })
2087
+ ] }, entry.uuid)) });
2088
+ return /* @__PURE__ */ (0, import_jsx_runtime31.jsxs)(
2089
+ "details",
2090
+ {
2091
+ onToggle: (event) => setOpen(event.currentTarget.open),
2092
+ className: "px-6 pb-2.5 mt-2.5 border-t border-[var(--color-border)]",
2093
+ children: [
2094
+ /* @__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" }),
2095
+ /* @__PURE__ */ (0, import_jsx_runtime31.jsx)("div", { className: "mt-1.5", children: body })
2096
+ ]
2097
+ }
2098
+ );
2099
+ }
2100
+ function SongPanelContent({
2101
+ song,
2102
+ scope,
2103
+ getBackendBaseUrl,
2104
+ shareUrl,
2105
+ renderArtistLink,
2106
+ genrePath,
2107
+ onSelectGenre
2108
+ }) {
2109
+ const { playNewTrackListFromTrackUuid } = (0, import_genre_tree16.useTrackList)();
2110
+ const { playerTrackObject, playState, handlePlayPauseAction } = (0, import_player11.usePlayer)();
2111
+ const unplayableReason = (0, import_genre_tree16.songUnplayableReason)(song);
2112
+ const isCurrent = playerTrackObject?.track.id === song.uuid;
2113
+ const isPlaying = isCurrent && playState === import_player11.PlayStates.PLAYING;
2114
+ const shareTarget = { kind: "song", uuid: song.uuid, name: song.title, scope };
2115
+ const musicBrainzUrl = (0, import_genre_tree16.musicBrainzRecordingUrl)(song);
2116
+ const fields = [];
2117
+ if (song.genre) {
2118
+ const path = genrePath.length > 0 ? genrePath : null;
2119
+ fields.push({
2120
+ label: "Genre",
2121
+ 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: [
2122
+ index > 0 && /* @__PURE__ */ (0, import_jsx_runtime31.jsx)("span", { "aria-hidden": "true", children: "\u203A " }),
2123
+ /* @__PURE__ */ (0, import_jsx_runtime31.jsx)("button", { type: "button", onClick: () => onSelectGenre(genrePlaylist), className: "hover:underline", children: genrePlaylist.name })
2124
+ ] }, genrePlaylist.uuid)) }) : song.genre.name
2125
+ });
2126
+ } else {
2127
+ fields.push({ label: "Genre", value: /* @__PURE__ */ (0, import_jsx_runtime31.jsx)("span", { className: "text-[var(--color-text-muted)]", children: "No genre" }) });
2128
+ }
2129
+ if (song.firstReleaseYear) fields.push({ label: "First released", value: song.firstReleaseYear });
2130
+ const viewCount = song.youtubeVideo?.viewCount;
2131
+ if (viewCount !== null && viewCount !== void 0)
2132
+ fields.push({ label: "YouTube views", value: numberFormat.format(viewCount) });
2133
+ fields.push({ label: "Plays", value: numberFormat.format(song.playCount) });
2134
+ const links = [];
2135
+ if (musicBrainzUrl)
2136
+ links.push(
2137
+ /* @__PURE__ */ (0, import_jsx_runtime31.jsx)("a", { href: musicBrainzUrl, target: "_blank", rel: "noopener noreferrer", className: externalLink, children: "MusicBrainz" }, "mb")
2138
+ );
2139
+ if (song.youtubeVideo)
2140
+ links.push(
2141
+ /* @__PURE__ */ (0, import_jsx_runtime31.jsx)(
2142
+ "a",
2143
+ {
2144
+ href: `https://www.youtube.com/watch?v=${encodeURIComponent(song.youtubeVideo.id)}`,
2145
+ target: "_blank",
2146
+ rel: "noopener noreferrer",
2147
+ className: externalLink,
2148
+ children: "YouTube"
2149
+ },
2150
+ "yt"
2151
+ )
2152
+ );
2153
+ if (links.length > 0)
2154
+ fields.push({
2155
+ label: "Links",
2156
+ value: /* @__PURE__ */ (0, import_jsx_runtime31.jsx)("span", { className: "flex gap-3", children: links })
2157
+ });
2158
+ return /* @__PURE__ */ (0, import_jsx_runtime31.jsxs)(import_jsx_runtime31.Fragment, { children: [
2159
+ /* @__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: [
2160
+ 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)(
2161
+ "button",
2162
+ {
2163
+ type: "button",
2164
+ onClick: () => isCurrent ? handlePlayPauseAction() : playNewTrackListFromTrackUuid(song, scope),
2165
+ className: "group flex flex-col items-center gap-1 w-16 text-xs text-center text-[var(--color-text)] focus-visible:outline-none",
2166
+ children: [
2167
+ /* @__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" }) }),
2168
+ isPlaying ? "Pause" : "Play"
2169
+ ]
2170
+ }
2171
+ ),
2172
+ shareUrl && /* @__PURE__ */ (0, import_jsx_runtime31.jsx)(ShareActionButton, { target: shareTarget, url: shareUrl })
2173
+ ] }) }),
2174
+ 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 }) }),
2175
+ /* @__PURE__ */ (0, import_jsx_runtime31.jsx)(import_ui12.FieldList, { items: fields }),
2176
+ 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)) }) }),
2177
+ /* @__PURE__ */ (0, import_jsx_runtime31.jsx)(SongHistory, { uuid: song.uuid, getBackendBaseUrl })
2178
+ ] });
2179
+ }
2180
+
2181
+ // src/genre-tree/song/useSongPanel.tsx
2145
2182
  var import_jsx_runtime32 = require("react/jsx-runtime");
2183
+ function useSongPanel({
2184
+ uuid,
2185
+ scope,
2186
+ getBackendBaseUrl,
2187
+ getShareUrl,
2188
+ renderArtistLink,
2189
+ getGenrePath,
2190
+ onSelectGenre,
2191
+ expanded,
2192
+ onExpandedChange,
2193
+ onClose
2194
+ }) {
2195
+ const result = (0, import_genre_tree17.useSong)(uuid, getBackendBaseUrl);
2196
+ const panel = { expanded, onExpandedChange, onClose };
2197
+ switch (result.status) {
2198
+ case "idle":
2199
+ return void 0;
2200
+ case "loading":
2201
+ return {
2202
+ ...panel,
2203
+ title: "Song",
2204
+ content: /* @__PURE__ */ (0, import_jsx_runtime32.jsxs)("div", { "aria-busy": "true", className: "flex flex-col gap-2.5 px-6 py-2.5", children: [
2205
+ /* @__PURE__ */ (0, import_jsx_runtime32.jsx)(import_ui13.Skeleton, { className: "h-3 w-16" }),
2206
+ /* @__PURE__ */ (0, import_jsx_runtime32.jsx)(import_ui13.Skeleton, { className: "h-4 w-32" }),
2207
+ /* @__PURE__ */ (0, import_jsx_runtime32.jsx)(import_ui13.Skeleton, { className: "h-3 w-16" }),
2208
+ /* @__PURE__ */ (0, import_jsx_runtime32.jsx)(import_ui13.Skeleton, { className: "h-4 w-24" })
2209
+ ] })
2210
+ };
2211
+ case "not-found":
2212
+ return {
2213
+ ...panel,
2214
+ title: "Song not found",
2215
+ 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." })
2216
+ };
2217
+ case "error":
2218
+ return {
2219
+ ...panel,
2220
+ title: "Song",
2221
+ content: /* @__PURE__ */ (0, import_jsx_runtime32.jsx)("p", { className: "px-6 py-2.5 text-red-600", children: "Could not load this song." })
2222
+ };
2223
+ case "success": {
2224
+ const { song } = result;
2225
+ const shareUrl = getShareUrl?.({ kind: "song", uuid: song.uuid, name: song.title, scope });
2226
+ return {
2227
+ ...panel,
2228
+ title: song.title,
2229
+ content: /* @__PURE__ */ (0, import_jsx_runtime32.jsx)(
2230
+ SongPanelContent,
2231
+ {
2232
+ song,
2233
+ scope,
2234
+ getBackendBaseUrl,
2235
+ shareUrl: shareUrl ?? void 0,
2236
+ renderArtistLink,
2237
+ genrePath: song.genre ? getGenrePath(song.genre) : [],
2238
+ onSelectGenre
2239
+ }
2240
+ )
2241
+ };
2242
+ }
2243
+ }
2244
+ }
2245
+
2246
+ // src/genre-tree/SizeMetricToggle.tsx
2247
+ var import_md = require("react-icons/md");
2248
+ var import_ui14 = require("@behindthemusictree/ui");
2249
+ var import_jsx_runtime33 = require("react/jsx-runtime");
2146
2250
  function SizeMetricToggle({
2147
2251
  value,
2148
2252
  onChange
2149
2253
  }) {
2150
- return /* @__PURE__ */ (0, import_jsx_runtime32.jsx)(
2151
- import_ui12.SegmentedControl,
2254
+ return /* @__PURE__ */ (0, import_jsx_runtime33.jsx)(
2255
+ import_ui14.SegmentedControl,
2152
2256
  {
2153
2257
  variant: "floating",
2154
2258
  "aria-label": "Size nodes by",
2155
2259
  value,
2156
2260
  onChange,
2157
2261
  options: [
2158
- { value: "songs", label: /* @__PURE__ */ (0, import_jsx_runtime32.jsx)(import_md.MdMusicNote, { size: 18, "aria-hidden": true }), "aria-label": "Size by songs", title: "Node size by song count" },
2159
- { value: "views", label: /* @__PURE__ */ (0, import_jsx_runtime32.jsx)(import_md.MdVisibility, { size: 18, "aria-hidden": true }), "aria-label": "Size by views", title: "Node size by song views" }
2262
+ { 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" },
2263
+ { 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" }
2160
2264
  ]
2161
2265
  }
2162
2266
  );
2163
2267
  }
2164
2268
 
2165
2269
  // src/genre-tree/GenreTreeView.tsx
2166
- var import_jsx_runtime33 = require("react/jsx-runtime");
2167
- var PREVIOUS_BUTTON_CSS = `
2168
- .genre-tree-view .search-overlay .gtv-info-panel-previous { display: none; }
2169
- .genre-tree-view:has(.search-overlay .gtv-info-panel-previous) .genre-search input { padding-left: 2.75rem; }
2170
- .genre-tree-view:has(.search-overlay .gtv-info-panel-previous) .genre-search-previous { display: flex; }
2171
- @keyframes genre-tree-view-previous-in { from { opacity: 0; transform: translate(-0.5rem, -50%); } to { opacity: 1; transform: translate(0, -50%); } }
2172
- @media (prefers-reduced-motion: no-preference) {
2173
- .genre-search-previous { animation: genre-tree-view-previous-in 200ms ease-out; }
2174
- }
2175
- `;
2270
+ var import_jsx_runtime34 = require("react/jsx-runtime");
2176
2271
  var NO_POP_ROOT_TITLE = "This genre tree has no 'Mainstream Pop' root yet";
2177
- 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]";
2272
+ var REGIONS_CLASS = "[--gtv-center-left:var(--layout-center-left)] [--gtv-center-right:var(--layout-center-right)] [--gtv-center-top:var(--layout-band-top)]";
2273
+ var noop = () => {
2274
+ };
2178
2275
  function GenreTreeView({
2179
2276
  scope,
2180
2277
  handleGenreCreationAction,
@@ -2207,6 +2304,13 @@ function GenreTreeView({
2207
2304
  artistSearch = true,
2208
2305
  onArtistSelect,
2209
2306
  artistSearchBaseUrl,
2307
+ songUuid: controlledSongUuid,
2308
+ onSongUuidChange,
2309
+ songExpanded: controlledSongExpanded,
2310
+ onSongExpandedChange,
2311
+ songSearch = true,
2312
+ onSongSelect,
2313
+ renderSongLink,
2210
2314
  renderSummary,
2211
2315
  renderEssentialTracks,
2212
2316
  renderSongLeading,
@@ -2221,14 +2325,21 @@ function GenreTreeView({
2221
2325
  placement = "default",
2222
2326
  ...focus
2223
2327
  }) {
2224
- const getShareUrlRef = (0, import_react15.useRef)(getShareUrl);
2328
+ const getShareUrlRef = (0, import_react16.useRef)(getShareUrl);
2225
2329
  getShareUrlRef.current = getShareUrl;
2226
- const [internalArtistUuid, setInternalArtistUuid] = (0, import_react15.useState)(null);
2330
+ const [internalArtistUuid, setInternalArtistUuid] = (0, import_react16.useState)(null);
2227
2331
  const isArtistControlled = controlledArtistUuid !== void 0;
2228
2332
  const artistUuid = isArtistControlled ? controlledArtistUuid : internalArtistUuid;
2229
- const [internalSizeMetric, setInternalSizeMetric] = (0, import_react15.useState)("songs");
2333
+ const [internalSongUuid, setInternalSongUuid] = (0, import_react16.useState)(null);
2334
+ const isSongControlled = controlledSongUuid !== void 0;
2335
+ const songUuid = isSongControlled ? controlledSongUuid : internalSongUuid;
2336
+ const changeSong = (uuid) => {
2337
+ if (!isSongControlled) setInternalSongUuid(uuid);
2338
+ onSongUuidChange?.(uuid);
2339
+ };
2340
+ const [internalSizeMetric, setInternalSizeMetric] = (0, import_react16.useState)("songs");
2230
2341
  const sizeMetric = controlledSizeMetric ?? internalSizeMetric;
2231
- const deferredSizeMetric = (0, import_react15.useDeferredValue)(sizeMetric);
2342
+ const deferredSizeMetric = (0, import_react16.useDeferredValue)(sizeMetric);
2232
2343
  const changeSizeMetric = (metric) => {
2233
2344
  if (controlledSizeMetric === void 0) setInternalSizeMetric(metric);
2234
2345
  onSizeMetricChange?.(metric);
@@ -2255,7 +2366,7 @@ function GenreTreeView({
2255
2366
  allowWheelRotation: internalAllowWheelRotation,
2256
2367
  toggleWheelRotation,
2257
2368
  onNodeHover: handleNodeHover
2258
- } = (0, import_genre_tree17.useGenreTreeViewState)({
2369
+ } = (0, import_genre_tree19.useGenreTreeViewState)({
2259
2370
  scope,
2260
2371
  getBackendBaseUrl,
2261
2372
  viewMode: controlledViewMode,
@@ -2265,7 +2376,7 @@ function GenreTreeView({
2265
2376
  artistUuid
2266
2377
  });
2267
2378
  const restoreRevision = viewportRestore?.revision;
2268
- const [stackedRestoreOwner, setStackedRestoreOwner] = (0, import_react15.useState)({ revision: restoreRevision, rootUuid: selectedGenrePlaylist?.root.uuid });
2379
+ const [stackedRestoreOwner, setStackedRestoreOwner] = (0, import_react16.useState)({ revision: restoreRevision, rootUuid: selectedGenrePlaylist?.root.uuid });
2269
2380
  let restoreRootUuid = stackedRestoreOwner.rootUuid;
2270
2381
  if (stackedRestoreOwner.revision !== restoreRevision) {
2271
2382
  restoreRootUuid = selectedGenrePlaylist?.root.uuid;
@@ -2274,14 +2385,14 @@ function GenreTreeView({
2274
2385
  restoreRootUuid = selectedGenrePlaylist.root.uuid;
2275
2386
  setStackedRestoreOwner({ revision: restoreRevision, rootUuid: restoreRootUuid });
2276
2387
  }
2277
- const isGenreUpdating = (0, import_react_query3.useIsMutating)({ mutationKey: import_genre_tree17.genreUpdateMutationKey }) > 0;
2278
- const { mutate: updateGenre } = (0, import_genre_tree17.useUpdateGenre)(scope, getBackendBaseUrl);
2388
+ const isGenreUpdating = (0, import_react_query3.useIsMutating)({ mutationKey: import_genre_tree19.genreUpdateMutationKey }) > 0;
2389
+ const { mutate: updateGenre } = (0, import_genre_tree19.useUpdateGenre)(scope, getBackendBaseUrl);
2279
2390
  const busy = deferredSizeMetric !== sizeMetric || isTreeFetching || isGenreUpdating;
2280
2391
  const isArtistFiltered = artistUuid !== null;
2281
2392
  const canSizeByViews = !isArtistFiltered && hasViewsCount(visibleGenrePlaylists);
2282
- const artistResult = (0, import_genre_tree17.useArtist)(artistUuid, artistSearchBaseUrl ?? getBackendBaseUrl);
2393
+ const artistResult = (0, import_genre_tree19.useArtist)(artistUuid, artistSearchBaseUrl ?? getBackendBaseUrl);
2283
2394
  const filterArtistName = artistResult.status === "success" ? artistResult.artist.name : null;
2284
- const filterArtist = (0, import_react15.useMemo)(
2395
+ const filterArtist = (0, import_react16.useMemo)(
2285
2396
  () => artistUuid !== null && filterArtistName !== null ? { uuid: artistUuid, name: filterArtistName } : null,
2286
2397
  [artistUuid, filterArtistName]
2287
2398
  );
@@ -2289,27 +2400,71 @@ function GenreTreeView({
2289
2400
  const playback = useGenrePlaylistPlayback({ scope, genrePlaylists: visibleGenrePlaylists, artist: filterArtist });
2290
2401
  const openArtist = (uuid) => {
2291
2402
  if (selectedNodeId !== null) clearSelection();
2403
+ if (songUuid !== null) changeSong(null);
2292
2404
  changeArtist(uuid);
2293
2405
  };
2294
- const [expandedArtistUuid, setExpandedArtistUuid] = (0, import_react15.useState)(null);
2406
+ const openSong = (uuid) => {
2407
+ if (selectedNodeId !== null) clearSelection();
2408
+ changeSong(uuid);
2409
+ };
2410
+ const openSongRef = (0, import_react16.useRef)(openSong);
2411
+ openSongRef.current = openSong;
2412
+ const resolvedRenderSongLink = (0, import_react16.useCallback)(
2413
+ (song) => renderSongLink ? renderSongLink(song) : /* @__PURE__ */ (0, import_jsx_runtime34.jsx)("button", { type: "button", onClick: () => openSongRef.current(song.uuid), className: "hover:underline", children: song.title }),
2414
+ [renderSongLink]
2415
+ );
2416
+ const [expandedSongUuid, setExpandedSongUuid] = (0, import_react16.useState)(null);
2417
+ const [expandedArtistUuid, setExpandedArtistUuid] = (0, import_react16.useState)(null);
2295
2418
  const artistBaseUrl = artistSearchBaseUrl ?? getBackendBaseUrl;
2419
+ const songBaseUrl = getBackendBaseUrl;
2296
2420
  const artistPanel = useArtistPanel({
2297
2421
  uuid: artistUuid,
2298
2422
  scope,
2299
2423
  getBackendBaseUrl: artistBaseUrl,
2300
2424
  getShareUrl: getShareUrlRef.current,
2301
- renderArtistLink: renderArtistLink ?? ((artist) => artist.isSpecialPurpose ? artist.name : /* @__PURE__ */ (0, import_jsx_runtime33.jsx)("button", { type: "button", onClick: () => openArtist(artist.uuid), className: "hover:underline", children: artist.name })),
2425
+ 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 })),
2302
2426
  expanded: controlledArtistExpanded ?? (artistUuid !== null && expandedArtistUuid === artistUuid),
2303
2427
  onExpandedChange: (on) => {
2304
2428
  if (controlledArtistExpanded === void 0) setExpandedArtistUuid(on ? artistUuid : null);
2305
2429
  onArtistExpandedChange?.(on);
2306
2430
  },
2431
+ renderSongLink: resolvedRenderSongLink,
2307
2432
  onClose: () => changeArtist(null)
2308
2433
  });
2309
- const builtInArtistPanel = overlayPanel || hasSelectedGenreNode ? void 0 : artistPanel;
2310
- const activeOverlayPanel = overlayPanel ?? (builtInArtistPanel && { ...builtInArtistPanel, hideTitle: true, hideClose: true });
2311
- const [uncontrolledCollapsed, setUncontrolledCollapsed] = (0, import_react15.useState)(false);
2312
- const [uncontrolledExpanded, setUncontrolledExpanded] = (0, import_react15.useState)(false);
2434
+ 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 }));
2435
+ const getGenrePath = (genre) => {
2436
+ const path = [];
2437
+ let current = visibleGenrePlaylists.find((playlist) => playlist.criteria?.uuid === genre.uuid);
2438
+ while (current) {
2439
+ path.unshift(current);
2440
+ const parentUuid = current.parent?.uuid;
2441
+ current = parentUuid ? visibleGenrePlaylists.find((playlist) => playlist.uuid === parentUuid) : void 0;
2442
+ }
2443
+ return path;
2444
+ };
2445
+ const songPanel = useSongPanel({
2446
+ uuid: songUuid,
2447
+ scope,
2448
+ getBackendBaseUrl: songBaseUrl,
2449
+ getShareUrl: getShareUrlRef.current,
2450
+ renderArtistLink: resolvedRenderArtistLink,
2451
+ getGenrePath,
2452
+ onSelectGenre: (genrePlaylist) => {
2453
+ changeSong(null);
2454
+ selectGenrePlaylist(genrePlaylist);
2455
+ },
2456
+ expanded: controlledSongExpanded ?? (songUuid !== null && expandedSongUuid === songUuid),
2457
+ onExpandedChange: (on) => {
2458
+ if (controlledSongExpanded === void 0) setExpandedSongUuid(on ? songUuid : null);
2459
+ onSongExpandedChange?.(on);
2460
+ },
2461
+ onClose: () => changeSong(null)
2462
+ });
2463
+ const builtInSongPanel = overlayPanel || hasSelectedGenreNode ? void 0 : songPanel;
2464
+ const builtInArtistPanel = overlayPanel || hasSelectedGenreNode || builtInSongPanel ? void 0 : artistPanel;
2465
+ const builtInPanel = builtInSongPanel ?? builtInArtistPanel;
2466
+ const [uncontrolledCollapsed, setUncontrolledCollapsed] = (0, import_react16.useState)(false);
2467
+ const [uncontrolledExpanded, setUncontrolledExpanded] = (0, import_react16.useState)(false);
2313
2468
  const isCollapsed = collapsed ?? uncontrolledCollapsed;
2314
2469
  const isExpanded = expanded ?? uncontrolledExpanded;
2315
2470
  const changeCollapsed = (next) => {
@@ -2320,54 +2475,56 @@ function GenreTreeView({
2320
2475
  setUncontrolledExpanded(next);
2321
2476
  onExpandedChange?.(next);
2322
2477
  };
2323
- (0, import_react15.useEffect)(() => {
2478
+ (0, import_react16.useEffect)(() => {
2324
2479
  setUncontrolledCollapsed(false);
2325
- }, [selectedNodeId, artistUuid]);
2326
- const isDetailHidden = isCollapsed || !hasSelectedGenreNode && !activeOverlayPanel;
2327
- useLeftDetailPanelLayout(!isDetailHidden, placement === "regions");
2328
- const detailOverlay = placement === "regions" && activeOverlayPanel?.onExpandedChange ? activeOverlayPanel : void 0;
2329
- const detailExpanded = detailOverlay ? !!detailOverlay.expanded : isExpanded;
2330
- const onDetailExpandedChange = detailOverlay ? detailOverlay.onExpandedChange : changeExpanded;
2331
- const infoPanelCuration = {
2332
- renderDangerZone,
2333
- expanded: placement === "regions" ? isExpanded : expanded,
2334
- onExpandedChange: placement === "regions" ? changeExpanded : onExpandedChange,
2335
- collapsed: placement === "regions" ? isCollapsed : void 0,
2336
- hideExpandButton: placement === "regions" ? true : void 0,
2337
- overlayPanel: detailOverlay ? { ...detailOverlay, collapsed: isCollapsed, hideExpandButton: true } : activeOverlayPanel
2338
- };
2339
- const [isWheelRevealed, setIsWheelRevealed] = (0, import_react15.useState)(false);
2480
+ }, [selectedNodeId, artistUuid, songUuid]);
2481
+ const [isWheelRevealed, setIsWheelRevealed] = (0, import_react16.useState)(false);
2340
2482
  const usesHandoff = viewMode === "wheel" || viewMode === "pop-core";
2341
2483
  const isTreeReady = !isLoading && (!usesHandoff || isWheelRevealed);
2342
- const rootRef = (0, import_react15.useRef)(null);
2343
- const handlePrevious = () => {
2344
- rootRef.current?.querySelector(".search-overlay .gtv-info-panel-previous")?.click();
2345
- };
2346
- (0, import_react15.useEffect)(() => {
2347
- if (!isTreeReady || viewMode !== "pop-core" || !hasSelectedGenreNode || !selectedGenrePlaylist) return;
2348
- const root = rootRef.current;
2349
- const centerChip = root?.querySelector(".gtv-wheel-center-node .gtv-wheel-chip");
2350
- if (!centerChip || centerChip.getAttribute("aria-label") !== selectedGenrePlaylist.name) return;
2351
- const panelTitle = root?.querySelector(".gtv-info-panel-title");
2352
- if (panelTitle?.textContent?.trim().startsWith(selectedGenrePlaylist.name)) return;
2353
- centerChip.click();
2354
- }, [hasSelectedGenreNode, isTreeReady, selectedGenrePlaylist, viewMode]);
2484
+ const [resolveStyle, setResolveStyle] = (0, import_react16.useState)(null);
2485
+ const handleNodeStyleResolverChange = (0, import_react16.useCallback)(
2486
+ (resolver) => setResolveStyle(() => resolver),
2487
+ []
2488
+ );
2489
+ const panelRef = (0, import_react16.useRef)(null);
2490
+ const obscuredInset = (0, import_ui15.useObscuredInset)(panelRef);
2491
+ const hostPanel = { obscuredInset, onNodeStyleResolverChange: handleNodeStyleResolverChange };
2492
+ const nodeMetric = isArtistFiltered || !canSizeByViews ? "songs" : deferredSizeMetric;
2493
+ const treeNodes = (0, import_react16.useMemo)(() => toTreeNodes(visibleGenrePlaylists, nodeMetric), [visibleGenrePlaylists, nodeMetric]);
2494
+ const relations = (0, import_genre_tree_view4.useGenreNodeRelations)(
2495
+ treeNodes,
2496
+ !overlayPanel && hasSelectedGenreNode ? selectedNodeId : null,
2497
+ resolveStyle
2498
+ );
2499
+ const [selectionHistory, setSelectionHistory] = (0, import_react16.useState)({
2500
+ current: selectedNodeId,
2501
+ previous: null
2502
+ });
2503
+ if (selectionHistory.current !== selectedNodeId) {
2504
+ setSelectionHistory({
2505
+ current: selectedNodeId,
2506
+ previous: selectionHistory.current ?? selectionHistory.previous
2507
+ });
2508
+ }
2509
+ const previousPlaylist = selectionHistory.previous !== null && selectionHistory.previous !== selectedNodeId ? genrePlaylistByNodeId.get(selectionHistory.previous) : void 0;
2510
+ const handlePrevious = previousPlaylist && visibleGenrePlaylists.includes(previousPlaylist) ? () => handleGenreSearchSelect(previousPlaylist) : void 0;
2355
2511
  const { showPopup, hidePopup } = (0, import_popup5.usePopup)();
2356
- const [searchText, setSearchText] = (0, import_react15.useState)("");
2357
- const artistSearchResult = (0, import_genre_tree16.useArtistSearch)(artistBaseUrl, artistSearch ? searchText : "");
2358
- const artistExtraResults = artistSearch ? {
2512
+ const [searchText, setSearchText] = (0, import_react16.useState)("");
2513
+ const artistSearchResult = (0, import_genre_tree18.useArtistSearch)(artistBaseUrl, artistSearch ? searchText : "");
2514
+ const artistGroup = artistSearch ? {
2515
+ key: "artists",
2359
2516
  label: "Artists",
2360
2517
  items: artistSearchResult.artists.map((artist) => ({
2361
2518
  key: artist.uuid,
2362
- content: /* @__PURE__ */ (0, import_jsx_runtime33.jsxs)(import_jsx_runtime33.Fragment, { children: [
2363
- /* @__PURE__ */ (0, import_jsx_runtime33.jsxs)("span", { className: "block truncate font-medium", children: [
2519
+ content: /* @__PURE__ */ (0, import_jsx_runtime34.jsxs)(import_jsx_runtime34.Fragment, { children: [
2520
+ /* @__PURE__ */ (0, import_jsx_runtime34.jsxs)("span", { className: "block truncate font-medium", children: [
2364
2521
  artist.name,
2365
- artist.disambiguation && /* @__PURE__ */ (0, import_jsx_runtime33.jsxs)("span", { className: "font-normal text-[var(--color-neutral-500)]", children: [
2522
+ artist.disambiguation && /* @__PURE__ */ (0, import_jsx_runtime34.jsxs)("span", { className: "font-normal text-[var(--color-neutral-500)]", children: [
2366
2523
  " \xB7 ",
2367
2524
  artist.disambiguation
2368
2525
  ] })
2369
2526
  ] }),
2370
- artist.matchedAlias && /* @__PURE__ */ (0, import_jsx_runtime33.jsxs)("span", { className: "block truncate text-xs text-[var(--color-neutral-500)]", children: [
2527
+ artist.matchedAlias && /* @__PURE__ */ (0, import_jsx_runtime34.jsxs)("span", { className: "block truncate text-xs text-[var(--color-neutral-500)]", children: [
2371
2528
  "also known as ",
2372
2529
  artist.matchedAlias
2373
2530
  ] })
@@ -2380,11 +2537,32 @@ function GenreTreeView({
2380
2537
  status: artistSearchResult.isError ? "Search failed" : artistSearchResult.settled && artistSearchResult.artists.length === 0 ? "No artists found" : "",
2381
2538
  loading: artistSearchResult.pending
2382
2539
  } : void 0;
2540
+ const songSearchResult = (0, import_genre_tree18.useSongSearch)(songBaseUrl, songSearch ? searchText : "");
2541
+ const songGroup = songSearch ? {
2542
+ key: "songs",
2543
+ label: "Songs",
2544
+ items: songSearchResult.songs.map((song) => ({
2545
+ key: song.uuid,
2546
+ content: /* @__PURE__ */ (0, import_jsx_runtime34.jsxs)("span", { className: (0, import_genre_tree18.isSongSearchResultPlayable)(song) ? void 0 : "opacity-60", children: [
2547
+ /* @__PURE__ */ (0, import_jsx_runtime34.jsx)("span", { className: "block truncate font-medium", children: song.title }),
2548
+ /* @__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 ") })
2549
+ ] }),
2550
+ onSelect: () => {
2551
+ openSong(song.uuid);
2552
+ onSongSelect?.(song);
2553
+ }
2554
+ })),
2555
+ status: songSearchResult.isError ? "Search failed" : songSearchResult.settled && songSearchResult.songs.length === 0 ? "No songs found" : "",
2556
+ loading: songSearchResult.pending
2557
+ } : void 0;
2558
+ const extraGroups = [artistGroup, songGroup].filter((group) => group !== void 0);
2383
2559
  const handleSelectedNodeChange = (node) => {
2560
+ if (node !== null && songUuid !== null) changeSong(null);
2384
2561
  selectNode(node);
2385
2562
  };
2386
2563
  const handleGenreSearchClear = clearSelection;
2387
2564
  const handleGenreSearchSelect = (genrePlaylist) => {
2565
+ if (songUuid !== null) changeSong(null);
2388
2566
  selectGenrePlaylist(genrePlaylist);
2389
2567
  };
2390
2568
  const selectedUuid = selectedGenrePlaylist?.uuid;
@@ -2393,36 +2571,49 @@ function GenreTreeView({
2393
2571
  const handleGenreSearchZoomIn = () => {
2394
2572
  if (selectedUuid) focus.onFocusGenreChange?.(selectedUuid, "zoom-in");
2395
2573
  };
2396
- const zoomInRef = (0, import_react15.useRef)(void 0);
2574
+ const zoomInRef = (0, import_react16.useRef)(void 0);
2397
2575
  zoomInRef.current = canZoomIn ? handleGenreSearchZoomIn : void 0;
2398
- const renderInfoPanelBody = (0, import_react15.useCallback)(
2399
- (node, { toolbar, ancestors, family }) => {
2400
- const genrePlaylist = genrePlaylistByNodeId.get(node.id);
2401
- const criteria = genrePlaylist?.criteria;
2402
- if (!genrePlaylist || !criteria) return { primary: /* @__PURE__ */ (0, import_jsx_runtime33.jsxs)(import_jsx_runtime33.Fragment, { children: [
2403
- toolbar,
2404
- family
2405
- ] }), aside: ancestors };
2406
- const genreUuid = criteria.uuid;
2407
- const parentPlaylist = visibleGenrePlaylists.find(({ uuid }) => uuid === genrePlaylist.parent?.uuid);
2408
- const canToggleSide = !readOnly && parentPlaylist !== void 0 && parentPlaylist.parent === null;
2409
- const side = criteria.side ?? "core";
2410
- const shareTarget = {
2411
- kind: "genre",
2412
- genrePlaylistUuid: genrePlaylist.uuid,
2413
- name: criteria.name,
2414
- scope,
2415
- artistUuid
2416
- };
2417
- const shareUrl = getShareUrlRef.current?.(shareTarget) ?? void 0;
2418
- const popSibling = canToggleSide && side === "core" ? visibleGenrePlaylists.find(
2419
- (sibling) => sibling.parent?.uuid === parentPlaylist.uuid && sibling.uuid !== genrePlaylist.uuid && sibling.criteria?.side === "pop"
2420
- ) : void 0;
2421
- return {
2422
- primary: /* @__PURE__ */ (0, import_jsx_runtime33.jsx)(
2576
+ const selectGenreFromPanel = (nodeId) => {
2577
+ const genrePlaylist = genrePlaylistByNodeId.get(nodeId);
2578
+ if (genrePlaylist) handleGenreSearchSelect(genrePlaylist);
2579
+ };
2580
+ const renderGenreBody = (relation) => {
2581
+ const { node } = relation;
2582
+ const family = /* @__PURE__ */ (0, import_jsx_runtime34.jsx)(
2583
+ import_genre_tree_view4.GenreNodeFamily,
2584
+ {
2585
+ node,
2586
+ fill: relation.fill,
2587
+ textColor: relation.textColor,
2588
+ parentNode: relation.parentNode,
2589
+ childNodes: relation.childNodes,
2590
+ onSelectNode: selectGenreFromPanel
2591
+ }
2592
+ );
2593
+ const ancestors = /* @__PURE__ */ (0, import_jsx_runtime34.jsx)(import_genre_tree_view4.GenreNodeAncestors, { ancestorNodes: relation.ancestorNodes, onSelectNode: selectGenreFromPanel });
2594
+ const genrePlaylist = genrePlaylistByNodeId.get(node.id);
2595
+ const criteria = genrePlaylist?.criteria;
2596
+ if (!genrePlaylist || !criteria) return { primary: family, aside: ancestors };
2597
+ const genreUuid = criteria.uuid;
2598
+ const parentPlaylist = visibleGenrePlaylists.find(({ uuid }) => uuid === genrePlaylist.parent?.uuid);
2599
+ const canToggleSide = !readOnly && parentPlaylist !== void 0 && parentPlaylist.parent === null;
2600
+ const side = criteria.side ?? "core";
2601
+ const shareTarget = {
2602
+ kind: "genre",
2603
+ genrePlaylistUuid: genrePlaylist.uuid,
2604
+ name: criteria.name,
2605
+ scope,
2606
+ artistUuid
2607
+ };
2608
+ const shareUrl = getShareUrlRef.current?.(shareTarget) ?? void 0;
2609
+ const popSibling = canToggleSide && side === "core" ? visibleGenrePlaylists.find(
2610
+ (sibling) => sibling.parent?.uuid === parentPlaylist.uuid && sibling.uuid !== genrePlaylist.uuid && sibling.criteria?.side === "pop"
2611
+ ) : void 0;
2612
+ return {
2613
+ primary: /* @__PURE__ */ (0, import_jsx_runtime34.jsxs)(import_jsx_runtime34.Fragment, { children: [
2614
+ /* @__PURE__ */ (0, import_jsx_runtime34.jsx)(
2423
2615
  GenreInfoPanelBody,
2424
2616
  {
2425
- toolbar,
2426
2617
  family,
2427
2618
  genreUuid,
2428
2619
  genrePlaylist,
@@ -2438,7 +2629,7 @@ function GenreTreeView({
2438
2629
  onAddChild: readOnly ? void 0 : () => handleGenreCreationAction(criteria),
2439
2630
  onRename: readOnly ? void 0 : () => handleGenreRenameAction(criteria),
2440
2631
  onChangeParent: readOnly ? void 0 : () => showPopup(
2441
- /* @__PURE__ */ (0, import_jsx_runtime33.jsx)(
2632
+ /* @__PURE__ */ (0, import_jsx_runtime34.jsx)(
2442
2633
  ChangeParentPopup,
2443
2634
  {
2444
2635
  genrePlaylist: { ...genrePlaylist, criteria },
@@ -2459,6 +2650,7 @@ function GenreTreeView({
2459
2650
  criteriaOverviewSchema,
2460
2651
  renderGenreDetailExtras,
2461
2652
  renderArtistLink,
2653
+ renderSongLink: resolvedRenderSongLink,
2462
2654
  artist: filterArtist,
2463
2655
  renderSummary,
2464
2656
  renderEssentialTracks,
@@ -2468,48 +2660,65 @@ function GenreTreeView({
2468
2660
  }
2469
2661
  }
2470
2662
  ),
2471
- aside: ancestors
2472
- };
2473
- },
2474
- [
2475
- genrePlaylistByNodeId,
2476
- additionalActions,
2477
- readOnly,
2478
- handleGenreCreationAction,
2479
- handleGenreRenameAction,
2480
- showPopup,
2481
- hidePopup,
2482
- onParentChanged,
2483
- visibleGenrePlaylists,
2484
- updateGenre,
2485
- playback,
2486
- filterArtist,
2487
- scope,
2488
- getBackendBaseUrl,
2489
- criteriaOverviewSchema,
2490
- renderGenreDetailExtras,
2491
- renderArtistLink,
2492
- renderSummary,
2493
- renderEssentialTracks,
2494
- renderSongLeading,
2495
- renderSongActions,
2496
- renderTracksToolbar
2497
- ]
2498
- );
2663
+ renderDangerZone?.(node)
2664
+ ] }),
2665
+ aside: ancestors
2666
+ };
2667
+ };
2499
2668
  const isRotationControlled = controlledAllowWheelRotation !== void 0;
2500
2669
  const allowWheelRotation = controlledAllowWheelRotation ?? internalAllowWheelRotation;
2501
- const detailTab = placement === "regions" && (hasSelectedGenreNode || detailOverlay) ? /* @__PURE__ */ (0, import_jsx_runtime33.jsx)(
2502
- PanelEdgeTab,
2670
+ let activePanel = null;
2671
+ if (overlayPanel) {
2672
+ activePanel = {
2673
+ title: overlayPanel.title,
2674
+ content: overlayPanel.content,
2675
+ aside: overlayPanel.extraContent,
2676
+ expanded: overlayPanel.expanded ?? false,
2677
+ onExpandedChange: overlayPanel.onExpandedChange,
2678
+ onClose: overlayPanel.onClose,
2679
+ compact: false
2680
+ };
2681
+ } else if (relations) {
2682
+ const { primary, aside } = renderGenreBody(relations);
2683
+ activePanel = {
2684
+ title: relations.node.name,
2685
+ content: primary,
2686
+ aside,
2687
+ expanded: isExpanded,
2688
+ onExpandedChange: changeExpanded,
2689
+ onClose: clearSelection,
2690
+ compact: true,
2691
+ headerAccent: { background: relations.fill, color: relations.textColor }
2692
+ };
2693
+ } else if (builtInPanel) {
2694
+ activePanel = {
2695
+ title: builtInPanel.title,
2696
+ content: builtInPanel.content,
2697
+ aside: builtInPanel.extraContent,
2698
+ expanded: builtInPanel.expanded,
2699
+ onExpandedChange: builtInPanel.onExpandedChange,
2700
+ onClose: builtInPanel.onClose,
2701
+ compact: true
2702
+ };
2703
+ }
2704
+ const isPanelShown = isTreeReady && activePanel !== null;
2705
+ const detailTab = placement === "regions" && activePanel?.onExpandedChange ? {
2706
+ edge: activePanel.expanded ? { label: "Shrink panel", onClick: () => activePanel.onExpandedChange?.(false) } : isCollapsed ? null : { label: "Hide panel", onClick: () => changeCollapsed(true) },
2707
+ center: activePanel.expanded ? null : isCollapsed ? { label: "Show panel", onClick: () => changeCollapsed(false) } : activePanel.onExpandedChange ? { label: "Extend panel", onClick: () => activePanel.onExpandedChange?.(true) } : null
2708
+ } : void 0;
2709
+ const expandButton = placement === "default" && activePanel?.onExpandedChange ? /* @__PURE__ */ (0, import_jsx_runtime34.jsx)(
2710
+ import_ui15.IconButton,
2503
2711
  {
2504
- side: "left",
2505
- edge: detailExpanded ? { label: "Shrink panel", onClick: () => onDetailExpandedChange?.(false) } : isCollapsed ? null : { label: "Hide panel", onClick: () => changeCollapsed(true) },
2506
- center: detailExpanded ? null : isCollapsed ? { label: "Show panel", onClick: () => changeCollapsed(false) } : onDetailExpandedChange ? { label: "Extend panel", onClick: () => onDetailExpandedChange(true) } : null
2712
+ "aria-label": activePanel.expanded ? "Shrink panel" : "Extend panel",
2713
+ style: activePanel.headerAccent && { color: "inherit" },
2714
+ onClick: () => activePanel.onExpandedChange?.(!activePanel.expanded),
2715
+ 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" })
2507
2716
  }
2508
2717
  ) : null;
2509
2718
  const renderArtistFilter = () => {
2510
2719
  if (isLoading || !filterArtist) return null;
2511
- return /* @__PURE__ */ (0, import_jsx_runtime33.jsxs)("span", { className: "inline-flex items-center overflow-hidden rounded-full bg-[var(--color-neutral-100)] text-sm", children: [
2512
- /* @__PURE__ */ (0, import_jsx_runtime33.jsx)(
2720
+ 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: [
2721
+ /* @__PURE__ */ (0, import_jsx_runtime34.jsx)(
2513
2722
  "button",
2514
2723
  {
2515
2724
  type: "button",
@@ -2521,7 +2730,7 @@ function GenreTreeView({
2521
2730
  children: filterArtist.name
2522
2731
  }
2523
2732
  ),
2524
- /* @__PURE__ */ (0, import_jsx_runtime33.jsx)(
2733
+ /* @__PURE__ */ (0, import_jsx_runtime34.jsx)(
2525
2734
  "button",
2526
2735
  {
2527
2736
  type: "button",
@@ -2533,23 +2742,23 @@ function GenreTreeView({
2533
2742
  )
2534
2743
  ] });
2535
2744
  };
2536
- const actions = /* @__PURE__ */ (0, import_jsx_runtime33.jsxs)(import_jsx_runtime33.Fragment, { children: [
2537
- !isLoading && /* @__PURE__ */ (0, import_jsx_runtime33.jsx)(
2745
+ const actions = /* @__PURE__ */ (0, import_jsx_runtime34.jsxs)(import_jsx_runtime34.Fragment, { children: [
2746
+ !isLoading && /* @__PURE__ */ (0, import_jsx_runtime34.jsx)(
2538
2747
  GenreSearch,
2539
2748
  {
2540
2749
  genrePlaylists: visibleGenrePlaylists,
2541
2750
  onSelect: handleGenreSearchSelect,
2542
- selectedName: selectedGenrePlaylist?.name ?? builtInArtistPanel?.title ?? null,
2543
- onClear: builtInArtistPanel ? () => changeArtist(null) : handleGenreSearchClear,
2751
+ selectedName: selectedGenrePlaylist?.name ?? (typeof builtInPanel?.title === "string" ? builtInPanel.title : null),
2752
+ onClear: builtInSongPanel ? () => changeSong(null) : builtInArtistPanel ? () => changeArtist(null) : handleGenreSearchClear,
2544
2753
  onPrevious: handlePrevious,
2545
- onQueryChange: artistSearch ? setSearchText : void 0,
2546
- extraResults: artistExtraResults
2754
+ onQueryChange: artistSearch || songSearch ? setSearchText : void 0,
2755
+ extraGroups
2547
2756
  }
2548
2757
  ),
2549
- !isLoading && filterArtist && /* @__PURE__ */ (0, import_jsx_runtime33.jsx)("div", { className: "hidden w-full justify-center max-md:flex", children: renderArtistFilter() }),
2550
- /* @__PURE__ */ (0, import_jsx_runtime33.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: [
2551
- !isLoading && !isControlled && /* @__PURE__ */ (0, import_jsx_runtime33.jsx)(
2552
- import_ui13.SegmentedControl,
2758
+ !isLoading && filterArtist && /* @__PURE__ */ (0, import_jsx_runtime34.jsx)("div", { className: "hidden w-full justify-center max-md:flex", children: renderArtistFilter() }),
2759
+ /* @__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: [
2760
+ !isLoading && !isControlled && /* @__PURE__ */ (0, import_jsx_runtime34.jsx)(
2761
+ import_ui15.SegmentedControl,
2553
2762
  {
2554
2763
  "aria-label": "Tree view mode",
2555
2764
  value: viewMode,
@@ -2562,14 +2771,14 @@ function GenreTreeView({
2562
2771
  ]
2563
2772
  }
2564
2773
  ),
2565
- !isLoading && showDisplayOptions && /* @__PURE__ */ (0, import_jsx_runtime33.jsx)(
2774
+ !isLoading && showDisplayOptions && /* @__PURE__ */ (0, import_jsx_runtime34.jsx)(
2566
2775
  "div",
2567
2776
  {
2568
2777
  className: "flex items-center gap-1",
2569
2778
  role: "group",
2570
2779
  "aria-label": "Tree display options",
2571
- children: !isRotationControlled && viewMode !== "stacked" && viewMode !== "outline" && /* @__PURE__ */ (0, import_jsx_runtime33.jsx)(
2572
- import_ui13.Button,
2780
+ children: !isRotationControlled && viewMode !== "stacked" && viewMode !== "outline" && /* @__PURE__ */ (0, import_jsx_runtime34.jsx)(
2781
+ import_ui15.Button,
2573
2782
  {
2574
2783
  variant: allowWheelRotation ? "default" : "outline",
2575
2784
  size: "sm",
@@ -2579,8 +2788,8 @@ function GenreTreeView({
2579
2788
  )
2580
2789
  }
2581
2790
  ),
2582
- !isLoading && !readOnly && showAddRoot && /* @__PURE__ */ (0, import_jsx_runtime33.jsx)(
2583
- import_ui13.IconTextButton,
2791
+ !isLoading && !readOnly && showAddRoot && /* @__PURE__ */ (0, import_jsx_runtime34.jsx)(
2792
+ import_ui15.IconTextButton,
2584
2793
  {
2585
2794
  icon: import_lucide_react11.Plus,
2586
2795
  text: "Add root",
@@ -2589,42 +2798,34 @@ function GenreTreeView({
2589
2798
  )
2590
2799
  ] })
2591
2800
  ] });
2592
- return /* @__PURE__ */ (0, import_jsx_runtime33.jsxs)(
2801
+ return /* @__PURE__ */ (0, import_jsx_runtime34.jsxs)(
2593
2802
  "div",
2594
2803
  {
2595
- ref: rootRef,
2804
+ "data-left-panel": isPanelShown && !isCollapsed ? void 0 : "hidden",
2596
2805
  className: ["genre-tree-view relative flex flex-col h-full", placement === "regions" && REGIONS_CLASS, className2].filter(Boolean).join(" "),
2597
2806
  children: [
2598
- /* @__PURE__ */ (0, import_jsx_runtime33.jsx)("style", { children: PREVIOUS_BUTTON_CSS }),
2599
- /* @__PURE__ */ (0, import_jsx_runtime33.jsx)(
2807
+ /* @__PURE__ */ (0, import_jsx_runtime34.jsx)(
2600
2808
  "div",
2601
2809
  {
2602
2810
  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"}`,
2603
2811
  children: actions
2604
2812
  }
2605
2813
  ),
2606
- !isLoading && filterArtist && /* @__PURE__ */ (0, import_jsx_runtime33.jsx)(
2814
+ !isLoading && filterArtist && /* @__PURE__ */ (0, import_jsx_runtime34.jsx)(
2607
2815
  "div",
2608
2816
  {
2609
2817
  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"}`,
2610
2818
  children: renderArtistFilter()
2611
2819
  }
2612
2820
  ),
2613
- detailTab && /* @__PURE__ */ (0, import_jsx_runtime33.jsx)(
2614
- "div",
2615
- {
2616
- 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))]"}`,
2617
- children: detailTab
2618
- }
2619
- ),
2620
- /* @__PURE__ */ (0, import_jsx_runtime33.jsx)("div", { className: "content-container flex-1 min-h-0 flex flex-row gap-4", children: /* @__PURE__ */ (0, import_jsx_runtime33.jsxs)("div", { className: "tree-view-container relative flex-1 min-w-0 flex flex-col h-full", children: [
2621
- !isLoading && viewMode === "outline" && floatingControlsLeading && /* @__PURE__ */ (0, import_jsx_runtime33.jsx)("div", { className: "absolute bottom-3 right-3 z-10", children: floatingControlsLeading }),
2622
- isLoading ? /* @__PURE__ */ (0, import_jsx_runtime33.jsx)(import_genre_tree_view4.GenreTreeViewSkeleton, { viewMode }) : viewMode === "wheel" ? (
2821
+ /* @__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: [
2822
+ !isLoading && viewMode === "outline" && floatingControlsLeading && /* @__PURE__ */ (0, import_jsx_runtime34.jsx)("div", { className: "absolute bottom-3 right-3 z-10", children: floatingControlsLeading }),
2823
+ isLoading ? /* @__PURE__ */ (0, import_jsx_runtime34.jsx)(import_genre_tree_view4.GenreTreeViewSkeleton, { viewMode }) : viewMode === "wheel" ? (
2623
2824
  // The info panel's top edge aligns with the tree's; the search bar floats `--layout-panel-inset` below it,
2624
2825
  // and the header inset (the bar's height plus that inset) extends the header's fill behind the bar,
2625
2826
  // Google Maps style; the 8px bottom padding mirrors the inset above the bar, and the hidden title's height is zeroed.
2626
2827
  // The panel's own Zoom in button is hidden: the search bar carries it.
2627
- /* @__PURE__ */ (0, import_jsx_runtime33.jsx)("div", { className: "tree-container search-overlay relative flex-1 w-full min-h-0 [--gtv-info-panel-header-inset:calc(var(--layout-search-h,2.5rem)+var(--layout-panel-inset,0.5rem))] [&_.gtv-info-panel-header]:pb-2 [&_.gtv-info-panel-title--hidden]:h-0 [&_.gtv-toolbar]:hidden", children: /* @__PURE__ */ (0, import_jsx_runtime33.jsx)(GenreTreeWheelHandoff, { skeleton: /* @__PURE__ */ (0, import_jsx_runtime33.jsx)(import_genre_tree_view4.GenreTreeWheelSkeleton, {}), onRevealedChange: setIsWheelRevealed, children: /* @__PURE__ */ (0, import_jsx_runtime33.jsx)(
2828
+ /* @__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)(
2628
2829
  GenrePlaylistTreeWheel,
2629
2830
  {
2630
2831
  scope,
@@ -2633,29 +2834,26 @@ function GenreTreeView({
2633
2834
  handleGenreRenameAction,
2634
2835
  additionalActions,
2635
2836
  onSelectedNodeChange: handleSelectedNodeChange,
2636
- hideInfoPanelClose: true,
2637
- hideInfoPanelTitle: true,
2638
2837
  onNodeHover: handleNodeHover,
2639
- renderInfoPanelBody,
2838
+ ...hostPanel,
2640
2839
  playback,
2641
2840
  selectedNodeId,
2642
2841
  readOnly,
2643
2842
  allowWheelRotation,
2644
2843
  sizeMetric: isArtistFiltered || !canSizeByViews ? "songs" : deferredSizeMetric,
2645
2844
  busy,
2646
- floatingControlsLeading: floatingControlsLeading || canSizeByViews ? /* @__PURE__ */ (0, import_jsx_runtime33.jsxs)("div", { className: "flex items-center gap-2", children: [
2845
+ floatingControlsLeading: floatingControlsLeading || canSizeByViews ? /* @__PURE__ */ (0, import_jsx_runtime34.jsxs)("div", { className: "flex items-center gap-2", children: [
2647
2846
  floatingControlsLeading,
2648
- canSizeByViews && /* @__PURE__ */ (0, import_jsx_runtime33.jsx)(SizeMetricToggle, { value: sizeMetric, onChange: changeSizeMetric })
2847
+ canSizeByViews && /* @__PURE__ */ (0, import_jsx_runtime34.jsx)(SizeMetricToggle, { value: sizeMetric, onChange: changeSizeMetric })
2649
2848
  ] }) : void 0,
2650
2849
  defaultViewport,
2651
2850
  viewportRestore,
2652
- onViewportChange,
2653
- ...infoPanelCuration
2851
+ onViewportChange
2654
2852
  }
2655
2853
  ) }) })
2656
2854
  ) : viewMode === "pop-core" ? (
2657
2855
  // Same as the wheel: the header inset extends its fill behind the floating search bar, Google Maps style.
2658
- /* @__PURE__ */ (0, import_jsx_runtime33.jsx)("div", { className: "tree-container search-overlay relative flex-1 w-full min-h-0 [--gtv-info-panel-header-inset:calc(var(--layout-search-h,2.5rem)+var(--layout-panel-inset,0.5rem))] [&_.gtv-info-panel-header]:pb-2 [&_.gtv-info-panel-title--hidden]:h-0 [&_.gtv-toolbar]:hidden [&_.gtv-info-panel-zoom-in]:hidden", children: /* @__PURE__ */ (0, import_jsx_runtime33.jsx)(GenreTreeWheelHandoff, { skeleton: /* @__PURE__ */ (0, import_jsx_runtime33.jsx)(import_genre_tree_view4.GenreTreeWheelSkeleton, {}), onRevealedChange: setIsWheelRevealed, children: /* @__PURE__ */ (0, import_jsx_runtime33.jsx)(
2856
+ /* @__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)(
2659
2857
  GenrePlaylistTreeWheelRadialPopCore,
2660
2858
  {
2661
2859
  scope,
@@ -2664,28 +2862,25 @@ function GenreTreeView({
2664
2862
  handleGenreRenameAction,
2665
2863
  additionalActions,
2666
2864
  onSelectedNodeChange: handleSelectedNodeChange,
2667
- hideInfoPanelClose: true,
2668
- hideInfoPanelTitle: true,
2669
2865
  onNodeHover: handleNodeHover,
2670
- renderInfoPanelBody,
2866
+ ...hostPanel,
2671
2867
  playback,
2672
2868
  selectedNodeId,
2673
2869
  readOnly,
2674
2870
  allowWheelRotation,
2675
2871
  sizeMetric: isArtistFiltered || !canSizeByViews ? "songs" : deferredSizeMetric,
2676
2872
  busy,
2677
- floatingControlsLeading: floatingControlsLeading || canSizeByViews ? /* @__PURE__ */ (0, import_jsx_runtime33.jsxs)("div", { className: "flex items-center gap-2", children: [
2873
+ floatingControlsLeading: floatingControlsLeading || canSizeByViews ? /* @__PURE__ */ (0, import_jsx_runtime34.jsxs)("div", { className: "flex items-center gap-2", children: [
2678
2874
  floatingControlsLeading,
2679
- canSizeByViews && /* @__PURE__ */ (0, import_jsx_runtime33.jsx)(SizeMetricToggle, { value: sizeMetric, onChange: changeSizeMetric })
2875
+ canSizeByViews && /* @__PURE__ */ (0, import_jsx_runtime34.jsx)(SizeMetricToggle, { value: sizeMetric, onChange: changeSizeMetric })
2680
2876
  ] }) : void 0,
2681
2877
  defaultViewport,
2682
2878
  viewportRestore,
2683
2879
  onViewportChange,
2684
- ...infoPanelCuration,
2685
2880
  ...focus
2686
2881
  }
2687
2882
  ) }) })
2688
- ) : viewMode === "outline" ? /* @__PURE__ */ (0, import_jsx_runtime33.jsx)("div", { className: "tree-container flex-1 min-h-0 w-full overflow-y-auto relative [--gtv-info-panel-header-inset:calc(var(--layout-search-h,2.5rem)+var(--layout-panel-inset,0.5rem)+0.75rem)] [&_.gtv-toolbar]:hidden", children: /* @__PURE__ */ (0, import_jsx_runtime33.jsx)(
2883
+ ) : 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)(
2689
2884
  GenrePlaylistTreeWheelRadialPopCore,
2690
2885
  {
2691
2886
  outline: true,
@@ -2695,22 +2890,19 @@ function GenreTreeView({
2695
2890
  handleGenreRenameAction,
2696
2891
  additionalActions,
2697
2892
  onSelectedNodeChange: handleSelectedNodeChange,
2698
- hideInfoPanelClose: true,
2699
- hideInfoPanelTitle: true,
2700
2893
  onNodeHover: handleNodeHover,
2701
- renderInfoPanelBody,
2894
+ ...hostPanel,
2702
2895
  playback,
2703
2896
  selectedNodeId,
2704
- readOnly,
2705
- ...infoPanelCuration
2897
+ readOnly
2706
2898
  }
2707
- ) }) : /* @__PURE__ */ (0, import_jsx_runtime33.jsx)("div", { className: "tree-container relative flex flex-col gap-4 w-full pt-14 overflow-x-auto overflow-y-auto text-[var(--color-text)] [&_.gtv-toolbar]:hidden", children: Object.entries(groupedGenrePlaylistsByRoot).map(
2708
- ([uuid, genrePlaylistTreePerRoot], rootIndex) => {
2709
- return /* @__PURE__ */ (0, import_jsx_runtime33.jsx)(
2899
+ ) }) : /* @__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(
2900
+ ([uuid, genrePlaylistTreePerRoot]) => {
2901
+ return /* @__PURE__ */ (0, import_jsx_runtime34.jsx)(
2710
2902
  "div",
2711
2903
  {
2712
2904
  className: "tree-per-root-container relative shrink-0 h-[28rem] mt-2 mr-16 p-2 bg-[var(--color-neutral-100)] rounded-lg",
2713
- children: /* @__PURE__ */ (0, import_jsx_runtime33.jsx)("div", { className: "graph-container relative z-10 h-full", children: /* @__PURE__ */ (0, import_jsx_runtime33.jsx)(
2905
+ children: /* @__PURE__ */ (0, import_jsx_runtime34.jsx)("div", { className: "graph-container relative z-10 h-full", children: /* @__PURE__ */ (0, import_jsx_runtime34.jsx)(
2714
2906
  GenrePlaylistTreePerRoot,
2715
2907
  {
2716
2908
  scope,
@@ -2720,26 +2912,45 @@ function GenreTreeView({
2720
2912
  handleGenreRenameAction,
2721
2913
  additionalActions,
2722
2914
  onSelectedNodeChange: handleSelectedNodeChange,
2723
- hideInfoPanelClose: true,
2724
- hideInfoPanelTitle: true,
2725
2915
  onNodeHover: handleNodeHover,
2726
- renderInfoPanelBody,
2916
+ ...hostPanel,
2727
2917
  playback,
2728
2918
  selectedNodeId: uuid === selectedGenrePlaylist?.root.uuid ? selectedNodeId : null,
2729
2919
  readOnly,
2730
2920
  wheelZoom: "modifier",
2731
2921
  defaultViewport: uuid === selectedGenrePlaylist?.root.uuid ? defaultViewport : void 0,
2732
2922
  viewportRestore: uuid === selectedGenrePlaylist?.root.uuid && uuid === restoreRootUuid ? viewportRestore : void 0,
2733
- onViewportChange: uuid === selectedGenrePlaylist?.root.uuid ? onViewportChange : void 0,
2734
- ...infoPanelCuration,
2735
- overlayPanel: rootIndex === 0 ? activeOverlayPanel : void 0
2923
+ onViewportChange: uuid === selectedGenrePlaylist?.root.uuid ? onViewportChange : void 0
2736
2924
  }
2737
2925
  ) })
2738
2926
  },
2739
2927
  uuid
2740
2928
  );
2741
2929
  }
2742
- ) })
2930
+ ) }),
2931
+ /* @__PURE__ */ (0, import_jsx_runtime34.jsx)(
2932
+ import_ui15.DetailPanel,
2933
+ {
2934
+ ref: panelRef,
2935
+ side: "left",
2936
+ title: activePanel?.title ?? "",
2937
+ hideTitle: activePanel?.compact,
2938
+ hideClose: activePanel?.compact,
2939
+ headerAccent: activePanel?.headerAccent,
2940
+ headerActions: expandButton,
2941
+ onClose: activePanel?.onClose ?? noop,
2942
+ collapsed: isCollapsed || !isTreeReady || !activePanel,
2943
+ expanded: activePanel?.expanded,
2944
+ onExpandedChange: activePanel?.onExpandedChange,
2945
+ aside: activePanel?.aside,
2946
+ edgeTab: detailTab,
2947
+ headerClassName: activePanel?.compact ? "pt-[calc(var(--layout-search-h)+var(--layout-panel-inset)+0.5rem)]" : void 0,
2948
+ children: activePanel?.aside && !activePanel.expanded ? /* @__PURE__ */ (0, import_jsx_runtime34.jsxs)(import_jsx_runtime34.Fragment, { children: [
2949
+ activePanel.content,
2950
+ activePanel.aside
2951
+ ] }) : activePanel?.content
2952
+ }
2953
+ )
2743
2954
  ] }) })
2744
2955
  ]
2745
2956
  }
@@ -2747,12 +2958,12 @@ function GenreTreeView({
2747
2958
  }
2748
2959
 
2749
2960
  // src/genre-tree/GenreDetailPanel.tsx
2750
- var import_react16 = require("react");
2961
+ var import_react17 = require("react");
2751
2962
  var import_genre_tree_view5 = require("@behindthemusictree/genre-tree-view");
2752
- var import_ui14 = require("@behindthemusictree/ui");
2753
- var import_genre_tree18 = require("@behindthemusictree/app-kit/genre-tree");
2754
- var import_jsx_runtime34 = require("react/jsx-runtime");
2755
- var ITEM_COUNT_LABEL = "Songs";
2963
+ var import_ui16 = require("@behindthemusictree/ui");
2964
+ var import_lucide_react13 = require("lucide-react");
2965
+ var import_genre_tree20 = require("@behindthemusictree/app-kit/genre-tree");
2966
+ var import_jsx_runtime35 = require("react/jsx-runtime");
2756
2967
  var RELATIVE_ITEM_COUNT = 1;
2757
2968
  function toNode(genre, parentId) {
2758
2969
  return {
@@ -2776,11 +2987,12 @@ function GenreDetailPanel({
2776
2987
  collapsed,
2777
2988
  expanded,
2778
2989
  onExpandedChange,
2990
+ edgeTab,
2779
2991
  extras
2780
2992
  }) {
2781
- const { data: detail, isPending, isError } = (0, import_genre_tree18.useFetchGenreDetail)(genreUuid, scope, getBackendBaseUrl);
2993
+ const { data: detail, isPending, isError } = (0, import_genre_tree20.useFetchGenreDetail)(genreUuid, scope, getBackendBaseUrl);
2782
2994
  const playback = useGenrePlaylistPlayback({ scope, genrePlaylists: [] });
2783
- const view = (0, import_react16.useMemo)(() => {
2995
+ const view = (0, import_react17.useMemo)(() => {
2784
2996
  if (!detail) return null;
2785
2997
  const rootColor = (0, import_genre_tree_view5.getGenreTreeColor)(detail.root.wikidataId);
2786
2998
  const relative = (genre, parentId) => ({
@@ -2788,7 +3000,7 @@ function GenreDetailPanel({
2788
3000
  fill: (0, import_genre_tree_view5.tintSurface)(rootColor),
2789
3001
  textColor: import_genre_tree_view5.TEXT_COLOR
2790
3002
  });
2791
- const ancestors = [...detail.ascendants].sort((a, b) => b.degree - a.degree);
3003
+ const ancestors2 = [...detail.ascendants].sort((a, b) => b.degree - a.degree);
2792
3004
  return {
2793
3005
  node: {
2794
3006
  id: detail.uuid,
@@ -2800,57 +3012,72 @@ function GenreDetailPanel({
2800
3012
  fill: rootColor,
2801
3013
  parentNode: detail.parent ? relative(detail.parent, null) : null,
2802
3014
  childNodes: detail.children.map((child) => relative(child, detail.uuid)),
2803
- ancestorNodes: ancestors.map(
2804
- ({ ascendant }, index) => relative(ascendant, index === 0 ? null : ancestors[index - 1].ascendant.uuid)
3015
+ ancestorNodes: ancestors2.map(
3016
+ ({ ascendant }, index) => relative(ascendant, index === 0 ? null : ancestors2[index - 1].ascendant.uuid)
2805
3017
  )
2806
3018
  };
2807
3019
  }, [detail]);
2808
3020
  if (isError) {
2809
- return /* @__PURE__ */ (0, import_jsx_runtime34.jsx)("div", { role: "alert", className: "p-4 text-sm text-[var(--color-text)]", children: "This genre could not be loaded." });
3021
+ 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." });
2810
3022
  }
2811
3023
  if (isPending || !detail || !view) {
2812
- return /* @__PURE__ */ (0, import_jsx_runtime34.jsxs)("div", { className: "flex flex-col gap-3 p-4", "aria-busy": "true", "data-testid": "genre-detail-panel-skeleton", children: [
2813
- /* @__PURE__ */ (0, import_jsx_runtime34.jsx)(import_ui14.Skeleton, { className: "h-6 w-2/3" }),
2814
- /* @__PURE__ */ (0, import_jsx_runtime34.jsx)(import_ui14.Skeleton, { className: "h-4 w-full" }),
2815
- /* @__PURE__ */ (0, import_jsx_runtime34.jsx)(import_ui14.Skeleton, { className: "h-4 w-3/4" })
3024
+ 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: [
3025
+ /* @__PURE__ */ (0, import_jsx_runtime35.jsx)(import_ui16.Skeleton, { className: "h-6 w-2/3" }),
3026
+ /* @__PURE__ */ (0, import_jsx_runtime35.jsx)(import_ui16.Skeleton, { className: "h-4 w-full" }),
3027
+ /* @__PURE__ */ (0, import_jsx_runtime35.jsx)(import_ui16.Skeleton, { className: "h-4 w-3/4" })
2816
3028
  ] });
2817
3029
  }
2818
- const renderBody = (node, sections) => ({
2819
- primary: /* @__PURE__ */ (0, import_jsx_runtime34.jsx)(
2820
- GenreInfoPanelBody,
2821
- {
2822
- toolbar: sections.toolbar,
2823
- family: sections.family,
2824
- genreUuid: detail.uuid,
2825
- genrePlaylist: { ...detail.criteriaPlaylist, tracksCount: detail.tracksCount },
2826
- wikidataId: detail.wikidataId,
2827
- scope,
2828
- getBackendBaseUrl,
2829
- playback,
2830
- band: { node, onZoomIn, shareUrl, shareTarget: shareUrl ? shareTarget : void 0 },
2831
- extras
2832
- }
2833
- ),
2834
- aside: null
2835
- });
2836
- return /* @__PURE__ */ (0, import_jsx_runtime34.jsx)(
2837
- import_genre_tree_view5.InfoPanel,
3030
+ const selectGenre = (nodeId) => onSelectGenre(nodeId);
3031
+ const family = /* @__PURE__ */ (0, import_jsx_runtime35.jsx)(
3032
+ import_genre_tree_view5.GenreNodeFamily,
2838
3033
  {
2839
3034
  node: view.node,
2840
3035
  fill: view.fill,
2841
3036
  textColor: import_genre_tree_view5.ACCENT_TEXT_COLOR,
2842
3037
  parentNode: view.parentNode,
2843
3038
  childNodes: view.childNodes,
2844
- ancestorNodes: view.ancestorNodes,
3039
+ onSelectNode: selectGenre
3040
+ }
3041
+ );
3042
+ const ancestors = /* @__PURE__ */ (0, import_jsx_runtime35.jsx)(import_genre_tree_view5.GenreNodeAncestors, { ancestorNodes: view.ancestorNodes, onSelectNode: selectGenre });
3043
+ return /* @__PURE__ */ (0, import_jsx_runtime35.jsxs)(
3044
+ import_ui16.DetailPanel,
3045
+ {
2845
3046
  side: "left",
3047
+ title: view.node.name,
3048
+ headerAccent: { background: view.fill, color: import_genre_tree_view5.ACCENT_TEXT_COLOR },
3049
+ headerActions: onExpandedChange ? /* @__PURE__ */ (0, import_jsx_runtime35.jsx)(
3050
+ import_ui16.IconButton,
3051
+ {
3052
+ "aria-label": expanded ? "Shrink panel" : "Extend panel",
3053
+ style: { color: "inherit" },
3054
+ onClick: () => onExpandedChange(!expanded),
3055
+ 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" })
3056
+ }
3057
+ ) : void 0,
2846
3058
  onClose,
2847
- onSelectNode: (nodeId) => onSelectGenre(nodeId),
2848
- itemCountLabel: ITEM_COUNT_LABEL,
2849
- renderInfoPanelBody: renderBody,
3059
+ collapsed,
2850
3060
  expanded,
2851
3061
  onExpandedChange,
2852
- collapsed,
2853
- hideExpandButton: onExpandedChange === void 0
3062
+ edgeTab,
3063
+ aside: ancestors,
3064
+ children: [
3065
+ /* @__PURE__ */ (0, import_jsx_runtime35.jsx)(
3066
+ GenreInfoPanelBody,
3067
+ {
3068
+ family,
3069
+ genreUuid: detail.uuid,
3070
+ genrePlaylist: { ...detail.criteriaPlaylist, tracksCount: detail.tracksCount },
3071
+ wikidataId: detail.wikidataId,
3072
+ scope,
3073
+ getBackendBaseUrl,
3074
+ playback,
3075
+ band: { node: view.node, onZoomIn, shareUrl, shareTarget: shareUrl ? shareTarget : void 0 },
3076
+ extras
3077
+ }
3078
+ ),
3079
+ !expanded && ancestors
3080
+ ]
2854
3081
  }
2855
3082
  );
2856
3083
  }
@@ -2859,13 +3086,13 @@ function GenreDetailPanel({
2859
3086
  var import_genre_tree_view6 = require("@behindthemusictree/genre-tree-view");
2860
3087
 
2861
3088
  // src/genre-tree/track-list-sidebar/TrackListSidebar.tsx
2862
- var import_react17 = require("react");
2863
- var import_lucide_react12 = require("lucide-react");
2864
- var import_genre_tree19 = require("@behindthemusictree/app-kit/genre-tree");
2865
- var import_genre_tree20 = require("@behindthemusictree/app-kit/genre-tree");
2866
- var import_track_noun6 = require("@behindthemusictree/app-kit/track-noun");
2867
- var import_jsx_runtime35 = require("react/jsx-runtime");
2868
- var useBrowserLayoutEffect = typeof window === "undefined" ? import_react17.useEffect : import_react17.useLayoutEffect;
3089
+ var import_react18 = require("react");
3090
+ var import_lucide_react14 = require("lucide-react");
3091
+ var import_genre_tree21 = require("@behindthemusictree/app-kit/genre-tree");
3092
+ var import_genre_tree22 = require("@behindthemusictree/app-kit/genre-tree");
3093
+ var import_track_noun3 = require("@behindthemusictree/app-kit/track-noun");
3094
+ var import_jsx_runtime36 = require("react/jsx-runtime");
3095
+ var useBrowserLayoutEffect = typeof window === "undefined" ? import_react18.useEffect : import_react18.useLayoutEffect;
2869
3096
  function TrackListSidebar({
2870
3097
  className: className2 = "",
2871
3098
  renderDuration,
@@ -2874,20 +3101,21 @@ function TrackListSidebar({
2874
3101
  isCollapsed = false,
2875
3102
  onExpand,
2876
3103
  onMinimize,
2877
- renderArtistLink
3104
+ renderArtistLink,
3105
+ renderSongLink
2878
3106
  }) {
2879
- const { trackList, loadMore, autoAdvanceScrollRequest } = (0, import_genre_tree19.useTrackList)();
2880
- const trackNoun = (0, import_track_noun6.useTrackNoun)();
2881
- const trackListRef = (0, import_react17.useRef)(null);
2882
- const previousOriginRef = (0, import_react17.useRef)(trackList?.origin ?? null);
2883
- const handledAutoAdvanceRequestIdRef = (0, import_react17.useRef)(null);
2884
- const shouldFollowPlaybackRef = (0, import_react17.useRef)(true);
2885
- const programmaticScrollTopRef = (0, import_react17.useRef)(null);
2886
- const userScrollIntentRef = (0, import_react17.useRef)(false);
2887
- const userScrollIntentTimerRef = (0, import_react17.useRef)(null);
2888
- const loadMoreSentinelRef = (0, import_react17.useRef)(null);
3107
+ const { trackList, loadMore, autoAdvanceScrollRequest } = (0, import_genre_tree21.useTrackList)();
3108
+ const trackNoun = (0, import_track_noun3.useTrackNoun)();
3109
+ const trackListRef = (0, import_react18.useRef)(null);
3110
+ const previousOriginRef = (0, import_react18.useRef)(trackList?.origin ?? null);
3111
+ const handledAutoAdvanceRequestIdRef = (0, import_react18.useRef)(null);
3112
+ const shouldFollowPlaybackRef = (0, import_react18.useRef)(true);
3113
+ const programmaticScrollTopRef = (0, import_react18.useRef)(null);
3114
+ const userScrollIntentRef = (0, import_react18.useRef)(false);
3115
+ const userScrollIntentTimerRef = (0, import_react18.useRef)(null);
3116
+ const loadMoreSentinelRef = (0, import_react18.useRef)(null);
2889
3117
  const hasMore = !!trackList && trackList.nextPage !== null;
2890
- (0, import_react17.useEffect)(() => {
3118
+ (0, import_react18.useEffect)(() => {
2891
3119
  const sentinel = loadMoreSentinelRef.current;
2892
3120
  if (!hasMore || !sentinel) return;
2893
3121
  const observer = new IntersectionObserver((entries) => {
@@ -2907,11 +3135,11 @@ function TrackListSidebar({
2907
3135
  programmaticScrollTopRef.current = null;
2908
3136
  if (trackListRef.current) trackListRef.current.scrollTop = 0;
2909
3137
  }, [trackList?.origin]);
2910
- (0, import_react17.useEffect)(() => {
3138
+ (0, import_react18.useEffect)(() => {
2911
3139
  const list = trackListRef.current;
2912
3140
  if (!isCollapsed && list?.scrollTop === 0) shouldFollowPlaybackRef.current = true;
2913
3141
  }, [isCollapsed]);
2914
- (0, import_react17.useEffect)(
3142
+ (0, import_react18.useEffect)(
2915
3143
  () => () => {
2916
3144
  if (userScrollIntentTimerRef.current !== null) clearTimeout(userScrollIntentTimerRef.current);
2917
3145
  },
@@ -2965,25 +3193,25 @@ function TrackListSidebar({
2965
3193
  }
2966
3194
  };
2967
3195
  const positionClasses = layout === "inline" ? "relative w-full flex h-full flex-col" : "fixed right-0 w-[50rem] flex flex-col";
2968
- return trackList ? /* @__PURE__ */ (0, import_jsx_runtime35.jsxs)(
3196
+ return trackList ? /* @__PURE__ */ (0, import_jsx_runtime36.jsxs)(
2969
3197
  "div",
2970
3198
  {
2971
3199
  "data-theme": "dark",
2972
3200
  className: `track-list-sidebar ${positionClasses} rounded-2xl bg-[var(--color-surface)] pb-1 ${className2}`,
2973
3201
  style: layout === "fixed" ? { top: "63.5px", maxHeight: "calc(100vh - 63.5px - 79px)" } : void 0,
2974
3202
  children: [
2975
- /* @__PURE__ */ (0, import_jsx_runtime35.jsxs)("div", { className: "header flex h-16 items-center px-4 py-2 text-[var(--color-text)]", children: [
2976
- /* @__PURE__ */ (0, import_jsx_runtime35.jsxs)("div", { className: "min-w-0 flex flex-1 flex-col justify-center", children: [
2977
- /* @__PURE__ */ (0, import_jsx_runtime35.jsxs)("div", { className: "origin flex text-xl", children: [
2978
- /* @__PURE__ */ (0, import_jsx_runtime35.jsx)("div", { className: "from h-auto flex flex-col justify-center items-center mr-2", children: "From" }),
2979
- /* @__PURE__ */ (0, import_jsx_runtime35.jsx)("div", { className: "name-container flex flex-col justify-center items-center text-[var(--color-text)] font-bold pr-2 max-w-[27.5rem]", children: /* @__PURE__ */ (0, import_jsx_runtime35.jsx)("div", { className: "name max-w-full truncate", children: trackList.origin.label }) })
3203
+ /* @__PURE__ */ (0, import_jsx_runtime36.jsxs)("div", { className: "header flex h-16 items-center px-4 py-2 text-[var(--color-text)]", children: [
3204
+ /* @__PURE__ */ (0, import_jsx_runtime36.jsxs)("div", { className: "min-w-0 flex flex-1 flex-col justify-center", children: [
3205
+ /* @__PURE__ */ (0, import_jsx_runtime36.jsxs)("div", { className: "origin flex text-xl", children: [
3206
+ /* @__PURE__ */ (0, import_jsx_runtime36.jsx)("div", { className: "from h-auto flex flex-col justify-center items-center mr-2", children: "From" }),
3207
+ /* @__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 }) })
2980
3208
  ] }),
2981
- /* @__PURE__ */ (0, import_jsx_runtime35.jsxs)("div", { className: "info flex items-center text-sm text-[var(--color-text-muted)]", children: [
2982
- trackList.origin.type === import_genre_tree20.TrackListOriginType.GENRE_PLAYLIST ? "Genre \u2022 " : trackList.origin.type === import_genre_tree20.TrackListOriginType.ARTIST ? "Artist \u2022 " : `${trackNoun.singular} playlist \u2022 `,
3209
+ /* @__PURE__ */ (0, import_jsx_runtime36.jsxs)("div", { className: "info flex items-center text-sm text-[var(--color-text-muted)]", children: [
3210
+ 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 `,
2983
3211
  `${trackList.total} ${trackList.total > 1 ? trackNoun.plural : trackNoun.singular}`
2984
3212
  ] })
2985
3213
  ] }),
2986
- (isCollapsed && onExpand || !isCollapsed && onMinimize) && /* @__PURE__ */ (0, import_jsx_runtime35.jsx)(
3214
+ (isCollapsed && onExpand || !isCollapsed && onMinimize) && /* @__PURE__ */ (0, import_jsx_runtime36.jsx)(
2987
3215
  "button",
2988
3216
  {
2989
3217
  type: "button",
@@ -2992,11 +3220,11 @@ function TrackListSidebar({
2992
3220
  "aria-expanded": !isCollapsed,
2993
3221
  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"}`,
2994
3222
  onClick: isCollapsed ? onExpand : onMinimize,
2995
- children: /* @__PURE__ */ (0, import_jsx_runtime35.jsx)(import_lucide_react12.ListMusic, { "aria-hidden": "true", className: "h-5 w-5" })
3223
+ children: /* @__PURE__ */ (0, import_jsx_runtime36.jsx)(import_lucide_react14.ListMusic, { "aria-hidden": "true", className: "h-5 w-5" })
2996
3224
  }
2997
3225
  )
2998
3226
  ] }),
2999
- !isCollapsed && /* @__PURE__ */ (0, import_jsx_runtime35.jsxs)(
3227
+ !isCollapsed && /* @__PURE__ */ (0, import_jsx_runtime36.jsxs)(
3000
3228
  "ul",
3001
3229
  {
3002
3230
  ref: trackListRef,
@@ -3006,17 +3234,18 @@ function TrackListSidebar({
3006
3234
  onPointerDown: markUserScrollIntent,
3007
3235
  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",
3008
3236
  children: [
3009
- trackList.tracks.map((track, index) => /* @__PURE__ */ (0, import_jsx_runtime35.jsx)("li", { "data-track-uuid": track.uuid, children: /* @__PURE__ */ (0, import_jsx_runtime35.jsx)(
3237
+ trackList.tracks.map((track, index) => /* @__PURE__ */ (0, import_jsx_runtime36.jsx)("li", { "data-track-uuid": track.uuid, children: /* @__PURE__ */ (0, import_jsx_runtime36.jsx)(
3010
3238
  TrackItem,
3011
3239
  {
3012
3240
  track,
3013
3241
  position: index + 1,
3014
3242
  renderDuration,
3015
3243
  renderActions,
3016
- renderArtistLink
3244
+ renderArtistLink,
3245
+ renderSongLink
3017
3246
  }
3018
3247
  ) }, track.uuid)),
3019
- hasMore && /* @__PURE__ */ (0, import_jsx_runtime35.jsx)("li", { ref: loadMoreSentinelRef, "aria-hidden": "true", className: "h-px" })
3248
+ hasMore && /* @__PURE__ */ (0, import_jsx_runtime36.jsx)("li", { ref: loadMoreSentinelRef, "aria-hidden": "true", className: "h-px" })
3020
3249
  ]
3021
3250
  }
3022
3251
  )
@@ -3026,16 +3255,17 @@ function TrackListSidebar({
3026
3255
  }
3027
3256
 
3028
3257
  // src/genre-tree/PlayerPanel.tsx
3029
- var import_react18 = require("react");
3030
- var import_tailwind_merge2 = require("tailwind-merge");
3031
- var import_player12 = require("@behindthemusictree/app-kit/player");
3032
- var import_genre_tree22 = require("@behindthemusictree/app-kit/genre-tree");
3258
+ var import_react19 = require("react");
3259
+ var import_tailwind_merge = require("tailwind-merge");
3260
+ var import_player13 = require("@behindthemusictree/app-kit/player");
3261
+ var import_genre_tree24 = require("@behindthemusictree/app-kit/genre-tree");
3262
+ var import_ui17 = require("@behindthemusictree/ui");
3033
3263
 
3034
3264
  // src/genre-tree/PlayerControls.tsx
3035
- var import_lucide_react13 = require("lucide-react");
3036
- var import_genre_tree21 = require("@behindthemusictree/app-kit/genre-tree");
3037
- var import_player11 = require("@behindthemusictree/app-kit/player");
3038
- var import_jsx_runtime36 = require("react/jsx-runtime");
3265
+ var import_lucide_react15 = require("lucide-react");
3266
+ var import_genre_tree23 = require("@behindthemusictree/app-kit/genre-tree");
3267
+ var import_player12 = require("@behindthemusictree/app-kit/player");
3268
+ var import_jsx_runtime37 = require("react/jsx-runtime");
3039
3269
  function PlayerControls() {
3040
3270
  const {
3041
3271
  duration,
@@ -3046,23 +3276,23 @@ function PlayerControls() {
3046
3276
  seekTo,
3047
3277
  setVolume,
3048
3278
  volume
3049
- } = (0, import_player11.usePlayer)();
3050
- const { hasNextTrack, hasPreviousTrack, playNextTrack, playPreviousTrack } = (0, import_genre_tree21.useTrackList)();
3051
- const currentTime = (0, import_player11.useCurrentTime)();
3052
- const isBusy = isLoading || playState === import_player11.PlayStates.LOADING;
3279
+ } = (0, import_player12.usePlayer)();
3280
+ const { hasNextTrack, hasPreviousTrack, playNextTrack, playPreviousTrack } = (0, import_genre_tree23.useTrackList)();
3281
+ const currentTime = (0, import_player12.useCurrentTime)();
3282
+ const isBusy = isLoading || playState === import_player12.PlayStates.LOADING;
3053
3283
  const isReady = !!playerTrackObject?.isReady && !isBusy && !playerTrackObject.loadError;
3054
3284
  const safeDuration = Number.isFinite(duration) && duration > 0 ? duration : 0;
3055
3285
  const safeCurrentTime = Number.isFinite(currentTime) ? Math.max(0, Math.min(currentTime, safeDuration)) : 0;
3056
- const isPlaying = playState === import_player11.PlayStates.PLAYING;
3057
- return /* @__PURE__ */ (0, import_jsx_runtime36.jsxs)(
3286
+ const isPlaying = playState === import_player12.PlayStates.PLAYING;
3287
+ return /* @__PURE__ */ (0, import_jsx_runtime37.jsxs)(
3058
3288
  "section",
3059
3289
  {
3060
3290
  "aria-label": "Player controls",
3061
3291
  className: "player-controls flex shrink-0 flex-col gap-2 border-b border-zinc-800 px-3 py-2 text-zinc-200",
3062
3292
  children: [
3063
- /* @__PURE__ */ (0, import_jsx_runtime36.jsxs)("div", { className: "flex min-w-0 items-center gap-2 text-xs tabular-nums text-zinc-400", children: [
3064
- /* @__PURE__ */ (0, import_jsx_runtime36.jsx)("span", { "aria-label": "Elapsed time", children: (0, import_genre_tree21.formatTime)(safeCurrentTime) }),
3065
- /* @__PURE__ */ (0, import_jsx_runtime36.jsx)(
3293
+ /* @__PURE__ */ (0, import_jsx_runtime37.jsxs)("div", { className: "flex min-w-0 items-center gap-2 text-xs tabular-nums text-zinc-400", children: [
3294
+ /* @__PURE__ */ (0, import_jsx_runtime37.jsx)("span", { "aria-label": "Elapsed time", children: (0, import_genre_tree23.formatTime)(safeCurrentTime) }),
3295
+ /* @__PURE__ */ (0, import_jsx_runtime37.jsx)(
3066
3296
  "input",
3067
3297
  {
3068
3298
  "aria-label": "Seek",
@@ -3076,11 +3306,11 @@ function PlayerControls() {
3076
3306
  onChange: (event) => seekTo(Number(event.currentTarget.value))
3077
3307
  }
3078
3308
  ),
3079
- /* @__PURE__ */ (0, import_jsx_runtime36.jsx)("span", { "aria-label": "Duration", children: (0, import_genre_tree21.formatTime)(safeDuration) })
3309
+ /* @__PURE__ */ (0, import_jsx_runtime37.jsx)("span", { "aria-label": "Duration", children: (0, import_genre_tree23.formatTime)(safeDuration) })
3080
3310
  ] }),
3081
- /* @__PURE__ */ (0, import_jsx_runtime36.jsxs)("div", { className: "grid min-h-9 grid-cols-[1fr_auto_1fr] items-center gap-3", children: [
3082
- /* @__PURE__ */ (0, import_jsx_runtime36.jsxs)("div", { className: "col-start-2 flex items-center justify-center gap-2", children: [
3083
- /* @__PURE__ */ (0, import_jsx_runtime36.jsx)(
3311
+ /* @__PURE__ */ (0, import_jsx_runtime37.jsxs)("div", { className: "grid min-h-9 grid-cols-[1fr_auto_1fr] items-center gap-3", children: [
3312
+ /* @__PURE__ */ (0, import_jsx_runtime37.jsxs)("div", { className: "col-start-2 flex items-center justify-center gap-2", children: [
3313
+ /* @__PURE__ */ (0, import_jsx_runtime37.jsx)(
3084
3314
  "button",
3085
3315
  {
3086
3316
  type: "button",
@@ -3089,10 +3319,10 @@ function PlayerControls() {
3089
3319
  disabled: !isReady || !hasPreviousTrack,
3090
3320
  onClick: playPreviousTrack,
3091
3321
  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",
3092
- children: /* @__PURE__ */ (0, import_jsx_runtime36.jsx)(import_lucide_react13.SkipBack, { "aria-hidden": "true", className: "h-4 w-4" })
3322
+ children: /* @__PURE__ */ (0, import_jsx_runtime37.jsx)(import_lucide_react15.SkipBack, { "aria-hidden": "true", className: "h-4 w-4" })
3093
3323
  }
3094
3324
  ),
3095
- /* @__PURE__ */ (0, import_jsx_runtime36.jsx)(
3325
+ /* @__PURE__ */ (0, import_jsx_runtime37.jsx)(
3096
3326
  "button",
3097
3327
  {
3098
3328
  type: "button",
@@ -3101,10 +3331,10 @@ function PlayerControls() {
3101
3331
  disabled: !isReady,
3102
3332
  onClick: handlePlayPauseAction,
3103
3333
  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",
3104
- children: isPlaying ? /* @__PURE__ */ (0, import_jsx_runtime36.jsx)(import_lucide_react13.Pause, { "aria-hidden": "true", className: "h-4 w-4", fill: "currentColor" }) : /* @__PURE__ */ (0, import_jsx_runtime36.jsx)(import_lucide_react13.Play, { "aria-hidden": "true", className: "h-4 w-4", fill: "currentColor" })
3334
+ 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" })
3105
3335
  }
3106
3336
  ),
3107
- /* @__PURE__ */ (0, import_jsx_runtime36.jsx)(
3337
+ /* @__PURE__ */ (0, import_jsx_runtime37.jsx)(
3108
3338
  "button",
3109
3339
  {
3110
3340
  type: "button",
@@ -3113,14 +3343,14 @@ function PlayerControls() {
3113
3343
  disabled: !isReady || !hasNextTrack,
3114
3344
  onClick: playNextTrack,
3115
3345
  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",
3116
- children: /* @__PURE__ */ (0, import_jsx_runtime36.jsx)(import_lucide_react13.SkipForward, { "aria-hidden": "true", className: "h-4 w-4" })
3346
+ children: /* @__PURE__ */ (0, import_jsx_runtime37.jsx)(import_lucide_react15.SkipForward, { "aria-hidden": "true", className: "h-4 w-4" })
3117
3347
  }
3118
3348
  )
3119
3349
  ] }),
3120
- /* @__PURE__ */ (0, import_jsx_runtime36.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: [
3121
- volume === 0 ? /* @__PURE__ */ (0, import_jsx_runtime36.jsx)(import_lucide_react13.VolumeX, { "aria-hidden": "true", className: "h-[0.7rem] w-[0.7rem] shrink-0" }) : /* @__PURE__ */ (0, import_jsx_runtime36.jsx)(import_lucide_react13.Volume2, { "aria-hidden": "true", className: "h-[0.7rem] w-[0.7rem] shrink-0" }),
3122
- /* @__PURE__ */ (0, import_jsx_runtime36.jsx)("span", { className: "sr-only", children: "Volume" }),
3123
- /* @__PURE__ */ (0, import_jsx_runtime36.jsx)(
3350
+ /* @__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: [
3351
+ 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" }),
3352
+ /* @__PURE__ */ (0, import_jsx_runtime37.jsx)("span", { className: "sr-only", children: "Volume" }),
3353
+ /* @__PURE__ */ (0, import_jsx_runtime37.jsx)(
3124
3354
  "input",
3125
3355
  {
3126
3356
  "aria-label": "Volume",
@@ -3142,7 +3372,7 @@ function PlayerControls() {
3142
3372
  }
3143
3373
 
3144
3374
  // src/genre-tree/PlayerPanel.tsx
3145
- var import_jsx_runtime37 = require("react/jsx-runtime");
3375
+ var import_jsx_runtime38 = require("react/jsx-runtime");
3146
3376
  var PANEL_ID = "player-panel";
3147
3377
  var DEFAULT_EXTENDED_WIDTH = "calc(100vw - var(--layout-center-left) - var(--layout-gutter))";
3148
3378
  function PlayerPanel({
@@ -3151,16 +3381,17 @@ function PlayerPanel({
3151
3381
  expanded,
3152
3382
  onExpandedChange,
3153
3383
  extendedWidth = DEFAULT_EXTENDED_WIDTH,
3154
- renderArtistLink
3384
+ renderArtistLink,
3385
+ renderSongLink
3155
3386
  }) {
3156
- const { playerTrackObject } = (0, import_player12.usePlayer)();
3157
- const { toggleTrackListSidebar, isTrackListSidebarVisible } = (0, import_genre_tree22.useTrackListSidebarVisibility)();
3158
- const [lastLoadedTrackTitle, setLastLoadedTrackTitle] = (0, import_react18.useState)("");
3387
+ const { playerTrackObject } = (0, import_player13.usePlayer)();
3388
+ const { toggleTrackListSidebar, isTrackListSidebarVisible } = (0, import_genre_tree24.useTrackListSidebarVisibility)();
3389
+ const [lastLoadedTrackTitle, setLastLoadedTrackTitle] = (0, import_react19.useState)("");
3159
3390
  const loadedTrackTitle = playerTrackObject?.track.title;
3160
- (0, import_react18.useEffect)(() => {
3391
+ (0, import_react19.useEffect)(() => {
3161
3392
  if (loadedTrackTitle) setLastLoadedTrackTitle(loadedTrackTitle);
3162
3393
  }, [loadedTrackTitle]);
3163
- const [uncontrolledExtended, setUncontrolledExtended] = (0, import_react18.useState)(false);
3394
+ const [uncontrolledExtended, setUncontrolledExtended] = (0, import_react19.useState)(false);
3164
3395
  const isExtendRequested = expanded ?? uncontrolledExtended;
3165
3396
  const setIsExtendRequested = (next) => {
3166
3397
  setUncontrolledExtended(next);
@@ -3181,11 +3412,11 @@ function PlayerPanel({
3181
3412
  onClick: () => setIsExtendRequested(true),
3182
3413
  expanded: false
3183
3414
  } : null;
3184
- return /* @__PURE__ */ (0, import_jsx_runtime37.jsxs)(
3415
+ return /* @__PURE__ */ (0, import_jsx_runtime38.jsxs)(
3185
3416
  "div",
3186
3417
  {
3187
3418
  id: PANEL_ID,
3188
- className: (0, import_tailwind_merge2.twMerge)(
3419
+ className: (0, import_tailwind_merge.twMerge)(
3189
3420
  "fixed right-[var(--layout-gutter)] top-[var(--layout-band-top)] z-40 max-h-[calc(100vh-var(--layout-band-top)-3.5rem)]",
3190
3421
  !isExtended && "w-[var(--layout-right-w)]",
3191
3422
  isTrackListSidebarVisible && "h-[calc(100vh-var(--layout-band-top)-3.5rem)]",
@@ -3194,8 +3425,8 @@ function PlayerPanel({
3194
3425
  ),
3195
3426
  style: isExtended ? { width: extendedWidth } : void 0,
3196
3427
  children: [
3197
- /* @__PURE__ */ (0, import_jsx_runtime37.jsx)(
3198
- PanelEdgeTab,
3428
+ /* @__PURE__ */ (0, import_jsx_runtime38.jsx)(
3429
+ import_ui17.PanelEdgeTab,
3199
3430
  {
3200
3431
  side: "right",
3201
3432
  edge: tabEdge,
@@ -3204,34 +3435,35 @@ function PlayerPanel({
3204
3435
  className: "absolute right-full top-1/2 -translate-y-1/2"
3205
3436
  }
3206
3437
  ),
3207
- /* @__PURE__ */ (0, import_jsx_runtime37.jsxs)(
3438
+ /* @__PURE__ */ (0, import_jsx_runtime38.jsxs)(
3208
3439
  "div",
3209
3440
  {
3210
- className: (0, import_tailwind_merge2.twMerge)(
3441
+ className: (0, import_tailwind_merge.twMerge)(
3211
3442
  "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)]",
3212
3443
  isExtended ? "flex-row" : "flex-col"
3213
3444
  ),
3214
3445
  children: [
3215
- /* @__PURE__ */ (0, import_jsx_runtime37.jsxs)("div", { className: "flex w-[var(--layout-right-w)] shrink-0 flex-col", children: [
3216
- /* @__PURE__ */ (0, import_jsx_runtime37.jsx)("div", { className: "flex shrink-0 items-center justify-between gap-2 border-b border-zinc-800 p-2", children: /* @__PURE__ */ (0, import_jsx_runtime37.jsx)("span", { className: "min-w-0 truncate text-sm font-semibold", children: loadedTrackTitle || lastLoadedTrackTitle }) }),
3217
- /* @__PURE__ */ (0, import_jsx_runtime37.jsxs)("div", { className: "relative w-full shrink-0", children: [
3218
- /* @__PURE__ */ (0, import_jsx_runtime37.jsx)(import_player12.PlayerVideoSurface, { className: "aspect-video w-full bg-black" }),
3219
- playerTrackObject?.loadError && /* @__PURE__ */ (0, import_jsx_runtime37.jsx)("span", { className: "absolute bottom-2 left-2 right-2 text-xs text-red-400", children: playerTrackObject.loadError })
3446
+ /* @__PURE__ */ (0, import_jsx_runtime38.jsxs)("div", { className: "flex w-[var(--layout-right-w)] shrink-0 flex-col", children: [
3447
+ /* @__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 }) }),
3448
+ /* @__PURE__ */ (0, import_jsx_runtime38.jsxs)("div", { className: "relative w-full shrink-0", children: [
3449
+ /* @__PURE__ */ (0, import_jsx_runtime38.jsx)(import_player13.PlayerVideoSurface, { className: "aspect-video w-full bg-black" }),
3450
+ 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 })
3220
3451
  ] }),
3221
- /* @__PURE__ */ (0, import_jsx_runtime37.jsx)(PlayerControls, {})
3452
+ /* @__PURE__ */ (0, import_jsx_runtime38.jsx)(PlayerControls, {})
3222
3453
  ] }),
3223
- /* @__PURE__ */ (0, import_jsx_runtime37.jsx)(
3454
+ /* @__PURE__ */ (0, import_jsx_runtime38.jsx)(
3224
3455
  TrackListSidebar,
3225
3456
  {
3226
3457
  layout: "inline",
3227
3458
  isCollapsed: !isTrackListSidebarVisible,
3228
3459
  onExpand: toggleTrackListSidebar,
3229
3460
  onMinimize: hideSongList,
3230
- className: (0, import_tailwind_merge2.twMerge)(
3461
+ className: (0, import_tailwind_merge.twMerge)(
3231
3462
  "min-h-0 min-w-0 flex-1",
3232
3463
  isExtended && "border-l border-zinc-800"
3233
3464
  ),
3234
- renderArtistLink
3465
+ renderArtistLink,
3466
+ renderSongLink
3235
3467
  }
3236
3468
  )
3237
3469
  ]
@@ -3245,33 +3477,33 @@ function PlayerPanel({
3245
3477
  // src/genre-tree/Rating.tsx
3246
3478
  var import_uuid = require("uuid");
3247
3479
  var import_ai = require("react-icons/ai");
3248
- var import_genre_tree23 = require("@behindthemusictree/app-kit/genre-tree");
3249
- var import_jsx_runtime38 = require("react/jsx-runtime");
3480
+ var import_genre_tree25 = require("@behindthemusictree/app-kit/genre-tree");
3481
+ var import_jsx_runtime39 = require("react/jsx-runtime");
3250
3482
  function Rating({ rating, handleChange }) {
3251
3483
  const instanceId = (0, import_uuid.v4)();
3252
- return /* @__PURE__ */ (0, import_jsx_runtime38.jsx)("div", { className: "flex items-center", children: [...Array(6)].map((_, i) => /* @__PURE__ */ (0, import_jsx_runtime38.jsxs)("div", { className: `relative ${i === 0 ? "mr-4" : "mr-1"}`, children: [
3253
- /* @__PURE__ */ (0, import_jsx_runtime38.jsx)(
3484
+ 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: [
3485
+ /* @__PURE__ */ (0, import_jsx_runtime39.jsx)(
3254
3486
  "input",
3255
3487
  {
3256
3488
  id: `star-${instanceId}-${i}`,
3257
3489
  className: "w-6 h-6 opacity-0",
3258
3490
  type: "radio",
3259
3491
  name: "rating",
3260
- value: i === 0 ? import_genre_tree23.FORM_RATING_NULL_VALUE : i,
3492
+ value: i === 0 ? import_genre_tree25.FORM_RATING_NULL_VALUE : i,
3261
3493
  checked: rating === (i === 0 ? null : i),
3262
3494
  onChange: handleChange
3263
3495
  }
3264
3496
  ),
3265
- /* @__PURE__ */ (0, import_jsx_runtime38.jsx)("label", { htmlFor: `star-${instanceId}-${i}`, className: "cursor-pointer", children: i === 0 ? /* @__PURE__ */ (0, import_jsx_runtime38.jsxs)("div", { className: "flex", children: [
3266
- /* @__PURE__ */ (0, import_jsx_runtime38.jsx)(
3497
+ /* @__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: [
3498
+ /* @__PURE__ */ (0, import_jsx_runtime39.jsx)(
3267
3499
  import_ai.AiOutlineStar,
3268
3500
  {
3269
- className: `w-7 h-7 absolute top-0 left-0 ${rating === import_genre_tree23.FORM_RATING_NULL_VALUE ? "text-[var(--color-accent-rating)]" : "text-[var(--color-text-muted)]"}`
3501
+ 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)]"}`
3270
3502
  }
3271
3503
  ),
3272
- /* @__PURE__ */ (0, import_jsx_runtime38.jsx)("span", { className: "flex-grow w-8" }),
3504
+ /* @__PURE__ */ (0, import_jsx_runtime39.jsx)("span", { className: "flex-grow w-8" }),
3273
3505
  " "
3274
- ] }) : /* @__PURE__ */ (0, import_jsx_runtime38.jsx)(
3506
+ ] }) : /* @__PURE__ */ (0, import_jsx_runtime39.jsx)(
3275
3507
  import_ai.AiFillStar,
3276
3508
  {
3277
3509
  className: `w-7 h-7 absolute top-0 left-0 ${(rating ?? -1) >= i ? "text-[var(--color-accent-rating)]" : "text-[var(--color-text-muted)]"}`
@@ -3291,13 +3523,11 @@ function Rating({ rating, handleChange }) {
3291
3523
  GenreTreeView,
3292
3524
  GenreTreeViewSkeleton,
3293
3525
  GenreTreeWheelSkeleton,
3294
- PanelEdgeTab,
3295
3526
  PlayerPanel,
3296
3527
  Rating,
3297
3528
  TrackItem,
3298
3529
  TrackListSidebar,
3299
3530
  TrackPositionPlayPause,
3300
- useGenrePlaylistPlayback,
3301
- useLeftDetailPanelLayout
3531
+ useGenrePlaylistPlayback
3302
3532
  });
3303
3533
  //# sourceMappingURL=index.cjs.map