@behindthemusictree/app-kit-ui 28.0.2 → 29.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.cjs CHANGED
@@ -24,13 +24,16 @@ __export(src_exports, {
24
24
  AuthErrorPopup: () => AuthErrorPopup,
25
25
  AuthPopup: () => AuthPopup,
26
26
  BasePopup: () => BasePopup,
27
+ GenreDetailPanel: () => GenreDetailPanel,
28
+ GenreInfoPanelBody: () => GenreInfoPanelBody,
29
+ GenreNodeActionBand: () => GenreNodeActionBand,
27
30
  GenrePlaylistTreePerRoot: () => GenrePlaylistTreePerRoot,
28
31
  GenreSearch: () => GenreSearch,
29
- GenreTreeOutlineSkeleton: () => import_genre_tree_view5.GenreTreeOutlineSkeleton,
30
- GenreTreeSkeleton: () => import_genre_tree_view5.GenreTreeSkeleton,
32
+ GenreTreeOutlineSkeleton: () => import_genre_tree_view6.GenreTreeOutlineSkeleton,
33
+ GenreTreeSkeleton: () => import_genre_tree_view6.GenreTreeSkeleton,
31
34
  GenreTreeView: () => GenreTreeView,
32
- GenreTreeViewSkeleton: () => import_genre_tree_view5.GenreTreeViewSkeleton,
33
- GenreTreeWheelSkeleton: () => import_genre_tree_view5.GenreTreeWheelSkeleton,
35
+ GenreTreeViewSkeleton: () => import_genre_tree_view6.GenreTreeViewSkeleton,
36
+ GenreTreeWheelSkeleton: () => import_genre_tree_view6.GenreTreeWheelSkeleton,
34
37
  InternalErrorPopup: () => InternalErrorPopup,
35
38
  NetworkErrorPopup: () => NetworkErrorPopup,
36
39
  PanelEdgeTab: () => PanelEdgeTab,
@@ -43,7 +46,9 @@ __export(src_exports, {
43
46
  TrackItem: () => TrackItem,
44
47
  TrackListSidebar: () => TrackListSidebar,
45
48
  TrackPositionPlayPause: () => TrackPositionPlayPause,
46
- TrackUploadPopup: () => TrackUploadPopup
49
+ TrackUploadPopup: () => TrackUploadPopup,
50
+ useGenrePlaylistPlayback: () => useGenrePlaylistPlayback,
51
+ useLeftDetailPanelLayout: () => useLeftDetailPanelLayout
47
52
  });
48
53
  module.exports = __toCommonJS(src_exports);
49
54
 
@@ -575,419 +580,238 @@ function AuthCallbackHandler({ renderStatus, ...options }) {
575
580
  }
576
581
 
577
582
  // src/genre-tree/GenreTreeView.tsx
578
- var import_react13 = require("react");
583
+ var import_react15 = require("react");
579
584
  var import_react_query3 = require("@tanstack/react-query");
580
585
  var import_lucide_react15 = require("lucide-react");
581
586
  var import_ui16 = require("@behindthemusictree/ui");
582
587
  var import_genre_tree_view4 = require("@behindthemusictree/genre-tree-view");
583
588
  var import_genre_tree16 = require("@behindthemusictree/app-kit/genre-tree");
584
589
  var import_genre_tree17 = require("@behindthemusictree/app-kit/genre-tree");
585
- var import_player10 = require("@behindthemusictree/app-kit/player");
586
590
  var import_popup6 = require("@behindthemusictree/app-kit/popup");
587
591
 
588
- // src/genre-tree/GenreDetailExtras.tsx
589
- var import_ui7 = require("@behindthemusictree/ui");
590
- var import_genre_tree7 = require("@behindthemusictree/app-kit/genre-tree");
591
-
592
- // src/genre-tree/ClampedText.tsx
593
- var import_react3 = require("react");
594
- var import_jsx_runtime12 = require("react/jsx-runtime");
595
- function ClampedText({ children, className: className2 = "" }) {
596
- const ref = (0, import_react3.useRef)(null);
597
- const [expanded, setExpanded] = (0, import_react3.useState)(false);
598
- const [overflows, setOverflows] = (0, import_react3.useState)(false);
599
- (0, import_react3.useLayoutEffect)(() => {
600
- const element = ref.current;
601
- if (!element || expanded) return;
602
- setOverflows(element.scrollHeight > element.clientHeight);
603
- }, [children, expanded]);
604
- return /* @__PURE__ */ (0, import_jsx_runtime12.jsxs)(import_jsx_runtime12.Fragment, { children: [
605
- /* @__PURE__ */ (0, import_jsx_runtime12.jsx)("p", { ref, className: `${className2} ${expanded ? "" : "line-clamp-3"}`, children }),
606
- (overflows || expanded) && /* @__PURE__ */ (0, import_jsx_runtime12.jsx)(
607
- "button",
608
- {
609
- type: "button",
610
- "aria-expanded": expanded,
611
- onClick: () => setExpanded((value) => !value),
612
- className: "mt-1 text-xs text-[var(--color-text-muted)] underline hover:text-[var(--color-text)]",
613
- children: expanded ? "Show less" : "Show more"
614
- }
615
- )
616
- ] });
617
- }
618
-
619
- // src/genre-tree/GenrePlaylistTracks.tsx
592
+ // src/genre-tree/playlist-tree/TreePerRoot.tsx
620
593
  var import_react4 = require("react");
621
- var import_react_query = require("@tanstack/react-query");
622
- var import_zod = require("zod");
623
- var import_fa3 = require("react-icons/fa");
624
- var import_ui6 = require("@behindthemusictree/ui");
625
- var import_auth2 = require("@behindthemusictree/app-kit/auth");
626
- var import_transport2 = require("@behindthemusictree/app-kit/transport");
627
- var import_transport3 = require("@behindthemusictree/app-kit/transport");
628
- var import_transport4 = require("@behindthemusictree/app-kit/transport");
594
+ var import_genre_tree_view = require("@behindthemusictree/genre-tree-view");
629
595
  var import_player = require("@behindthemusictree/app-kit/player");
630
- var import_player2 = require("@behindthemusictree/app-kit/player");
631
596
  var import_popup2 = require("@behindthemusictree/app-kit/popup");
632
- var import_transport5 = require("@behindthemusictree/app-kit/transport");
633
- var import_genre_tree2 = require("@behindthemusictree/app-kit/genre-tree");
634
- var import_genre_tree3 = require("@behindthemusictree/app-kit/genre-tree");
635
- var import_genre_tree4 = require("@behindthemusictree/app-kit/genre-tree");
636
- var import_genre_tree5 = require("@behindthemusictree/app-kit/genre-tree");
597
+ var import_transport2 = require("@behindthemusictree/app-kit/transport");
637
598
 
638
- // src/genre-tree/MusicBrainzRecordingLink.tsx
599
+ // src/genre-tree/playlist-tree/useGenrePlaylistPlayback.ts
600
+ var import_react3 = require("react");
639
601
  var import_genre_tree = require("@behindthemusictree/app-kit/genre-tree");
640
- var import_jsx_runtime13 = require("react/jsx-runtime");
641
- function MusicBrainzRecordingLink({
642
- song,
643
- children,
644
- className: className2 = ""
602
+ function useGenrePlaylistPlayback({
603
+ scope,
604
+ genrePlaylists,
605
+ artist = null
645
606
  }) {
646
- const url = (0, import_genre_tree.musicBrainzRecordingUrl)(song);
647
- if (!url) return /* @__PURE__ */ (0, import_jsx_runtime13.jsx)(import_jsx_runtime13.Fragment, { children });
648
- return /* @__PURE__ */ (0, import_jsx_runtime13.jsx)(
649
- "a",
607
+ const { trackList, playNewTrackListFromGenrePlaylist, playNewTrackListFromArtist } = (0, import_genre_tree.useTrackList)();
608
+ const origin = trackList?.origin;
609
+ const playingNodeId = (0, import_react3.useMemo)(() => {
610
+ if (!origin) return null;
611
+ if (!artist) return origin.type === import_genre_tree.TrackListOriginType.GENRE_PLAYLIST ? origin.uuid : null;
612
+ if (origin.type !== import_genre_tree.TrackListOriginType.ARTIST || origin.uuid !== artist.uuid) return null;
613
+ const { genreUuid } = origin;
614
+ return genreUuid ? genrePlaylists.find((gp) => gp.criteria?.uuid === genreUuid)?.uuid ?? null : null;
615
+ }, [origin, artist, genrePlaylists]);
616
+ const play = (0, import_react3.useCallback)(
617
+ async (genrePlaylist) => {
618
+ if (!artist) return playNewTrackListFromGenrePlaylist(genrePlaylist, scope);
619
+ if (!genrePlaylist.criteria) return;
620
+ return playNewTrackListFromArtist(artist, scope, { ordering: "title", genreUuid: genrePlaylist.criteria.uuid });
621
+ },
622
+ [artist, scope, playNewTrackListFromGenrePlaylist, playNewTrackListFromArtist]
623
+ );
624
+ return (0, import_react3.useMemo)(() => ({ playingNodeId, play }), [playingNodeId, play]);
625
+ }
626
+
627
+ // src/genre-tree/playlist-tree/TreePerRoot.tsx
628
+ var import_track_noun2 = require("@behindthemusictree/app-kit/track-noun");
629
+ var import_jsx_runtime12 = require("react/jsx-runtime");
630
+ function GenrePlaylistTreePerRoot({
631
+ scope,
632
+ className: className2,
633
+ rootUuid,
634
+ genrePlaylistTreePerRoot,
635
+ handleGenreCreationAction,
636
+ handleGenreRenameAction,
637
+ additionalActions,
638
+ onNodeClick,
639
+ onNodeHover,
640
+ renderInfoPanelBody,
641
+ selectedNodeId,
642
+ onSelectedNodeChange,
643
+ hideInfoPanelClose,
644
+ hideInfoPanelTitle,
645
+ readOnly = false,
646
+ showToolbar,
647
+ wheelZoom,
648
+ defaultViewport,
649
+ viewportRestore,
650
+ onViewportChange,
651
+ playback,
652
+ ...infoPanelCuration
653
+ }) {
654
+ const { isPlaying, setIsPlaying } = (0, import_player.usePlayer)();
655
+ const defaultPlayback = useGenrePlaylistPlayback({ scope, genrePlaylists: genrePlaylistTreePerRoot });
656
+ const { playingNodeId, play } = playback ?? defaultPlayback;
657
+ const trackNoun = (0, import_track_noun2.useTrackNoun)();
658
+ const { showPopup } = (0, import_popup2.usePopup)();
659
+ const nodes = (0, import_react4.useMemo)(
660
+ () => genrePlaylistTreePerRoot.map((genrePlaylist) => ({
661
+ id: genrePlaylist.uuid,
662
+ parentId: genrePlaylist.parent?.uuid ?? null,
663
+ name: genrePlaylist.name,
664
+ itemCount: genrePlaylist.tracksCount,
665
+ actionable: Boolean(genrePlaylist.criteria),
666
+ side: genrePlaylist.criteria?.side ?? void 0,
667
+ wikidataId: genrePlaylist.criteria?.wikidataId ?? null,
668
+ umbrella: genrePlaylist.isUmbrella
669
+ })),
670
+ [genrePlaylistTreePerRoot]
671
+ );
672
+ const handlePlayPause = (0, import_react4.useCallback)(
673
+ (nodeId) => {
674
+ const genrePlaylist = genrePlaylistTreePerRoot.find((g) => g.uuid === nodeId);
675
+ if (!genrePlaylist) return;
676
+ if (nodeId === playingNodeId) {
677
+ setIsPlaying(!isPlaying);
678
+ return;
679
+ }
680
+ if (genrePlaylist.tracksCount === 0) {
681
+ return;
682
+ }
683
+ play(genrePlaylist).catch((error) => {
684
+ console.error("Failed to load genre playlist tracks:", error);
685
+ showPopup(/* @__PURE__ */ (0, import_jsx_runtime12.jsx)(InternalErrorPopup, { errorCode: import_transport2.ErrorCode.CLIENT_UNKNOWN }));
686
+ });
687
+ },
688
+ [
689
+ genrePlaylistTreePerRoot,
690
+ playingNodeId,
691
+ isPlaying,
692
+ setIsPlaying,
693
+ play,
694
+ showPopup
695
+ ]
696
+ );
697
+ const handleAddChild = (0, import_react4.useCallback)(
698
+ (parentId) => {
699
+ const genrePlaylist = genrePlaylistTreePerRoot.find((g) => g.uuid === parentId);
700
+ if (!genrePlaylist?.criteria) return;
701
+ handleGenreCreationAction(genrePlaylist.criteria);
702
+ },
703
+ [genrePlaylistTreePerRoot, handleGenreCreationAction]
704
+ );
705
+ const handleRenameRequest = (0, import_react4.useCallback)(
706
+ (node) => {
707
+ const genrePlaylist = genrePlaylistTreePerRoot.find((g) => g.uuid === node.id);
708
+ if (!genrePlaylist?.criteria) return;
709
+ handleGenreRenameAction(genrePlaylist.criteria);
710
+ },
711
+ [genrePlaylistTreePerRoot, handleGenreRenameAction]
712
+ );
713
+ const handleDeleteRequest = (0, import_react4.useCallback)((node) => {
714
+ if (confirm(`Are you sure you want to delete "${node.name}"?`)) {
715
+ }
716
+ }, []);
717
+ return /* @__PURE__ */ (0, import_jsx_runtime12.jsx)(
718
+ import_genre_tree_view.GenreTree,
650
719
  {
651
- href: url,
652
- target: "_blank",
653
- rel: "noopener noreferrer",
654
- "aria-label": `View ${song.title} on MusicBrainz`,
655
720
  className: className2,
656
- onClick: (event) => event.stopPropagation(),
657
- children
721
+ nodes,
722
+ itemCountLabel: `${(0, import_track_noun2.capitalize)(trackNoun.singular)} count`,
723
+ rootColor: (0, import_genre_tree_view.getGenreTreeColor)(rootUuid),
724
+ playingNodeId,
725
+ playState: isPlaying ? "playing" : "paused",
726
+ onPlayPause: handlePlayPause,
727
+ onAddChild: readOnly ? void 0 : handleAddChild,
728
+ onRenameRequest: readOnly ? void 0 : handleRenameRequest,
729
+ onDeleteRequest: readOnly ? void 0 : handleDeleteRequest,
730
+ additionalActions,
731
+ onNodeClick,
732
+ onNodeHover,
733
+ renderInfoPanelBody,
734
+ selectedNodeId,
735
+ onSelectedNodeChange,
736
+ hideInfoPanelClose,
737
+ hideInfoPanelTitle,
738
+ showToolbar,
739
+ wheelZoom,
740
+ defaultViewport,
741
+ viewportRestore,
742
+ onViewportChange,
743
+ ...infoPanelCuration,
744
+ readOnly
658
745
  }
659
746
  );
660
747
  }
661
748
 
662
- // src/genre-tree/GenrePlaylistTracks.tsx
663
- var import_genre_tree6 = require("@behindthemusictree/app-kit/genre-tree");
664
- var import_track_noun2 = require("@behindthemusictree/app-kit/track-noun");
665
- var import_jsx_runtime14 = require("react/jsx-runtime");
666
- var CATALOGUE_PAGE_SIZE = 100;
667
- var SongPageSchema = (0, import_transport3.PaginatedResponseSchema)(import_genre_tree4.SongDetailedSchema).extend({
668
- overallTotal: import_zod.z.number().nullable(),
669
- page: import_zod.z.number().nullable(),
670
- totalPages: import_zod.z.number().nullable()
671
- });
672
- var TEXT_CELL = "min-w-0 flex-1 truncate [@container(min-width:40rem)]:flex [@container(min-width:40rem)]:gap-2";
673
- var TITLE_CELL = "[@container(min-width:40rem)]:flex-[2] [@container(min-width:40rem)]:truncate";
674
- var COLUMN_CELL = "[@container(min-width:40rem)]:flex-1 [@container(min-width:40rem)]:truncate";
675
- var ALBUM_CELL = `hidden [@container(min-width:40rem)]:block ${COLUMN_CELL}`;
676
- var SEPARATOR = "[@container(min-width:40rem)]:hidden";
677
- var nextCursor = (next) => {
678
- const cursor = new URL(next).searchParams.get("cursor");
679
- if (!cursor) throw new Error(`Songs next link has no cursor: ${next}`);
680
- return cursor;
749
+ // src/genre-tree/playlist-tree/TreeWheel.tsx
750
+ var import_react5 = require("react");
751
+ var import_genre_tree_view2 = require("@behindthemusictree/genre-tree-view");
752
+ var import_player2 = require("@behindthemusictree/app-kit/player");
753
+ var import_popup3 = require("@behindthemusictree/app-kit/popup");
754
+ var import_transport3 = require("@behindthemusictree/app-kit/transport");
755
+
756
+ // src/genre-tree/playlist-tree/size-metric.ts
757
+ var hasViewsCount = (genrePlaylists) => genrePlaylists.every((genrePlaylist) => genrePlaylist.viewsCount !== void 0);
758
+ var genrePlaylistSize = (genrePlaylist, metric) => {
759
+ if (metric === "songs") return genrePlaylist.tracksCount;
760
+ if (genrePlaylist.viewsCount === void 0) {
761
+ throw new Error(`Genre playlist ${genrePlaylist.uuid} has no viewsCount to size by views`);
762
+ }
763
+ return genrePlaylist.viewsCount;
681
764
  };
682
- function GenrePlaylistTracks({
683
- genreUuid,
684
- genrePlaylist,
765
+
766
+ // src/genre-tree/playlist-tree/TreeWheel.tsx
767
+ var import_track_noun3 = require("@behindthemusictree/app-kit/track-noun");
768
+ var import_jsx_runtime13 = require("react/jsx-runtime");
769
+ function GenrePlaylistTreeWheel({
685
770
  scope,
686
- getBackendBaseUrl,
687
- renderArtistLink,
688
- renderSongLeading,
689
- renderSongActions,
690
- renderTracksToolbar,
691
- artist = null
771
+ className: className2,
772
+ genrePlaylists,
773
+ handleGenreCreationAction,
774
+ handleGenreRenameAction,
775
+ additionalActions,
776
+ onNodeClick,
777
+ onNodeHover,
778
+ renderInfoPanelBody,
779
+ selectedNodeId,
780
+ onSelectedNodeChange,
781
+ hideInfoPanelClose,
782
+ hideInfoPanelTitle,
783
+ readOnly = false,
784
+ allowWheelRotation,
785
+ showToolbar,
786
+ sizeMetric = "songs",
787
+ floatingControlsLeading,
788
+ busy,
789
+ defaultViewport,
790
+ viewportRestore,
791
+ onViewportChange,
792
+ playback,
793
+ ...infoPanelCuration
692
794
  }) {
693
- const { fetchGenrePlaylistTracksPage, playNewTrackListFromGenrePlaylist, playNewTrackListFromArtist } = (0, import_genre_tree5.useTrackList)();
694
- const { session, sessionRestored } = (0, import_auth2.useSession)();
695
- const { playerTrackObject, playState, handlePlayPauseAction } = (0, import_player.usePlayer)();
696
- const { showPopup } = (0, import_popup2.usePopup)();
697
- const trackNoun = (0, import_track_noun2.useTrackNoun)();
698
- const { uuid } = genrePlaylist;
699
- const { fetch } = (0, import_transport2.useFetchWrapper)(getBackendBaseUrl);
700
- const { data, isPending, isError, refetch, hasNextPage, isFetchingNextPage, fetchNextPage } = (0, import_react_query.useInfiniteQuery)({
701
- queryKey: scope === "me" ? import_genre_tree2.genrePlaylistQueryKeys.me.tracks(uuid) : artist ? [...import_genre_tree3.libraryQueryKeys.reference.songs.byGenre(getBackendBaseUrl(), genreUuid), "artist", artist.uuid] : import_genre_tree3.libraryQueryKeys.reference.songs.byGenre(getBackendBaseUrl(), genreUuid),
702
- queryFn: async ({ pageParam }) => {
703
- if (scope === "me") return fetchGenrePlaylistTracksPage(uuid, scope, Number(pageParam));
704
- const response = await fetch(
705
- import_genre_tree3.libraryEndpoints.reference.songs.list(),
706
- true,
707
- false,
708
- {},
709
- { genre: genreUuid, ...artist && { artist: artist.uuid }, pageSize: CATALOGUE_PAGE_SIZE, withTotal: false, ...pageParam && { cursor: pageParam } }
710
- );
711
- const parsed = (0, import_transport4.parseWithLog)(SongPageSchema, response, "GenrePlaylistTracks");
712
- return { tracks: parsed.results, total: parsed.overallTotal, nextPage: parsed.next ? nextCursor(parsed.next) : null };
713
- },
714
- initialPageParam: scope === "me" ? 1 : "",
715
- getNextPageParam: (lastPage) => lastPage.nextPage ?? void 0,
716
- enabled: scope !== "me" || sessionRestored && !!session?.accessToken
717
- });
718
- const sentinelRef = (0, import_react4.useRef)(null);
719
- (0, import_react4.useEffect)(() => {
720
- const sentinel = sentinelRef.current;
721
- if (!hasNextPage || isFetchingNextPage || !sentinel) return;
722
- const observer = new IntersectionObserver((entries) => {
723
- if (entries.some((entry) => entry.isIntersecting)) {
724
- void fetchNextPage();
725
- }
726
- });
727
- observer.observe(sentinel);
728
- return () => observer.disconnect();
729
- }, [hasNextPage, isFetchingNextPage, fetchNextPage]);
730
- const pages = data?.pages ?? [];
731
- const tracks = pages.flatMap((page) => page.tracks);
732
- const total = pages[0]?.total ?? void 0;
733
- const play = (startSongUuid) => {
734
- const started = artist ? playNewTrackListFromArtist(artist, scope, { startSongUuid, ordering: "title", genreUuid }) : playNewTrackListFromGenrePlaylist(genrePlaylist, scope, { startSongUuid });
735
- started.catch((error) => {
736
- console.error("Failed to play genre playlist:", error);
737
- showPopup(/* @__PURE__ */ (0, import_jsx_runtime14.jsx)(InternalErrorPopup, { errorCode: import_transport5.ErrorCode.CLIENT_UNKNOWN }));
738
- });
739
- };
740
- return /* @__PURE__ */ (0, import_jsx_runtime14.jsxs)("div", { className: "gtv-info-panel-children", children: [
741
- /* @__PURE__ */ (0, import_jsx_runtime14.jsxs)("span", { className: "gtv-info-panel-children-title", children: [
742
- (0, import_track_noun2.capitalize)(trackNoun.plural),
743
- total !== void 0 && ` (${total})`
744
- ] }),
745
- renderTracksToolbar?.(),
746
- isPending ? /* @__PURE__ */ (0, import_jsx_runtime14.jsx)(import_ui6.Skeleton, { className: "h-4 w-3/4", "aria-busy": "true" }) : isError && tracks.length === 0 ? /* @__PURE__ */ (0, import_jsx_runtime14.jsxs)("p", { role: "alert", children: [
747
- "Couldn't load ",
748
- trackNoun.plural,
749
- ".",
750
- " ",
751
- /* @__PURE__ */ (0, import_jsx_runtime14.jsx)("button", { type: "button", className: "underline", onClick: () => void refetch(), children: "Retry" })
752
- ] }) : tracks.length === 0 ? /* @__PURE__ */ (0, import_jsx_runtime14.jsx)("p", { children: "\u2014" }) : /* @__PURE__ */ (0, import_jsx_runtime14.jsxs)("ul", { className: "mt-1 max-h-64 overflow-y-auto text-[0.8125rem] leading-[1.45] [container-type:inline-size]", "aria-label": `${genrePlaylist.name} ${trackNoun.plural}`, children: [
753
- tracks.map((track, index) => {
754
- const label = track.artists?.length ? `${track.title} \u2014 ${track.artists.map((artist2) => artist2.name).join(", ")}` : track.title;
755
- const isCurrent = playerTrackObject?.track.id === track.uuid;
756
- const isPlaying = isCurrent && playState === import_player2.PlayStates.PLAYING;
757
- const text = /* @__PURE__ */ (0, import_jsx_runtime14.jsxs)("span", { className: TEXT_CELL, children: [
758
- /* @__PURE__ */ (0, import_jsx_runtime14.jsx)("span", { className: TITLE_CELL, children: (0, import_genre_tree4.isSong)(track) ? /* @__PURE__ */ (0, import_jsx_runtime14.jsx)(MusicBrainzRecordingLink, { song: track, className: "hover:underline focus-visible:underline", children: track.title }) : track.title }),
759
- track.artists?.length ? /* @__PURE__ */ (0, import_jsx_runtime14.jsx)("span", { className: SEPARATOR, children: " \u2014 " }) : null,
760
- /* @__PURE__ */ (0, import_jsx_runtime14.jsx)("span", { className: COLUMN_CELL, children: track.artists?.length ? /* @__PURE__ */ (0, import_jsx_runtime14.jsx)(import_genre_tree6.ArtistNames, { artists: track.artists, renderArtistLink }) : null }),
761
- track.album && /* @__PURE__ */ (0, import_jsx_runtime14.jsx)("span", { className: ALBUM_CELL, children: track.album.name })
762
- ] });
763
- const leading = renderSongLeading?.(track, index);
764
- const actions = renderSongActions?.(track);
765
- const isUnplayable = (0, import_genre_tree4.isSong)(track) && (0, import_genre_tree4.songUnplayableReason)(track) !== null;
766
- if (isUnplayable) {
767
- return /* @__PURE__ */ (0, import_jsx_runtime14.jsxs)("li", { className: "flex min-h-8 items-center gap-1 px-1 opacity-50", children: [
768
- leading,
769
- /* @__PURE__ */ (0, import_jsx_runtime14.jsx)(
770
- "button",
771
- {
772
- type: "button",
773
- disabled: true,
774
- "aria-disabled": "true",
775
- "aria-label": label,
776
- className: "flex h-6 w-6 shrink-0 items-center justify-center rounded-full opacity-0 cursor-not-allowed",
777
- children: /* @__PURE__ */ (0, import_jsx_runtime14.jsx)(import_fa3.FaPlay, { size: 10 })
778
- }
779
- ),
780
- text,
781
- actions
782
- ] }, track.uuid);
783
- }
784
- return /* @__PURE__ */ (0, import_jsx_runtime14.jsxs)(
785
- "li",
786
- {
787
- className: `group flex min-h-8 items-center gap-1 rounded px-1 hover:bg-[var(--color-neutral-100)] ${isCurrent ? "font-medium text-[var(--color-primary)]" : ""}`,
788
- children: [
789
- leading,
790
- /* @__PURE__ */ (0, import_jsx_runtime14.jsx)(
791
- "button",
792
- {
793
- type: "button",
794
- "aria-label": `${isPlaying ? "Pause" : "Play"} ${label}`,
795
- className: `flex h-6 w-6 shrink-0 items-center justify-center rounded-full cursor-pointer group-hover:opacity-100 focus-visible:opacity-100 ${isCurrent && playState !== import_player2.PlayStates.STOPPED ? "opacity-100" : "opacity-0 [@media(pointer:coarse)]:opacity-100"}`,
796
- onClick: () => isCurrent ? handlePlayPauseAction() : play(track.uuid),
797
- children: isCurrent && playState === import_player2.PlayStates.LOADING ? /* @__PURE__ */ (0, import_jsx_runtime14.jsx)(import_ui6.RingLoader, { size: 10 }) : isPlaying ? /* @__PURE__ */ (0, import_jsx_runtime14.jsx)(import_fa3.FaPause, { size: 10 }) : /* @__PURE__ */ (0, import_jsx_runtime14.jsx)(import_fa3.FaPlay, { size: 10 })
798
- }
799
- ),
800
- text,
801
- actions
802
- ]
803
- },
804
- track.uuid
805
- );
806
- }),
807
- hasNextPage && /* @__PURE__ */ (0, import_jsx_runtime14.jsx)("li", { ref: sentinelRef, "aria-hidden": "true", className: "h-4" })
808
- ] })
809
- ] });
810
- }
811
-
812
- // src/genre-tree/GenreDetailExtras.tsx
813
- var import_track_noun3 = require("@behindthemusictree/app-kit/track-noun");
814
- var import_si = require("react-icons/si");
815
- var import_jsx_runtime15 = require("react/jsx-runtime");
816
- function GenreDetailExtras({
817
- genreUuid,
818
- genrePlaylist,
819
- wikidataId,
820
- scope,
821
- getBackendBaseUrl,
822
- criteriaOverviewSchema,
823
- renderGenreDetailExtras,
824
- renderArtistLink,
825
- renderSummary,
826
- renderEssentialTracks,
827
- family,
828
- ...trackSlots
829
- }) {
830
- const trackNoun = (0, import_track_noun3.useTrackNoun)();
831
- const { data: overview, isPending } = (0, import_genre_tree7.useFetchGenreOverview)(
832
- genreUuid,
833
- scope,
834
- getBackendBaseUrl,
835
- criteriaOverviewSchema
836
- );
837
- if (isPending) {
838
- return /* @__PURE__ */ (0, import_jsx_runtime15.jsxs)(import_jsx_runtime15.Fragment, { children: [
839
- /* @__PURE__ */ (0, import_jsx_runtime15.jsxs)("div", { className: "gtv-info-panel-children", "aria-busy": "true", "data-testid": "genre-detail-extras-skeleton", children: [
840
- /* @__PURE__ */ (0, import_jsx_runtime15.jsx)("span", { className: "gtv-info-panel-children-title", children: "Summary" }),
841
- /* @__PURE__ */ (0, import_jsx_runtime15.jsx)(import_ui7.Skeleton, { className: "h-4 w-3/4" })
842
- ] }),
843
- family
844
- ] });
845
- }
846
- if (!overview) return /* @__PURE__ */ (0, import_jsx_runtime15.jsx)(import_jsx_runtime15.Fragment, { children: family });
847
- const { summary, summarySourceUrl, essentialSongs } = overview;
848
- const wikidataUrl = wikidataId ? `https://www.wikidata.org/wiki/${encodeURIComponent(wikidataId)}` : null;
849
- return /* @__PURE__ */ (0, import_jsx_runtime15.jsxs)(import_jsx_runtime15.Fragment, { children: [
850
- (summarySourceUrl || wikidataUrl) && /* @__PURE__ */ (0, import_jsx_runtime15.jsxs)("div", { className: "gtv-info-panel-children text-[0.8125rem] leading-[1.45] text-[var(--color-text)]", children: [
851
- /* @__PURE__ */ (0, import_jsx_runtime15.jsx)("span", { className: "gtv-info-panel-children-title", children: "Links" }),
852
- /* @__PURE__ */ (0, import_jsx_runtime15.jsxs)("div", { className: "mt-1 flex flex-wrap gap-x-3 gap-y-1 text-xs", children: [
853
- summarySourceUrl && /* @__PURE__ */ (0, import_jsx_runtime15.jsxs)(
854
- "a",
855
- {
856
- href: summarySourceUrl,
857
- target: "_blank",
858
- rel: "noopener noreferrer",
859
- className: "inline-flex items-center gap-1.5 text-[var(--color-text-muted)] hover:text-[var(--color-text)]",
860
- children: [
861
- /* @__PURE__ */ (0, import_jsx_runtime15.jsx)(import_si.SiWikipedia, { "aria-hidden": "true", className: "h-3.5 w-3.5" }),
862
- "Wikipedia"
863
- ]
864
- }
865
- ),
866
- wikidataUrl && /* @__PURE__ */ (0, import_jsx_runtime15.jsxs)(
867
- "a",
868
- {
869
- href: wikidataUrl,
870
- target: "_blank",
871
- rel: "noopener noreferrer",
872
- className: "inline-flex items-center gap-1.5 text-[var(--color-text-muted)] hover:text-[var(--color-text)]",
873
- children: [
874
- /* @__PURE__ */ (0, import_jsx_runtime15.jsx)(import_si.SiWikidata, { "aria-hidden": "true", className: "h-3.5 w-3.5" }),
875
- "Wikidata"
876
- ]
877
- }
878
- )
879
- ] })
880
- ] }),
881
- renderSummary ? renderSummary(overview) : /* @__PURE__ */ (0, import_jsx_runtime15.jsxs)("div", { className: "gtv-info-panel-children text-[0.8125rem] leading-[1.45] text-[var(--color-text)]", children: [
882
- /* @__PURE__ */ (0, import_jsx_runtime15.jsx)("span", { className: "gtv-info-panel-children-title", children: "Summary" }),
883
- /* @__PURE__ */ (0, import_jsx_runtime15.jsx)(ClampedText, { className: "mt-1", children: summary ?? "\u2014" }, genreUuid)
884
- ] }),
885
- family,
886
- renderGenreDetailExtras?.(overview),
887
- renderEssentialTracks ? renderEssentialTracks(overview) : essentialSongs && /* @__PURE__ */ (0, import_jsx_runtime15.jsxs)("div", { className: "gtv-info-panel-children text-[0.8125rem] leading-[1.45] text-[var(--color-text)]", children: [
888
- /* @__PURE__ */ (0, import_jsx_runtime15.jsxs)("span", { className: "gtv-info-panel-children-title", children: [
889
- "Essential ",
890
- trackNoun.plural
891
- ] }),
892
- essentialSongs.length > 0 ? /* @__PURE__ */ (0, import_jsx_runtime15.jsx)("ul", { className: "mt-1 divide-y divide-[var(--color-border)]", children: essentialSongs.map((track) => /* @__PURE__ */ (0, import_jsx_runtime15.jsx)("li", { className: "py-1", children: track.title }, track.uuid)) }) : /* @__PURE__ */ (0, import_jsx_runtime15.jsx)("p", { className: "mt-1", children: "\u2014" })
893
- ] }),
894
- /* @__PURE__ */ (0, import_jsx_runtime15.jsx)(
895
- GenrePlaylistTracks,
896
- {
897
- genreUuid,
898
- genrePlaylist,
899
- scope,
900
- getBackendBaseUrl,
901
- renderArtistLink,
902
- ...trackSlots
903
- }
904
- )
905
- ] });
906
- }
907
-
908
- // src/genre-tree/playlist-tree/TreePerRoot.tsx
909
- var import_react6 = require("react");
910
- var import_genre_tree_view = require("@behindthemusictree/genre-tree-view");
911
- var import_player3 = require("@behindthemusictree/app-kit/player");
912
- var import_popup3 = require("@behindthemusictree/app-kit/popup");
913
- var import_transport6 = require("@behindthemusictree/app-kit/transport");
914
-
915
- // src/genre-tree/playlist-tree/useGenrePlaylistPlayback.ts
916
- var import_react5 = require("react");
917
- var import_genre_tree8 = require("@behindthemusictree/app-kit/genre-tree");
918
- function useGenrePlaylistPlayback({
919
- scope,
920
- genrePlaylists,
921
- artist = null
922
- }) {
923
- const { trackList, playNewTrackListFromGenrePlaylist, playNewTrackListFromArtist } = (0, import_genre_tree8.useTrackList)();
924
- const origin = trackList?.origin;
925
- const playingNodeId = (0, import_react5.useMemo)(() => {
926
- if (!origin) return null;
927
- if (!artist) return origin.type === import_genre_tree8.TrackListOriginType.GENRE_PLAYLIST ? origin.uuid : null;
928
- if (origin.type !== import_genre_tree8.TrackListOriginType.ARTIST || origin.uuid !== artist.uuid) return null;
929
- const { genreUuid } = origin;
930
- return genreUuid ? genrePlaylists.find((gp) => gp.criteria?.uuid === genreUuid)?.uuid ?? null : null;
931
- }, [origin, artist, genrePlaylists]);
932
- const play = (0, import_react5.useCallback)(
933
- async (genrePlaylist) => {
934
- if (!artist) return playNewTrackListFromGenrePlaylist(genrePlaylist, scope);
935
- if (!genrePlaylist.criteria) return;
936
- return playNewTrackListFromArtist(artist, scope, { ordering: "title", genreUuid: genrePlaylist.criteria.uuid });
937
- },
938
- [artist, scope, playNewTrackListFromGenrePlaylist, playNewTrackListFromArtist]
939
- );
940
- return (0, import_react5.useMemo)(() => ({ playingNodeId, play }), [playingNodeId, play]);
941
- }
942
-
943
- // src/genre-tree/playlist-tree/TreePerRoot.tsx
944
- var import_track_noun4 = require("@behindthemusictree/app-kit/track-noun");
945
- var import_jsx_runtime16 = require("react/jsx-runtime");
946
- function GenrePlaylistTreePerRoot({
947
- scope,
948
- className: className2,
949
- rootUuid,
950
- genrePlaylistTreePerRoot,
951
- handleGenreCreationAction,
952
- handleGenreRenameAction,
953
- additionalActions,
954
- onNodeClick,
955
- onNodeHover,
956
- renderInfoPanelBody,
957
- selectedNodeId,
958
- onSelectedNodeChange,
959
- hideInfoPanelClose,
960
- hideInfoPanelTitle,
961
- readOnly = false,
962
- showToolbar,
963
- wheelZoom,
964
- controlsOrientation,
965
- defaultViewport,
966
- onViewportChange,
967
- playback,
968
- ...infoPanelCuration
969
- }) {
970
- const { isPlaying, setIsPlaying } = (0, import_player3.usePlayer)();
971
- const defaultPlayback = useGenrePlaylistPlayback({ scope, genrePlaylists: genrePlaylistTreePerRoot });
795
+ const { isPlaying, setIsPlaying } = (0, import_player2.usePlayer)();
796
+ const defaultPlayback = useGenrePlaylistPlayback({ scope, genrePlaylists });
972
797
  const { playingNodeId, play } = playback ?? defaultPlayback;
973
- const trackNoun = (0, import_track_noun4.useTrackNoun)();
798
+ const trackNoun = (0, import_track_noun3.useTrackNoun)();
974
799
  const { showPopup } = (0, import_popup3.usePopup)();
975
- const nodes = (0, import_react6.useMemo)(
976
- () => genrePlaylistTreePerRoot.map((genrePlaylist) => ({
800
+ const nodes = (0, import_react5.useMemo)(
801
+ () => genrePlaylists.map((genrePlaylist) => ({
977
802
  id: genrePlaylist.uuid,
978
803
  parentId: genrePlaylist.parent?.uuid ?? null,
979
804
  name: genrePlaylist.name,
980
- itemCount: genrePlaylist.tracksCount,
805
+ itemCount: genrePlaylistSize(genrePlaylist, sizeMetric),
981
806
  actionable: Boolean(genrePlaylist.criteria),
982
807
  side: genrePlaylist.criteria?.side ?? void 0,
983
- wikidataId: genrePlaylist.criteria?.wikidataId ?? null,
984
- umbrella: genrePlaylist.isUmbrella
808
+ wikidataId: genrePlaylist.criteria?.wikidataId ?? null
985
809
  })),
986
- [genrePlaylistTreePerRoot]
810
+ [genrePlaylists, sizeMetric]
987
811
  );
988
- const handlePlayPause = (0, import_react6.useCallback)(
812
+ const handlePlayPause = (0, import_react5.useCallback)(
989
813
  (nodeId) => {
990
- const genrePlaylist = genrePlaylistTreePerRoot.find((g) => g.uuid === nodeId);
814
+ const genrePlaylist = genrePlaylists.find((g) => g.uuid === nodeId);
991
815
  if (!genrePlaylist) return;
992
816
  if (nodeId === playingNodeId) {
993
817
  setIsPlaying(!isPlaying);
@@ -998,11 +822,11 @@ function GenrePlaylistTreePerRoot({
998
822
  }
999
823
  play(genrePlaylist).catch((error) => {
1000
824
  console.error("Failed to load genre playlist tracks:", error);
1001
- showPopup(/* @__PURE__ */ (0, import_jsx_runtime16.jsx)(InternalErrorPopup, { errorCode: import_transport6.ErrorCode.CLIENT_UNKNOWN }));
825
+ showPopup(/* @__PURE__ */ (0, import_jsx_runtime13.jsx)(InternalErrorPopup, { errorCode: import_transport3.ErrorCode.CLIENT_UNKNOWN }));
1002
826
  });
1003
827
  },
1004
828
  [
1005
- genrePlaylistTreePerRoot,
829
+ genrePlaylists,
1006
830
  playingNodeId,
1007
831
  isPlaying,
1008
832
  setIsPlaying,
@@ -1010,33 +834,32 @@ function GenrePlaylistTreePerRoot({
1010
834
  showPopup
1011
835
  ]
1012
836
  );
1013
- const handleAddChild = (0, import_react6.useCallback)(
837
+ const handleAddChild = (0, import_react5.useCallback)(
1014
838
  (parentId) => {
1015
- const genrePlaylist = genrePlaylistTreePerRoot.find((g) => g.uuid === parentId);
839
+ const genrePlaylist = genrePlaylists.find((g) => g.uuid === parentId);
1016
840
  if (!genrePlaylist?.criteria) return;
1017
841
  handleGenreCreationAction(genrePlaylist.criteria);
1018
842
  },
1019
- [genrePlaylistTreePerRoot, handleGenreCreationAction]
843
+ [genrePlaylists, handleGenreCreationAction]
1020
844
  );
1021
- const handleRenameRequest = (0, import_react6.useCallback)(
845
+ const handleRenameRequest = (0, import_react5.useCallback)(
1022
846
  (node) => {
1023
- const genrePlaylist = genrePlaylistTreePerRoot.find((g) => g.uuid === node.id);
847
+ const genrePlaylist = genrePlaylists.find((g) => g.uuid === node.id);
1024
848
  if (!genrePlaylist?.criteria) return;
1025
849
  handleGenreRenameAction(genrePlaylist.criteria);
1026
850
  },
1027
- [genrePlaylistTreePerRoot, handleGenreRenameAction]
851
+ [genrePlaylists, handleGenreRenameAction]
1028
852
  );
1029
- const handleDeleteRequest = (0, import_react6.useCallback)((node) => {
853
+ const handleDeleteRequest = (0, import_react5.useCallback)((node) => {
1030
854
  if (confirm(`Are you sure you want to delete "${node.name}"?`)) {
1031
855
  }
1032
856
  }, []);
1033
- return /* @__PURE__ */ (0, import_jsx_runtime16.jsx)(
1034
- import_genre_tree_view.GenreTree,
857
+ return /* @__PURE__ */ (0, import_jsx_runtime13.jsx)(
858
+ import_genre_tree_view2.GenreTreeWheel,
1035
859
  {
1036
860
  className: className2,
1037
861
  nodes,
1038
- itemCountLabel: `${(0, import_track_noun4.capitalize)(trackNoun.singular)} count`,
1039
- rootColor: (0, import_genre_tree_view.getGenreTreeColor)(rootUuid),
862
+ itemCountLabel: sizeMetric === "views" ? "Views" : `${(0, import_track_noun3.capitalize)(trackNoun.singular)} count`,
1040
863
  playingNodeId,
1041
864
  playState: isPlaying ? "playing" : "paused",
1042
865
  onPlayPause: handlePlayPause,
@@ -1051,151 +874,12 @@ function GenrePlaylistTreePerRoot({
1051
874
  onSelectedNodeChange,
1052
875
  hideInfoPanelClose,
1053
876
  hideInfoPanelTitle,
877
+ allowWheelRotation,
1054
878
  showToolbar,
1055
- wheelZoom,
1056
- controlsOrientation,
1057
- defaultViewport,
1058
- onViewportChange,
1059
- ...infoPanelCuration,
1060
- readOnly
1061
- }
1062
- );
1063
- }
1064
-
1065
- // src/genre-tree/playlist-tree/TreeWheel.tsx
1066
- var import_react7 = require("react");
1067
- var import_genre_tree_view2 = require("@behindthemusictree/genre-tree-view");
1068
- var import_player4 = require("@behindthemusictree/app-kit/player");
1069
- var import_popup4 = require("@behindthemusictree/app-kit/popup");
1070
- var import_transport7 = require("@behindthemusictree/app-kit/transport");
1071
-
1072
- // src/genre-tree/playlist-tree/size-metric.ts
1073
- var hasViewsCount = (genrePlaylists) => genrePlaylists.every((genrePlaylist) => genrePlaylist.viewsCount !== void 0);
1074
- var genrePlaylistSize = (genrePlaylist, metric) => {
1075
- if (metric === "songs") return genrePlaylist.tracksCount;
1076
- if (genrePlaylist.viewsCount === void 0) {
1077
- throw new Error(`Genre playlist ${genrePlaylist.uuid} has no viewsCount to size by views`);
1078
- }
1079
- return genrePlaylist.viewsCount;
1080
- };
1081
-
1082
- // src/genre-tree/playlist-tree/TreeWheel.tsx
1083
- var import_track_noun5 = require("@behindthemusictree/app-kit/track-noun");
1084
- var import_jsx_runtime17 = require("react/jsx-runtime");
1085
- function GenrePlaylistTreeWheel({
1086
- scope,
1087
- className: className2,
1088
- genrePlaylists,
1089
- handleGenreCreationAction,
1090
- handleGenreRenameAction,
1091
- additionalActions,
1092
- onNodeClick,
1093
- onNodeHover,
1094
- renderInfoPanelBody,
1095
- selectedNodeId,
1096
- onSelectedNodeChange,
1097
- hideInfoPanelClose,
1098
- hideInfoPanelTitle,
1099
- readOnly = false,
1100
- allowWheelRotation,
1101
- showToolbar,
1102
- controlsOrientation,
1103
- sizeMetric = "songs",
1104
- floatingControlsLeading,
1105
- busy,
1106
- defaultViewport,
1107
- onViewportChange,
1108
- playback,
1109
- ...infoPanelCuration
1110
- }) {
1111
- const { isPlaying, setIsPlaying } = (0, import_player4.usePlayer)();
1112
- const defaultPlayback = useGenrePlaylistPlayback({ scope, genrePlaylists });
1113
- const { playingNodeId, play } = playback ?? defaultPlayback;
1114
- const trackNoun = (0, import_track_noun5.useTrackNoun)();
1115
- const { showPopup } = (0, import_popup4.usePopup)();
1116
- const nodes = (0, import_react7.useMemo)(
1117
- () => genrePlaylists.map((genrePlaylist) => ({
1118
- id: genrePlaylist.uuid,
1119
- parentId: genrePlaylist.parent?.uuid ?? null,
1120
- name: genrePlaylist.name,
1121
- itemCount: genrePlaylistSize(genrePlaylist, sizeMetric),
1122
- actionable: Boolean(genrePlaylist.criteria),
1123
- side: genrePlaylist.criteria?.side ?? void 0,
1124
- wikidataId: genrePlaylist.criteria?.wikidataId ?? null
1125
- })),
1126
- [genrePlaylists, sizeMetric]
1127
- );
1128
- const handlePlayPause = (0, import_react7.useCallback)(
1129
- (nodeId) => {
1130
- const genrePlaylist = genrePlaylists.find((g) => g.uuid === nodeId);
1131
- if (!genrePlaylist) return;
1132
- if (nodeId === playingNodeId) {
1133
- setIsPlaying(!isPlaying);
1134
- return;
1135
- }
1136
- if (genrePlaylist.tracksCount === 0) {
1137
- return;
1138
- }
1139
- play(genrePlaylist).catch((error) => {
1140
- console.error("Failed to load genre playlist tracks:", error);
1141
- showPopup(/* @__PURE__ */ (0, import_jsx_runtime17.jsx)(InternalErrorPopup, { errorCode: import_transport7.ErrorCode.CLIENT_UNKNOWN }));
1142
- });
1143
- },
1144
- [
1145
- genrePlaylists,
1146
- playingNodeId,
1147
- isPlaying,
1148
- setIsPlaying,
1149
- play,
1150
- showPopup
1151
- ]
1152
- );
1153
- const handleAddChild = (0, import_react7.useCallback)(
1154
- (parentId) => {
1155
- const genrePlaylist = genrePlaylists.find((g) => g.uuid === parentId);
1156
- if (!genrePlaylist?.criteria) return;
1157
- handleGenreCreationAction(genrePlaylist.criteria);
1158
- },
1159
- [genrePlaylists, handleGenreCreationAction]
1160
- );
1161
- const handleRenameRequest = (0, import_react7.useCallback)(
1162
- (node) => {
1163
- const genrePlaylist = genrePlaylists.find((g) => g.uuid === node.id);
1164
- if (!genrePlaylist?.criteria) return;
1165
- handleGenreRenameAction(genrePlaylist.criteria);
1166
- },
1167
- [genrePlaylists, handleGenreRenameAction]
1168
- );
1169
- const handleDeleteRequest = (0, import_react7.useCallback)((node) => {
1170
- if (confirm(`Are you sure you want to delete "${node.name}"?`)) {
1171
- }
1172
- }, []);
1173
- return /* @__PURE__ */ (0, import_jsx_runtime17.jsx)(
1174
- import_genre_tree_view2.GenreTreeWheel,
1175
- {
1176
- className: className2,
1177
- nodes,
1178
- itemCountLabel: sizeMetric === "views" ? "Views" : `${(0, import_track_noun5.capitalize)(trackNoun.singular)} count`,
1179
- playingNodeId,
1180
- playState: isPlaying ? "playing" : "paused",
1181
- onPlayPause: handlePlayPause,
1182
- onAddChild: readOnly ? void 0 : handleAddChild,
1183
- onRenameRequest: readOnly ? void 0 : handleRenameRequest,
1184
- onDeleteRequest: readOnly ? void 0 : handleDeleteRequest,
1185
- additionalActions,
1186
- onNodeClick,
1187
- onNodeHover,
1188
- renderInfoPanelBody,
1189
- selectedNodeId,
1190
- onSelectedNodeChange,
1191
- hideInfoPanelClose,
1192
- hideInfoPanelTitle,
1193
- allowWheelRotation,
1194
- showToolbar,
1195
- controlsOrientation,
1196
- floatingControlsLeading,
1197
- busy,
879
+ floatingControlsLeading,
880
+ busy,
1198
881
  defaultViewport,
882
+ viewportRestore,
1199
883
  onViewportChange,
1200
884
  ...infoPanelCuration,
1201
885
  readOnly
@@ -1204,11 +888,11 @@ function GenrePlaylistTreeWheel({
1204
888
  }
1205
889
 
1206
890
  // src/genre-tree/playlist-tree/TreeWheelRadialPopCore.tsx
1207
- var import_react8 = require("react");
891
+ var import_react6 = require("react");
1208
892
  var import_genre_tree_view3 = require("@behindthemusictree/genre-tree-view");
1209
- var import_player5 = require("@behindthemusictree/app-kit/player");
1210
- var import_track_noun6 = require("@behindthemusictree/app-kit/track-noun");
1211
- var import_jsx_runtime18 = require("react/jsx-runtime");
893
+ var import_player3 = require("@behindthemusictree/app-kit/player");
894
+ var import_track_noun4 = require("@behindthemusictree/app-kit/track-noun");
895
+ var import_jsx_runtime14 = require("react/jsx-runtime");
1212
896
  function GenrePlaylistTreeWheelRadialPopCore(props) {
1213
897
  const {
1214
898
  scope,
@@ -1227,12 +911,12 @@ function GenrePlaylistTreeWheelRadialPopCore(props) {
1227
911
  readOnly = false,
1228
912
  allowWheelRotation,
1229
913
  showToolbar,
1230
- controlsOrientation,
1231
914
  sizeMetric = "songs",
1232
915
  floatingControlsLeading,
1233
916
  busy,
1234
917
  outline = false,
1235
918
  defaultViewport,
919
+ viewportRestore,
1236
920
  onViewportChange,
1237
921
  playback
1238
922
  } = props;
@@ -1245,11 +929,11 @@ function GenrePlaylistTreeWheelRadialPopCore(props) {
1245
929
  hideExpandButton: props.hideExpandButton,
1246
930
  overlayPanel: props.overlayPanel
1247
931
  };
1248
- const { isPlaying, setIsPlaying } = (0, import_player5.usePlayer)();
932
+ const { isPlaying, setIsPlaying } = (0, import_player3.usePlayer)();
1249
933
  const defaultPlayback = useGenrePlaylistPlayback({ scope, genrePlaylists });
1250
934
  const { playingNodeId, play } = playback ?? defaultPlayback;
1251
- const trackNoun = (0, import_track_noun6.useTrackNoun)();
1252
- const nodes = (0, import_react8.useMemo)(
935
+ const trackNoun = (0, import_track_noun4.useTrackNoun)();
936
+ const nodes = (0, import_react6.useMemo)(
1253
937
  () => genrePlaylists.map((genrePlaylist) => ({
1254
938
  id: genrePlaylist.uuid,
1255
939
  parentId: genrePlaylist.parent?.uuid ?? null,
@@ -1264,7 +948,7 @@ function GenrePlaylistTreeWheelRadialPopCore(props) {
1264
948
  })),
1265
949
  [genrePlaylists, sizeMetric]
1266
950
  );
1267
- const handlePlayPause = (0, import_react8.useCallback)(
951
+ const handlePlayPause = (0, import_react6.useCallback)(
1268
952
  (nodeId) => {
1269
953
  const genrePlaylist = genrePlaylists.find((g) => g.uuid === nodeId);
1270
954
  if (!genrePlaylist) return;
@@ -1281,7 +965,7 @@ function GenrePlaylistTreeWheelRadialPopCore(props) {
1281
965
  },
1282
966
  [genrePlaylists, playingNodeId, isPlaying, setIsPlaying, play]
1283
967
  );
1284
- const handleAddChild = (0, import_react8.useCallback)(
968
+ const handleAddChild = (0, import_react6.useCallback)(
1285
969
  (parentId) => {
1286
970
  const genrePlaylist = genrePlaylists.find((g) => g.uuid === parentId);
1287
971
  if (!genrePlaylist?.criteria) return;
@@ -1289,7 +973,7 @@ function GenrePlaylistTreeWheelRadialPopCore(props) {
1289
973
  },
1290
974
  [genrePlaylists, handleGenreCreationAction]
1291
975
  );
1292
- const handleRenameRequest = (0, import_react8.useCallback)(
976
+ const handleRenameRequest = (0, import_react6.useCallback)(
1293
977
  (node) => {
1294
978
  const genrePlaylist = genrePlaylists.find((g) => g.uuid === node.id);
1295
979
  if (!genrePlaylist?.criteria) return;
@@ -1297,14 +981,14 @@ function GenrePlaylistTreeWheelRadialPopCore(props) {
1297
981
  },
1298
982
  [genrePlaylists, handleGenreRenameAction]
1299
983
  );
1300
- const handleDeleteRequest = (0, import_react8.useCallback)((node) => {
984
+ const handleDeleteRequest = (0, import_react6.useCallback)((node) => {
1301
985
  if (confirm(`Are you sure you want to delete "${node.name}"?`)) {
1302
986
  }
1303
987
  }, []);
1304
988
  const treeProps = {
1305
989
  className: className2,
1306
990
  nodes,
1307
- itemCountLabel: sizeMetric === "views" ? "Views" : `${(0, import_track_noun6.capitalize)(trackNoun.singular)} count`,
991
+ itemCountLabel: sizeMetric === "views" ? "Views" : `${(0, import_track_noun4.capitalize)(trackNoun.singular)} count`,
1308
992
  playingNodeId,
1309
993
  playState: isPlaying ? "playing" : "paused",
1310
994
  onPlayPause: handlePlayPause,
@@ -1323,15 +1007,15 @@ function GenrePlaylistTreeWheelRadialPopCore(props) {
1323
1007
  ...infoPanelCuration,
1324
1008
  readOnly
1325
1009
  };
1326
- return outline ? /* @__PURE__ */ (0, import_jsx_runtime18.jsx)(import_genre_tree_view3.GenreTreeOutline, { ...treeProps }) : /* @__PURE__ */ (0, import_jsx_runtime18.jsx)(
1010
+ return outline ? /* @__PURE__ */ (0, import_jsx_runtime14.jsx)(import_genre_tree_view3.GenreTreeOutline, { ...treeProps }) : /* @__PURE__ */ (0, import_jsx_runtime14.jsx)(
1327
1011
  import_genre_tree_view3.GenreTreeWheelRadialPopCore,
1328
1012
  {
1329
1013
  ...treeProps,
1330
1014
  allowWheelRotation,
1331
- controlsOrientation,
1332
1015
  floatingControlsLeading,
1333
1016
  busy,
1334
1017
  defaultViewport,
1018
+ viewportRestore,
1335
1019
  onViewportChange,
1336
1020
  ...focus.onFocusGenreChange ? { focusNodeId: focus.focusGenreUuid, onFocusChange: focus.onFocusGenreChange } : { focusNodeId: void 0, onFocusChange: void 0 }
1337
1021
  }
@@ -1339,13 +1023,13 @@ function GenrePlaylistTreeWheelRadialPopCore(props) {
1339
1023
  }
1340
1024
 
1341
1025
  // src/genre-tree/GenreTreeWheelHandoff.tsx
1342
- var import_react9 = require("react");
1343
- var import_jsx_runtime19 = require("react/jsx-runtime");
1026
+ var import_react7 = require("react");
1027
+ var import_jsx_runtime15 = require("react/jsx-runtime");
1344
1028
  function GenreTreeWheelHandoff({ skeleton, children, onRevealedChange }) {
1345
- const [revealed, setRevealed] = (0, import_react9.useState)(false);
1346
- const onRevealedChangeRef = (0, import_react9.useRef)(onRevealedChange);
1029
+ const [revealed, setRevealed] = (0, import_react7.useState)(false);
1030
+ const onRevealedChangeRef = (0, import_react7.useRef)(onRevealedChange);
1347
1031
  onRevealedChangeRef.current = onRevealedChange;
1348
- (0, import_react9.useEffect)(() => {
1032
+ (0, import_react7.useEffect)(() => {
1349
1033
  let innerFrame = 0;
1350
1034
  const outerFrame = requestAnimationFrame(() => {
1351
1035
  innerFrame = requestAnimationFrame(() => {
@@ -1359,8 +1043,8 @@ function GenreTreeWheelHandoff({ skeleton, children, onRevealedChange }) {
1359
1043
  cancelAnimationFrame(innerFrame);
1360
1044
  };
1361
1045
  }, []);
1362
- return /* @__PURE__ */ (0, import_jsx_runtime19.jsxs)("div", { className: "relative w-full h-full", children: [
1363
- /* @__PURE__ */ (0, import_jsx_runtime19.jsx)(
1046
+ return /* @__PURE__ */ (0, import_jsx_runtime15.jsxs)("div", { className: "relative w-full h-full", children: [
1047
+ /* @__PURE__ */ (0, import_jsx_runtime15.jsx)(
1364
1048
  "div",
1365
1049
  {
1366
1050
  className: "absolute inset-0 transition-opacity duration-150 ease-out",
@@ -1369,15 +1053,15 @@ function GenreTreeWheelHandoff({ skeleton, children, onRevealedChange }) {
1369
1053
  children
1370
1054
  }
1371
1055
  ),
1372
- !revealed && /* @__PURE__ */ (0, import_jsx_runtime19.jsx)("div", { className: "absolute inset-0", "aria-hidden": "true", children: skeleton })
1056
+ !revealed && /* @__PURE__ */ (0, import_jsx_runtime15.jsx)("div", { className: "absolute inset-0", "aria-hidden": "true", children: skeleton })
1373
1057
  ] });
1374
1058
  }
1375
1059
 
1376
1060
  // src/genre-tree/GenreSearch.tsx
1377
1061
  var import_lucide_react7 = require("lucide-react");
1378
- var import_ui8 = require("@behindthemusictree/ui");
1379
- var import_genre_tree9 = require("@behindthemusictree/app-kit/genre-tree");
1380
- var import_jsx_runtime20 = require("react/jsx-runtime");
1062
+ var import_ui6 = require("@behindthemusictree/ui");
1063
+ var import_genre_tree2 = require("@behindthemusictree/app-kit/genre-tree");
1064
+ var import_jsx_runtime16 = require("react/jsx-runtime");
1381
1065
  function GenreSearch({
1382
1066
  genrePlaylists,
1383
1067
  onSelect,
@@ -1388,7 +1072,7 @@ function GenreSearch({
1388
1072
  onQueryChange,
1389
1073
  extraResults
1390
1074
  }) {
1391
- const { query, setQuery, results, canClear, clear, select } = (0, import_genre_tree9.useGenreSearch)({
1075
+ const { query, setQuery, results, canClear, clear, select } = (0, import_genre_tree2.useGenreSearch)({
1392
1076
  genrePlaylists,
1393
1077
  onSelect,
1394
1078
  selectedName,
@@ -1399,8 +1083,8 @@ function GenreSearch({
1399
1083
  const loading = query !== "" && query !== selectedName && (extraResults?.loading ?? false);
1400
1084
  const showList = results.length > 0 || extraItems.length > 0;
1401
1085
  const heading = "px-3 pt-2 pb-1 text-xs font-medium uppercase text-[var(--color-text-muted)]";
1402
- return /* @__PURE__ */ (0, import_jsx_runtime20.jsxs)("div", { className: "genre-search relative w-[calc(var(--layout-detail-panel-w,25.5rem)-2*var(--layout-panel-inset,1rem))] shrink-0 max-md:w-full", children: [
1403
- onPrevious && /* @__PURE__ */ (0, import_jsx_runtime20.jsx)(
1086
+ return /* @__PURE__ */ (0, import_jsx_runtime16.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: [
1087
+ onPrevious && /* @__PURE__ */ (0, import_jsx_runtime16.jsx)(
1404
1088
  "button",
1405
1089
  {
1406
1090
  type: "button",
@@ -1408,11 +1092,11 @@ function GenreSearch({
1408
1092
  title: "Previous",
1409
1093
  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)]",
1410
1094
  onClick: onPrevious,
1411
- children: /* @__PURE__ */ (0, import_jsx_runtime20.jsx)(import_lucide_react7.ArrowLeft, { "aria-hidden": "true", className: "w-4 h-4" })
1095
+ children: /* @__PURE__ */ (0, import_jsx_runtime16.jsx)(import_lucide_react7.ArrowLeft, { "aria-hidden": "true", className: "w-4 h-4" })
1412
1096
  }
1413
1097
  ),
1414
- /* @__PURE__ */ (0, import_jsx_runtime20.jsx)(
1415
- import_ui8.Input,
1098
+ /* @__PURE__ */ (0, import_jsx_runtime16.jsx)(
1099
+ import_ui6.Input,
1416
1100
  {
1417
1101
  type: "text",
1418
1102
  value: query,
@@ -1429,21 +1113,21 @@ function GenreSearch({
1429
1113
  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"
1430
1114
  }
1431
1115
  ),
1432
- /* @__PURE__ */ (0, import_jsx_runtime20.jsxs)("div", { className: "absolute right-3 top-1/2 flex items-center -translate-y-1/2", children: [
1433
- /* @__PURE__ */ (0, import_jsx_runtime20.jsxs)(
1116
+ /* @__PURE__ */ (0, import_jsx_runtime16.jsxs)("div", { className: "absolute right-3 top-1/2 flex items-center -translate-y-1/2", children: [
1117
+ /* @__PURE__ */ (0, import_jsx_runtime16.jsxs)(
1434
1118
  "span",
1435
1119
  {
1436
1120
  "data-loading": loading,
1437
1121
  className: "relative p-2 text-[var(--color-text-muted)] pointer-events-none",
1438
1122
  children: [
1439
- /* @__PURE__ */ (0, import_jsx_runtime20.jsx)(
1123
+ /* @__PURE__ */ (0, import_jsx_runtime16.jsx)(
1440
1124
  import_lucide_react7.Search,
1441
1125
  {
1442
1126
  "aria-hidden": "true",
1443
1127
  className: `w-6 h-6 transition-opacity duration-200 motion-reduce:transition-none ${loading ? "opacity-0" : ""}`
1444
1128
  }
1445
1129
  ),
1446
- /* @__PURE__ */ (0, import_jsx_runtime20.jsx)(
1130
+ /* @__PURE__ */ (0, import_jsx_runtime16.jsx)(
1447
1131
  import_lucide_react7.LoaderCircle,
1448
1132
  {
1449
1133
  "aria-hidden": "true",
@@ -1453,7 +1137,7 @@ function GenreSearch({
1453
1137
  ]
1454
1138
  }
1455
1139
  ),
1456
- canClear && /* @__PURE__ */ (0, import_jsx_runtime20.jsx)(
1140
+ canClear && /* @__PURE__ */ (0, import_jsx_runtime16.jsx)(
1457
1141
  "button",
1458
1142
  {
1459
1143
  type: "button",
@@ -1463,14 +1147,14 @@ function GenreSearch({
1463
1147
  clear();
1464
1148
  onQueryChange?.("");
1465
1149
  },
1466
- children: /* @__PURE__ */ (0, import_jsx_runtime20.jsx)(import_lucide_react7.X, { "aria-hidden": "true", className: "w-6 h-6" })
1150
+ children: /* @__PURE__ */ (0, import_jsx_runtime16.jsx)(import_lucide_react7.X, { "aria-hidden": "true", className: "w-6 h-6" })
1467
1151
  }
1468
1152
  )
1469
1153
  ] }),
1470
- extraResults?.status && /* @__PURE__ */ (0, import_jsx_runtime20.jsx)("p", { role: "status", "aria-live": "polite", className: "sr-only", children: extraResults.status }),
1471
- showList && /* @__PURE__ */ (0, import_jsx_runtime20.jsxs)("ul", { className: "genre-search-results absolute z-40 w-full max-h-[min(16rem,50dvh)] mt-1 overflow-y-auto bg-[var(--color-surface)] border border-[var(--color-border)] rounded-xl shadow-md", children: [
1472
- extraResults && results.length > 0 && /* @__PURE__ */ (0, import_jsx_runtime20.jsx)("li", { className: heading, children: "Genres" }),
1473
- results.map((genrePlaylist) => /* @__PURE__ */ (0, import_jsx_runtime20.jsx)("li", { children: /* @__PURE__ */ (0, import_jsx_runtime20.jsxs)(
1154
+ extraResults?.status && /* @__PURE__ */ (0, import_jsx_runtime16.jsx)("p", { role: "status", "aria-live": "polite", className: "sr-only", children: extraResults.status }),
1155
+ showList && /* @__PURE__ */ (0, import_jsx_runtime16.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: [
1156
+ extraResults && results.length > 0 && /* @__PURE__ */ (0, import_jsx_runtime16.jsx)("li", { className: heading, children: "Genres" }),
1157
+ results.map((genrePlaylist) => /* @__PURE__ */ (0, import_jsx_runtime16.jsx)("li", { children: /* @__PURE__ */ (0, import_jsx_runtime16.jsxs)(
1474
1158
  "button",
1475
1159
  {
1476
1160
  type: "button",
@@ -1481,12 +1165,12 @@ function GenreSearch({
1481
1165
  },
1482
1166
  children: [
1483
1167
  genrePlaylist.name,
1484
- genrePlaylist.isUmbrella && /* @__PURE__ */ (0, import_jsx_runtime20.jsx)("span", { className: "ml-2 text-xs italic text-[var(--color-text-muted)]", children: "umbrella" })
1168
+ genrePlaylist.isUmbrella && /* @__PURE__ */ (0, import_jsx_runtime16.jsx)("span", { className: "ml-2 text-xs italic text-[var(--color-text-muted)]", children: "umbrella" })
1485
1169
  ]
1486
1170
  }
1487
1171
  ) }, genrePlaylist.uuid)),
1488
- extraResults && extraItems.length > 0 && /* @__PURE__ */ (0, import_jsx_runtime20.jsx)("li", { className: heading, children: extraResults.label }),
1489
- extraItems.map((item) => /* @__PURE__ */ (0, import_jsx_runtime20.jsx)("li", { children: /* @__PURE__ */ (0, import_jsx_runtime20.jsx)(
1172
+ extraResults && extraItems.length > 0 && /* @__PURE__ */ (0, import_jsx_runtime16.jsx)("li", { className: heading, children: extraResults.label }),
1173
+ extraItems.map((item) => /* @__PURE__ */ (0, import_jsx_runtime16.jsx)("li", { children: /* @__PURE__ */ (0, import_jsx_runtime16.jsx)(
1490
1174
  "button",
1491
1175
  {
1492
1176
  type: "button",
@@ -1503,26 +1187,29 @@ function GenreSearch({
1503
1187
  ] });
1504
1188
  }
1505
1189
 
1190
+ // src/genre-tree/GenreInfoPanelBody.tsx
1191
+ var import_player6 = require("@behindthemusictree/app-kit/player");
1192
+
1506
1193
  // src/genre-tree/GenreActionBand.tsx
1507
1194
  var import_lucide_react11 = require("lucide-react");
1508
1195
 
1509
1196
  // src/genre-tree/ShareActionButton.tsx
1510
1197
  var import_lucide_react10 = require("lucide-react");
1511
- var import_popup5 = require("@behindthemusictree/app-kit/popup");
1198
+ var import_popup4 = require("@behindthemusictree/app-kit/popup");
1512
1199
 
1513
1200
  // src/genre-tree/SharePopup.tsx
1514
- var import_react10 = require("react");
1201
+ var import_react8 = require("react");
1515
1202
  var import_lucide_react9 = require("lucide-react");
1516
- var import_fa4 = require("react-icons/fa");
1517
- var import_si2 = require("react-icons/si");
1203
+ var import_fa3 = require("react-icons/fa");
1204
+ var import_si = require("react-icons/si");
1518
1205
 
1519
1206
  // src/genre-tree/ShareDestinationButton.tsx
1520
- var import_jsx_runtime21 = require("react/jsx-runtime");
1207
+ var import_jsx_runtime17 = require("react/jsx-runtime");
1521
1208
  var className = "group flex min-w-0 flex-1 flex-col items-center justify-center gap-1.5 rounded-xl px-2 py-2 text-xs text-[var(--color-text)] transition-colors hover:bg-[var(--color-surface-muted)] focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[var(--color-focus-ring)] disabled:cursor-not-allowed disabled:opacity-50";
1522
1209
  function Contents({ icon, label }) {
1523
- return /* @__PURE__ */ (0, import_jsx_runtime21.jsxs)(import_jsx_runtime21.Fragment, { children: [
1524
- /* @__PURE__ */ (0, import_jsx_runtime21.jsx)("span", { className: "flex h-12 w-12 items-center justify-center rounded-full bg-[var(--color-surface-muted)] transition-colors group-hover:bg-[var(--color-border)]", children: icon }),
1525
- /* @__PURE__ */ (0, import_jsx_runtime21.jsx)("span", { className: "max-w-full truncate", children: label })
1210
+ return /* @__PURE__ */ (0, import_jsx_runtime17.jsxs)(import_jsx_runtime17.Fragment, { children: [
1211
+ /* @__PURE__ */ (0, import_jsx_runtime17.jsx)("span", { className: "flex h-12 w-12 items-center justify-center rounded-full bg-[var(--color-surface-muted)] transition-colors group-hover:bg-[var(--color-border)]", children: icon }),
1212
+ /* @__PURE__ */ (0, import_jsx_runtime17.jsx)("span", { className: "max-w-full truncate", children: label })
1526
1213
  ] });
1527
1214
  }
1528
1215
  function ShareDestinationButton({
@@ -1534,29 +1221,29 @@ function ShareDestinationButton({
1534
1221
  ariaLabel
1535
1222
  }) {
1536
1223
  if (href) {
1537
- return /* @__PURE__ */ (0, import_jsx_runtime21.jsx)("a", { href, target: "_blank", rel: "noopener noreferrer", "aria-label": ariaLabel, className, children: /* @__PURE__ */ (0, import_jsx_runtime21.jsx)(Contents, { icon, label }) });
1224
+ return /* @__PURE__ */ (0, import_jsx_runtime17.jsx)("a", { href, target: "_blank", rel: "noopener noreferrer", "aria-label": ariaLabel, className, children: /* @__PURE__ */ (0, import_jsx_runtime17.jsx)(Contents, { icon, label }) });
1538
1225
  }
1539
- return /* @__PURE__ */ (0, import_jsx_runtime21.jsx)("button", { type: "button", onClick, disabled, "aria-label": ariaLabel, className, children: /* @__PURE__ */ (0, import_jsx_runtime21.jsx)(Contents, { icon, label }) });
1226
+ return /* @__PURE__ */ (0, import_jsx_runtime17.jsx)("button", { type: "button", onClick, disabled, "aria-label": ariaLabel, className, children: /* @__PURE__ */ (0, import_jsx_runtime17.jsx)(Contents, { icon, label }) });
1540
1227
  }
1541
1228
 
1542
1229
  // src/genre-tree/SharePreviewCard.tsx
1543
1230
  var import_lucide_react8 = require("lucide-react");
1544
- var import_jsx_runtime22 = require("react/jsx-runtime");
1231
+ var import_jsx_runtime18 = require("react/jsx-runtime");
1545
1232
  function SharePreviewCard({ target, url }) {
1546
1233
  const Icon = target.kind === "genre" ? import_lucide_react8.Music2 : import_lucide_react8.UserRound;
1547
1234
  const typeLabel = target.kind === "genre" ? "Genre" : "Artist";
1548
- return /* @__PURE__ */ (0, import_jsx_runtime22.jsx)("div", { className: "rounded-3xl bg-[var(--color-surface-muted)] p-4", children: /* @__PURE__ */ (0, import_jsx_runtime22.jsxs)("div", { className: "flex min-h-32 overflow-hidden rounded-2xl bg-[var(--color-surface)] shadow-lg", children: [
1549
- /* @__PURE__ */ (0, import_jsx_runtime22.jsx)("div", { className: "flex w-28 shrink-0 items-center justify-center bg-[var(--color-primary)]/15 text-[var(--color-primary)]", children: /* @__PURE__ */ (0, import_jsx_runtime22.jsx)(Icon, { "aria-hidden": "true", className: "h-10 w-10" }) }),
1550
- /* @__PURE__ */ (0, import_jsx_runtime22.jsxs)("div", { className: "flex min-w-0 flex-1 flex-col justify-center gap-1 p-4", children: [
1551
- /* @__PURE__ */ (0, import_jsx_runtime22.jsx)("span", { className: "text-xs font-medium uppercase tracking-wide text-[var(--color-text-muted)]", children: typeLabel }),
1552
- /* @__PURE__ */ (0, import_jsx_runtime22.jsx)("span", { className: "truncate text-base font-semibold text-[var(--color-text)]", title: target.name, children: target.name }),
1553
- /* @__PURE__ */ (0, import_jsx_runtime22.jsx)("span", { className: "truncate text-sm text-[var(--color-primary)]", title: url, children: url })
1235
+ return /* @__PURE__ */ (0, import_jsx_runtime18.jsx)("div", { className: "rounded-3xl bg-[var(--color-surface-muted)] p-4", children: /* @__PURE__ */ (0, import_jsx_runtime18.jsxs)("div", { className: "flex min-h-32 overflow-hidden rounded-2xl bg-[var(--color-surface)] shadow-lg", children: [
1236
+ /* @__PURE__ */ (0, import_jsx_runtime18.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_runtime18.jsx)(Icon, { "aria-hidden": "true", className: "h-10 w-10" }) }),
1237
+ /* @__PURE__ */ (0, import_jsx_runtime18.jsxs)("div", { className: "flex min-w-0 flex-1 flex-col justify-center gap-1 p-4", children: [
1238
+ /* @__PURE__ */ (0, import_jsx_runtime18.jsx)("span", { className: "text-xs font-medium uppercase tracking-wide text-[var(--color-text-muted)]", children: typeLabel }),
1239
+ /* @__PURE__ */ (0, import_jsx_runtime18.jsx)("span", { className: "truncate text-base font-semibold text-[var(--color-text)]", title: target.name, children: target.name }),
1240
+ /* @__PURE__ */ (0, import_jsx_runtime18.jsx)("span", { className: "truncate text-sm text-[var(--color-primary)]", title: url, children: url })
1554
1241
  ] })
1555
1242
  ] }) });
1556
1243
  }
1557
1244
 
1558
1245
  // src/genre-tree/SharePopup.tsx
1559
- var import_jsx_runtime23 = require("react/jsx-runtime");
1246
+ var import_jsx_runtime19 = require("react/jsx-runtime");
1560
1247
  function resolveAbsoluteUrl(url) {
1561
1248
  try {
1562
1249
  const parsed = new URL(url, window.location.href);
@@ -1589,20 +1276,20 @@ async function copyText(text) {
1589
1276
  }
1590
1277
  }
1591
1278
  function SharePopup({ target, url, onClose }) {
1592
- const [absoluteUrl, setAbsoluteUrl] = (0, import_react10.useState)(url);
1593
- const [copyStatus, setCopyStatus] = (0, import_react10.useState)("idle");
1594
- const [canUseNativeShare, setCanUseNativeShare] = (0, import_react10.useState)(false);
1595
- const [shareError, setShareError] = (0, import_react10.useState)(null);
1596
- const [toastId, setToastId] = (0, import_react10.useState)(0);
1597
- (0, import_react10.useEffect)(() => {
1279
+ const [absoluteUrl, setAbsoluteUrl] = (0, import_react8.useState)(url);
1280
+ const [copyStatus, setCopyStatus] = (0, import_react8.useState)("idle");
1281
+ const [canUseNativeShare, setCanUseNativeShare] = (0, import_react8.useState)(false);
1282
+ const [shareError, setShareError] = (0, import_react8.useState)(null);
1283
+ const [toastId, setToastId] = (0, import_react8.useState)(0);
1284
+ (0, import_react8.useEffect)(() => {
1598
1285
  setAbsoluteUrl(resolveAbsoluteUrl(url) ?? "");
1599
1286
  setCopyStatus("idle");
1600
1287
  setShareError(null);
1601
1288
  }, [url]);
1602
- (0, import_react10.useEffect)(() => {
1289
+ (0, import_react8.useEffect)(() => {
1603
1290
  setCanUseNativeShare(typeof navigator !== "undefined" && typeof navigator.share === "function");
1604
1291
  }, []);
1605
- const destinationUrls = (0, import_react10.useMemo)(() => {
1292
+ const destinationUrls = (0, import_react8.useMemo)(() => {
1606
1293
  if (!absoluteUrl) return null;
1607
1294
  const message = `${target.name} \u2014 ${absoluteUrl}`;
1608
1295
  return {
@@ -1620,7 +1307,7 @@ function SharePopup({ target, url, onClose }) {
1620
1307
  setCopyStatus(copied ? "idle" : "failed");
1621
1308
  if (copied) setToastId((id) => id + 1);
1622
1309
  };
1623
- const dismissToast = (0, import_react10.useCallback)(() => setToastId(0), []);
1310
+ const dismissToast = (0, import_react8.useCallback)(() => setToastId(0), []);
1624
1311
  const handleNativeShare = async () => {
1625
1312
  if (!absoluteUrl || !navigator.share) return;
1626
1313
  setShareError(null);
@@ -1631,8 +1318,8 @@ function SharePopup({ target, url, onClose }) {
1631
1318
  setShareError("Could not open the share options.");
1632
1319
  }
1633
1320
  };
1634
- return /* @__PURE__ */ (0, import_jsx_runtime23.jsxs)(import_jsx_runtime23.Fragment, { children: [
1635
- /* @__PURE__ */ (0, import_jsx_runtime23.jsx)(
1321
+ return /* @__PURE__ */ (0, import_jsx_runtime19.jsxs)(import_jsx_runtime19.Fragment, { children: [
1322
+ /* @__PURE__ */ (0, import_jsx_runtime19.jsx)(
1636
1323
  BasePopup_default,
1637
1324
  {
1638
1325
  title: "Share",
@@ -1647,175 +1334,550 @@ function SharePopup({ target, url, onClose }) {
1647
1334
  titleTextClassName: "!text-xl !font-semibold !text-[var(--color-text)]",
1648
1335
  closeButtonClassName: "!h-10 !w-10 !rounded-none !border-0 !bg-transparent !p-0 !text-[var(--color-text)] hover:!bg-transparent hover:!text-[var(--color-text)]",
1649
1336
  contentClassName: "!rounded-none !bg-transparent !px-6 !pb-6 !pt-2",
1650
- children: /* @__PURE__ */ (0, import_jsx_runtime23.jsxs)("div", { className: "flex flex-col gap-4", children: [
1651
- /* @__PURE__ */ (0, import_jsx_runtime23.jsx)(SharePreviewCard, { target, url: absoluteUrl || url }),
1652
- /* @__PURE__ */ (0, import_jsx_runtime23.jsxs)("div", { className: "grid grid-cols-3 gap-1 rounded-3xl bg-[var(--color-surface)] p-3 sm:grid-cols-4", children: [
1653
- /* @__PURE__ */ (0, import_jsx_runtime23.jsx)(
1337
+ children: /* @__PURE__ */ (0, import_jsx_runtime19.jsxs)("div", { className: "flex flex-col gap-4", children: [
1338
+ /* @__PURE__ */ (0, import_jsx_runtime19.jsx)(SharePreviewCard, { target, url: absoluteUrl || url }),
1339
+ /* @__PURE__ */ (0, import_jsx_runtime19.jsxs)("div", { className: "grid grid-cols-3 gap-1 rounded-3xl bg-[var(--color-surface)] p-3 sm:grid-cols-4", children: [
1340
+ /* @__PURE__ */ (0, import_jsx_runtime19.jsx)(
1654
1341
  ShareDestinationButton,
1655
1342
  {
1656
1343
  label: "Copy Link",
1657
- icon: /* @__PURE__ */ (0, import_jsx_runtime23.jsx)(import_lucide_react9.Link2, { "aria-hidden": "true", className: "h-6 w-6" }),
1344
+ icon: /* @__PURE__ */ (0, import_jsx_runtime19.jsx)(import_lucide_react9.Link2, { "aria-hidden": "true", className: "h-6 w-6" }),
1658
1345
  onClick: () => void handleCopy(),
1659
1346
  ariaLabel: "Copy link",
1660
1347
  disabled: !absoluteUrl
1661
1348
  }
1662
1349
  ),
1663
- /* @__PURE__ */ (0, import_jsx_runtime23.jsx)(
1350
+ /* @__PURE__ */ (0, import_jsx_runtime19.jsx)(
1664
1351
  ShareDestinationButton,
1665
1352
  {
1666
1353
  label: "WhatsApp",
1667
- icon: /* @__PURE__ */ (0, import_jsx_runtime23.jsx)(import_fa4.FaWhatsapp, { "aria-hidden": "true", className: "h-9 w-9 text-[#25D366]" }),
1354
+ icon: /* @__PURE__ */ (0, import_jsx_runtime19.jsx)(import_fa3.FaWhatsapp, { "aria-hidden": "true", className: "h-9 w-9 text-[#25D366]" }),
1668
1355
  href: destinationUrls?.whatsapp,
1669
1356
  ariaLabel: `Share ${target.name} via WhatsApp`,
1670
1357
  disabled: !absoluteUrl
1671
1358
  }
1672
1359
  ),
1673
- /* @__PURE__ */ (0, import_jsx_runtime23.jsx)(
1360
+ /* @__PURE__ */ (0, import_jsx_runtime19.jsx)(
1674
1361
  ShareDestinationButton,
1675
1362
  {
1676
1363
  label: "Gmail",
1677
- icon: /* @__PURE__ */ (0, import_jsx_runtime23.jsx)(import_si2.SiGmail, { "aria-hidden": "true", className: "h-8 w-8 text-[#EA4335]" }),
1364
+ icon: /* @__PURE__ */ (0, import_jsx_runtime19.jsx)(import_si.SiGmail, { "aria-hidden": "true", className: "h-8 w-8 text-[#EA4335]" }),
1678
1365
  href: destinationUrls?.gmail,
1679
1366
  ariaLabel: `Email ${target.name} with Gmail`,
1680
1367
  disabled: !absoluteUrl
1681
1368
  }
1682
1369
  ),
1683
- canUseNativeShare && /* @__PURE__ */ (0, import_jsx_runtime23.jsx)(
1370
+ canUseNativeShare && /* @__PURE__ */ (0, import_jsx_runtime19.jsx)(
1684
1371
  ShareDestinationButton,
1685
1372
  {
1686
1373
  label: "More",
1687
- icon: /* @__PURE__ */ (0, import_jsx_runtime23.jsx)(import_lucide_react9.Share2, { "aria-hidden": "true", className: "h-6 w-6" }),
1374
+ icon: /* @__PURE__ */ (0, import_jsx_runtime19.jsx)(import_lucide_react9.Share2, { "aria-hidden": "true", className: "h-6 w-6" }),
1688
1375
  onClick: () => void handleNativeShare(),
1689
1376
  ariaLabel: "More sharing options",
1690
1377
  disabled: !absoluteUrl
1691
1378
  }
1692
1379
  )
1693
1380
  ] }),
1694
- copyStatus === "failed" && /* @__PURE__ */ (0, import_jsx_runtime23.jsx)("p", { role: "alert", className: "text-center text-sm text-[var(--color-red-500)]", children: "Could not copy the link." }),
1695
- shareError && /* @__PURE__ */ (0, import_jsx_runtime23.jsx)("p", { role: "alert", className: "text-center text-sm text-[var(--color-red-500)]", children: shareError })
1381
+ copyStatus === "failed" && /* @__PURE__ */ (0, import_jsx_runtime19.jsx)("p", { role: "alert", className: "text-center text-sm text-[var(--color-red-500)]", children: "Could not copy the link." }),
1382
+ shareError && /* @__PURE__ */ (0, import_jsx_runtime19.jsx)("p", { role: "alert", className: "text-center text-sm text-[var(--color-red-500)]", children: shareError })
1696
1383
  ] })
1697
1384
  }
1698
1385
  ),
1699
- toastId > 0 && /* @__PURE__ */ (0, import_jsx_runtime23.jsx)(Toast, { message: "Link copied", onClose: dismissToast }, toastId)
1386
+ toastId > 0 && /* @__PURE__ */ (0, import_jsx_runtime19.jsx)(Toast, { message: "Link copied", onClose: dismissToast }, toastId)
1700
1387
  ] });
1701
1388
  }
1702
1389
 
1703
1390
  // src/genre-tree/ShareActionButton.tsx
1704
- var import_jsx_runtime24 = require("react/jsx-runtime");
1391
+ var import_jsx_runtime20 = require("react/jsx-runtime");
1705
1392
  function ShareActionButton({ target, url }) {
1706
- const { showPopup, hidePopup } = (0, import_popup5.usePopup)();
1393
+ const { showPopup, hidePopup } = (0, import_popup4.usePopup)();
1707
1394
  const openSharePopup = () => {
1708
- showPopup(/* @__PURE__ */ (0, import_jsx_runtime24.jsx)(SharePopup, { target, url, onClose: hidePopup }));
1395
+ showPopup(/* @__PURE__ */ (0, import_jsx_runtime20.jsx)(SharePopup, { target, url, onClose: hidePopup }));
1396
+ };
1397
+ return /* @__PURE__ */ (0, import_jsx_runtime20.jsxs)(
1398
+ "button",
1399
+ {
1400
+ type: "button",
1401
+ onClick: openSharePopup,
1402
+ className: "group flex flex-col items-center gap-1 w-16 text-xs text-center text-[var(--color-text)] focus-visible:outline-none",
1403
+ "aria-label": `Share ${target.name}`,
1404
+ title: `Share ${target.name}`,
1405
+ children: [
1406
+ /* @__PURE__ */ (0, import_jsx_runtime20.jsx)("span", { className: "flex items-center justify-center w-10 h-10 rounded-full group-focus-visible:ring-2 group-focus-visible:ring-[var(--color-focus-ring)] bg-[var(--color-surface-muted)] transition-colors group-hover:bg-[var(--color-border)]", children: /* @__PURE__ */ (0, import_jsx_runtime20.jsx)(import_lucide_react10.Share2, { "aria-hidden": "true", className: "w-5 h-5" }) }),
1407
+ "Share"
1408
+ ]
1409
+ }
1410
+ );
1411
+ }
1412
+
1413
+ // src/genre-tree/GenreActionBand.tsx
1414
+ var import_jsx_runtime21 = require("react/jsx-runtime");
1415
+ function GenreActionBand({
1416
+ node,
1417
+ onPlayPause,
1418
+ isPlaying = false,
1419
+ onZoomIn,
1420
+ onAddChild,
1421
+ onRename,
1422
+ onChangeParent,
1423
+ side,
1424
+ onToggleSide,
1425
+ toggleSideDisabledReason,
1426
+ additionalActions,
1427
+ shareTarget,
1428
+ shareUrl
1429
+ }) {
1430
+ const items = [];
1431
+ const adminItems = [];
1432
+ if (onPlayPause) {
1433
+ items.push({
1434
+ key: "play-pause",
1435
+ label: isPlaying ? "Pause" : "Play",
1436
+ icon: isPlaying ? /* @__PURE__ */ (0, import_jsx_runtime21.jsx)(import_lucide_react11.Pause, { "aria-hidden": "true", className: "w-5 h-5" }) : /* @__PURE__ */ (0, import_jsx_runtime21.jsx)(import_lucide_react11.Play, { "aria-hidden": "true", className: "w-5 h-5" }),
1437
+ onClick: onPlayPause
1438
+ });
1439
+ }
1440
+ if (onZoomIn) {
1441
+ items.push({
1442
+ key: "zoom-in",
1443
+ label: "Focus",
1444
+ icon: /* @__PURE__ */ (0, import_jsx_runtime21.jsx)(import_lucide_react11.ZoomIn, { "aria-hidden": "true", className: "w-5 h-5" }),
1445
+ onClick: onZoomIn
1446
+ });
1447
+ }
1448
+ if (onAddChild) {
1449
+ adminItems.push({ key: "add-child", label: "Add", icon: /* @__PURE__ */ (0, import_jsx_runtime21.jsx)(import_lucide_react11.Plus, { "aria-hidden": "true", className: "w-5 h-5" }), onClick: onAddChild });
1450
+ }
1451
+ if (onRename) {
1452
+ adminItems.push({ key: "rename", label: "Rename", icon: /* @__PURE__ */ (0, import_jsx_runtime21.jsx)(import_lucide_react11.Pencil, { "aria-hidden": "true", className: "w-5 h-5" }), onClick: onRename });
1453
+ }
1454
+ if (onChangeParent) {
1455
+ adminItems.push({ key: "change-parent", label: "Parent", icon: /* @__PURE__ */ (0, import_jsx_runtime21.jsx)(import_lucide_react11.MoveUpRight, { "aria-hidden": "true", className: "w-5 h-5" }), onClick: onChangeParent });
1456
+ }
1457
+ if (onToggleSide) {
1458
+ adminItems.push({
1459
+ key: "toggle-side",
1460
+ label: side === "pop" ? "Make core" : "Make pop",
1461
+ icon: /* @__PURE__ */ (0, import_jsx_runtime21.jsx)(import_lucide_react11.ArrowLeftRight, { "aria-hidden": "true", className: "w-5 h-5" }),
1462
+ onClick: onToggleSide,
1463
+ disabled: !!toggleSideDisabledReason,
1464
+ title: toggleSideDisabledReason
1465
+ });
1466
+ }
1467
+ for (const action of additionalActions?.(node) ?? []) {
1468
+ items.push({
1469
+ key: action.key,
1470
+ label: action.label(node),
1471
+ icon: action.icon(node),
1472
+ onClick: (event) => action.onClick(event, node),
1473
+ disabled: action.enabled ? !action.enabled(node) : false
1474
+ });
1475
+ }
1476
+ if (items.length === 0 && (!shareTarget || !shareUrl) && adminItems.length === 0) return null;
1477
+ const renderItems = (list) => list.map((item) => /* @__PURE__ */ (0, import_jsx_runtime21.jsxs)(
1478
+ "button",
1479
+ {
1480
+ type: "button",
1481
+ disabled: item.disabled,
1482
+ title: item.title,
1483
+ onClick: item.onClick,
1484
+ className: "group flex flex-col items-center gap-1 w-16 text-xs text-center text-[var(--color-text)] focus-visible:outline-none disabled:opacity-50",
1485
+ children: [
1486
+ /* @__PURE__ */ (0, import_jsx_runtime21.jsx)("span", { className: "flex items-center justify-center w-10 h-10 rounded-full group-focus-visible:ring-2 group-focus-visible:ring-[var(--color-focus-ring)] bg-[var(--color-surface-muted)] transition-colors group-hover:bg-[var(--color-border)] group-disabled:group-hover:bg-[var(--color-surface-muted)]", children: item.icon }),
1487
+ item.label
1488
+ ]
1489
+ },
1490
+ item.key
1491
+ ));
1492
+ return /* @__PURE__ */ (0, import_jsx_runtime21.jsxs)("div", { className: "genre-action-band border-b border-[var(--color-border)]", children: [
1493
+ (items.length > 0 || shareTarget && shareUrl) && /* @__PURE__ */ (0, import_jsx_runtime21.jsxs)("div", { className: "flex flex-wrap gap-1 px-3.5 py-3", children: [
1494
+ renderItems(items),
1495
+ shareTarget && shareUrl && /* @__PURE__ */ (0, import_jsx_runtime21.jsx)(ShareActionButton, { target: shareTarget, url: shareUrl })
1496
+ ] }),
1497
+ adminItems.length > 0 && /* @__PURE__ */ (0, import_jsx_runtime21.jsxs)(
1498
+ "section",
1499
+ {
1500
+ "aria-label": "Admin actions",
1501
+ className: `px-3.5 py-3 ${items.length > 0 ? "border-t border-[var(--color-border)]" : ""}`,
1502
+ children: [
1503
+ /* @__PURE__ */ (0, import_jsx_runtime21.jsx)("h3", { className: "mb-2 text-xs font-semibold uppercase tracking-wide text-[var(--color-text-muted)]", children: "Admin" }),
1504
+ /* @__PURE__ */ (0, import_jsx_runtime21.jsx)("div", { className: "flex flex-wrap gap-1", children: renderItems(adminItems) })
1505
+ ]
1506
+ }
1507
+ )
1508
+ ] });
1509
+ }
1510
+
1511
+ // src/genre-tree/GenreDetailExtras.tsx
1512
+ var import_ui8 = require("@behindthemusictree/ui");
1513
+ var import_genre_tree9 = require("@behindthemusictree/app-kit/genre-tree");
1514
+
1515
+ // src/genre-tree/ClampedText.tsx
1516
+ var import_react9 = require("react");
1517
+ var import_jsx_runtime22 = require("react/jsx-runtime");
1518
+ function ClampedText({ children, className: className2 = "" }) {
1519
+ const ref = (0, import_react9.useRef)(null);
1520
+ const [expanded, setExpanded] = (0, import_react9.useState)(false);
1521
+ const [overflows, setOverflows] = (0, import_react9.useState)(false);
1522
+ (0, import_react9.useLayoutEffect)(() => {
1523
+ const element = ref.current;
1524
+ if (!element || expanded) return;
1525
+ setOverflows(element.scrollHeight > element.clientHeight);
1526
+ }, [children, expanded]);
1527
+ return /* @__PURE__ */ (0, import_jsx_runtime22.jsxs)(import_jsx_runtime22.Fragment, { children: [
1528
+ /* @__PURE__ */ (0, import_jsx_runtime22.jsx)("p", { ref, className: `${className2} ${expanded ? "" : "line-clamp-3"}`, children }),
1529
+ (overflows || expanded) && /* @__PURE__ */ (0, import_jsx_runtime22.jsx)(
1530
+ "button",
1531
+ {
1532
+ type: "button",
1533
+ "aria-expanded": expanded,
1534
+ onClick: () => setExpanded((value) => !value),
1535
+ className: "mt-1 text-xs text-[var(--color-text-muted)] underline hover:text-[var(--color-text)]",
1536
+ children: expanded ? "Show less" : "Show more"
1537
+ }
1538
+ )
1539
+ ] });
1540
+ }
1541
+
1542
+ // src/genre-tree/GenrePlaylistTracks.tsx
1543
+ var import_react10 = require("react");
1544
+ var import_react_query = require("@tanstack/react-query");
1545
+ var import_zod = require("zod");
1546
+ var import_fa4 = require("react-icons/fa");
1547
+ var import_ui7 = require("@behindthemusictree/ui");
1548
+ var import_auth2 = require("@behindthemusictree/app-kit/auth");
1549
+ var import_transport4 = require("@behindthemusictree/app-kit/transport");
1550
+ var import_transport5 = require("@behindthemusictree/app-kit/transport");
1551
+ var import_transport6 = require("@behindthemusictree/app-kit/transport");
1552
+ var import_player4 = require("@behindthemusictree/app-kit/player");
1553
+ var import_player5 = require("@behindthemusictree/app-kit/player");
1554
+ var import_popup5 = require("@behindthemusictree/app-kit/popup");
1555
+ var import_transport7 = require("@behindthemusictree/app-kit/transport");
1556
+ var import_genre_tree4 = require("@behindthemusictree/app-kit/genre-tree");
1557
+ var import_genre_tree5 = require("@behindthemusictree/app-kit/genre-tree");
1558
+ var import_genre_tree6 = require("@behindthemusictree/app-kit/genre-tree");
1559
+ var import_genre_tree7 = require("@behindthemusictree/app-kit/genre-tree");
1560
+
1561
+ // src/genre-tree/MusicBrainzRecordingLink.tsx
1562
+ var import_genre_tree3 = require("@behindthemusictree/app-kit/genre-tree");
1563
+ var import_jsx_runtime23 = require("react/jsx-runtime");
1564
+ function MusicBrainzRecordingLink({
1565
+ song,
1566
+ children,
1567
+ className: className2 = ""
1568
+ }) {
1569
+ const url = (0, import_genre_tree3.musicBrainzRecordingUrl)(song);
1570
+ if (!url) return /* @__PURE__ */ (0, import_jsx_runtime23.jsx)(import_jsx_runtime23.Fragment, { children });
1571
+ return /* @__PURE__ */ (0, import_jsx_runtime23.jsx)(
1572
+ "a",
1573
+ {
1574
+ href: url,
1575
+ target: "_blank",
1576
+ rel: "noopener noreferrer",
1577
+ "aria-label": `View ${song.title} on MusicBrainz`,
1578
+ className: className2,
1579
+ onClick: (event) => event.stopPropagation(),
1580
+ children
1581
+ }
1582
+ );
1583
+ }
1584
+
1585
+ // src/genre-tree/GenrePlaylistTracks.tsx
1586
+ var import_genre_tree8 = require("@behindthemusictree/app-kit/genre-tree");
1587
+ var import_track_noun5 = require("@behindthemusictree/app-kit/track-noun");
1588
+ var import_jsx_runtime24 = require("react/jsx-runtime");
1589
+ var CATALOGUE_PAGE_SIZE = 100;
1590
+ var SongPageSchema = (0, import_transport5.PaginatedResponseSchema)(import_genre_tree6.SongDetailedSchema).extend({
1591
+ overallTotal: import_zod.z.number().nullable(),
1592
+ page: import_zod.z.number().nullable(),
1593
+ totalPages: import_zod.z.number().nullable()
1594
+ });
1595
+ var TEXT_CELL = "min-w-0 flex-1 truncate [@container(min-width:40rem)]:flex [@container(min-width:40rem)]:gap-2";
1596
+ var TITLE_CELL = "[@container(min-width:40rem)]:flex-[2] [@container(min-width:40rem)]:truncate";
1597
+ var COLUMN_CELL = "[@container(min-width:40rem)]:flex-1 [@container(min-width:40rem)]:truncate";
1598
+ var ALBUM_CELL = `hidden [@container(min-width:40rem)]:block ${COLUMN_CELL}`;
1599
+ var SEPARATOR = "[@container(min-width:40rem)]:hidden";
1600
+ var nextCursor = (next) => {
1601
+ const cursor = new URL(next).searchParams.get("cursor");
1602
+ if (!cursor) throw new Error(`Songs next link has no cursor: ${next}`);
1603
+ return cursor;
1604
+ };
1605
+ function GenrePlaylistTracks({
1606
+ genreUuid,
1607
+ genrePlaylist,
1608
+ scope,
1609
+ getBackendBaseUrl,
1610
+ renderArtistLink,
1611
+ renderSongLeading,
1612
+ renderSongActions,
1613
+ renderTracksToolbar,
1614
+ artist = null
1615
+ }) {
1616
+ const { fetchGenrePlaylistTracksPage, playNewTrackListFromGenrePlaylist, playNewTrackListFromArtist } = (0, import_genre_tree7.useTrackList)();
1617
+ const { session, sessionRestored } = (0, import_auth2.useSession)();
1618
+ const { playerTrackObject, playState, handlePlayPauseAction } = (0, import_player4.usePlayer)();
1619
+ const { showPopup } = (0, import_popup5.usePopup)();
1620
+ const trackNoun = (0, import_track_noun5.useTrackNoun)();
1621
+ const { uuid } = genrePlaylist;
1622
+ const { fetch } = (0, import_transport4.useFetchWrapper)(getBackendBaseUrl);
1623
+ const { data, isPending, isError, refetch, hasNextPage, isFetchingNextPage, fetchNextPage } = (0, import_react_query.useInfiniteQuery)({
1624
+ queryKey: scope === "me" ? import_genre_tree4.genrePlaylistQueryKeys.me.tracks(uuid) : artist ? [...import_genre_tree5.libraryQueryKeys.reference.songs.byGenre(getBackendBaseUrl(), genreUuid), "artist", artist.uuid] : import_genre_tree5.libraryQueryKeys.reference.songs.byGenre(getBackendBaseUrl(), genreUuid),
1625
+ queryFn: async ({ pageParam }) => {
1626
+ if (scope === "me") return fetchGenrePlaylistTracksPage(uuid, scope, Number(pageParam));
1627
+ const response = await fetch(
1628
+ import_genre_tree5.libraryEndpoints.reference.songs.list(),
1629
+ true,
1630
+ false,
1631
+ {},
1632
+ { genre: genreUuid, ...artist && { artist: artist.uuid }, pageSize: CATALOGUE_PAGE_SIZE, withTotal: false, ...pageParam && { cursor: pageParam } }
1633
+ );
1634
+ const parsed = (0, import_transport6.parseWithLog)(SongPageSchema, response, "GenrePlaylistTracks");
1635
+ return { tracks: parsed.results, total: parsed.overallTotal, nextPage: parsed.next ? nextCursor(parsed.next) : null };
1636
+ },
1637
+ initialPageParam: scope === "me" ? 1 : "",
1638
+ getNextPageParam: (lastPage) => lastPage.nextPage ?? void 0,
1639
+ enabled: scope !== "me" || sessionRestored && !!session?.accessToken
1640
+ });
1641
+ const sentinelRef = (0, import_react10.useRef)(null);
1642
+ (0, import_react10.useEffect)(() => {
1643
+ const sentinel = sentinelRef.current;
1644
+ if (!hasNextPage || isFetchingNextPage || !sentinel) return;
1645
+ const observer = new IntersectionObserver((entries) => {
1646
+ if (entries.some((entry) => entry.isIntersecting)) {
1647
+ void fetchNextPage();
1648
+ }
1649
+ });
1650
+ observer.observe(sentinel);
1651
+ return () => observer.disconnect();
1652
+ }, [hasNextPage, isFetchingNextPage, fetchNextPage]);
1653
+ const pages = data?.pages ?? [];
1654
+ const tracks = pages.flatMap((page) => page.tracks);
1655
+ const total = pages[0]?.total ?? void 0;
1656
+ const play = (startSongUuid) => {
1657
+ const started = artist ? playNewTrackListFromArtist(artist, scope, { startSongUuid, ordering: "title", genreUuid }) : playNewTrackListFromGenrePlaylist(genrePlaylist, scope, { startSongUuid });
1658
+ started.catch((error) => {
1659
+ console.error("Failed to play genre playlist:", error);
1660
+ showPopup(/* @__PURE__ */ (0, import_jsx_runtime24.jsx)(InternalErrorPopup, { errorCode: import_transport7.ErrorCode.CLIENT_UNKNOWN }));
1661
+ });
1662
+ };
1663
+ return /* @__PURE__ */ (0, import_jsx_runtime24.jsxs)("div", { className: "gtv-info-panel-children", children: [
1664
+ /* @__PURE__ */ (0, import_jsx_runtime24.jsxs)("span", { className: "gtv-info-panel-children-title", children: [
1665
+ (0, import_track_noun5.capitalize)(trackNoun.plural),
1666
+ total !== void 0 && ` (${total})`
1667
+ ] }),
1668
+ renderTracksToolbar?.(),
1669
+ isPending ? /* @__PURE__ */ (0, import_jsx_runtime24.jsx)(import_ui7.Skeleton, { className: "h-4 w-3/4", "aria-busy": "true" }) : isError && tracks.length === 0 ? /* @__PURE__ */ (0, import_jsx_runtime24.jsxs)("p", { role: "alert", children: [
1670
+ "Couldn't load ",
1671
+ trackNoun.plural,
1672
+ ".",
1673
+ " ",
1674
+ /* @__PURE__ */ (0, import_jsx_runtime24.jsx)("button", { type: "button", className: "underline", onClick: () => void refetch(), children: "Retry" })
1675
+ ] }) : tracks.length === 0 ? /* @__PURE__ */ (0, import_jsx_runtime24.jsx)("p", { children: "\u2014" }) : /* @__PURE__ */ (0, import_jsx_runtime24.jsxs)("ul", { className: "mt-1 max-h-64 overflow-y-auto text-[0.8125rem] leading-[1.45] [container-type:inline-size]", "aria-label": `${genrePlaylist.name} ${trackNoun.plural}`, children: [
1676
+ tracks.map((track, index) => {
1677
+ const label = track.artists?.length ? `${track.title} \u2014 ${track.artists.map((artist2) => artist2.name).join(", ")}` : track.title;
1678
+ const isCurrent = playerTrackObject?.track.id === track.uuid;
1679
+ const isPlaying = isCurrent && playState === import_player5.PlayStates.PLAYING;
1680
+ const text = /* @__PURE__ */ (0, import_jsx_runtime24.jsxs)("span", { className: TEXT_CELL, children: [
1681
+ /* @__PURE__ */ (0, import_jsx_runtime24.jsx)("span", { className: TITLE_CELL, children: (0, import_genre_tree6.isSong)(track) ? /* @__PURE__ */ (0, import_jsx_runtime24.jsx)(MusicBrainzRecordingLink, { song: track, className: "hover:underline focus-visible:underline", children: track.title }) : track.title }),
1682
+ track.artists?.length ? /* @__PURE__ */ (0, import_jsx_runtime24.jsx)("span", { className: SEPARATOR, children: " \u2014 " }) : null,
1683
+ /* @__PURE__ */ (0, import_jsx_runtime24.jsx)("span", { className: COLUMN_CELL, children: track.artists?.length ? /* @__PURE__ */ (0, import_jsx_runtime24.jsx)(import_genre_tree8.ArtistNames, { artists: track.artists, renderArtistLink }) : null }),
1684
+ track.album && /* @__PURE__ */ (0, import_jsx_runtime24.jsx)("span", { className: ALBUM_CELL, children: track.album.name })
1685
+ ] });
1686
+ const leading = renderSongLeading?.(track, index);
1687
+ const actions = renderSongActions?.(track);
1688
+ const isUnplayable = (0, import_genre_tree6.isSong)(track) && (0, import_genre_tree6.songUnplayableReason)(track) !== null;
1689
+ if (isUnplayable) {
1690
+ return /* @__PURE__ */ (0, import_jsx_runtime24.jsxs)("li", { className: "flex min-h-8 items-center gap-1 px-1 opacity-50", children: [
1691
+ leading,
1692
+ /* @__PURE__ */ (0, import_jsx_runtime24.jsx)(
1693
+ "button",
1694
+ {
1695
+ type: "button",
1696
+ disabled: true,
1697
+ "aria-disabled": "true",
1698
+ "aria-label": label,
1699
+ className: "flex h-6 w-6 shrink-0 items-center justify-center rounded-full opacity-0 cursor-not-allowed",
1700
+ children: /* @__PURE__ */ (0, import_jsx_runtime24.jsx)(import_fa4.FaPlay, { size: 10 })
1701
+ }
1702
+ ),
1703
+ text,
1704
+ actions
1705
+ ] }, track.uuid);
1706
+ }
1707
+ return /* @__PURE__ */ (0, import_jsx_runtime24.jsxs)(
1708
+ "li",
1709
+ {
1710
+ className: `group flex min-h-8 items-center gap-1 rounded px-1 hover:bg-[var(--color-neutral-100)] ${isCurrent ? "font-medium text-[var(--color-primary)]" : ""}`,
1711
+ children: [
1712
+ leading,
1713
+ /* @__PURE__ */ (0, import_jsx_runtime24.jsx)(
1714
+ "button",
1715
+ {
1716
+ type: "button",
1717
+ "aria-label": `${isPlaying ? "Pause" : "Play"} ${label}`,
1718
+ className: `flex h-6 w-6 shrink-0 items-center justify-center rounded-full cursor-pointer group-hover:opacity-100 focus-visible:opacity-100 ${isCurrent && playState !== import_player5.PlayStates.STOPPED ? "opacity-100" : "opacity-0 [@media(pointer:coarse)]:opacity-100"}`,
1719
+ onClick: () => isCurrent ? handlePlayPauseAction() : play(track.uuid),
1720
+ children: isCurrent && playState === import_player5.PlayStates.LOADING ? /* @__PURE__ */ (0, import_jsx_runtime24.jsx)(import_ui7.RingLoader, { size: 10 }) : isPlaying ? /* @__PURE__ */ (0, import_jsx_runtime24.jsx)(import_fa4.FaPause, { size: 10 }) : /* @__PURE__ */ (0, import_jsx_runtime24.jsx)(import_fa4.FaPlay, { size: 10 })
1721
+ }
1722
+ ),
1723
+ text,
1724
+ actions
1725
+ ]
1726
+ },
1727
+ track.uuid
1728
+ );
1729
+ }),
1730
+ hasNextPage && /* @__PURE__ */ (0, import_jsx_runtime24.jsx)("li", { ref: sentinelRef, "aria-hidden": "true", className: "h-4" })
1731
+ ] })
1732
+ ] });
1733
+ }
1734
+
1735
+ // src/genre-tree/GenreDetailExtras.tsx
1736
+ var import_track_noun6 = require("@behindthemusictree/app-kit/track-noun");
1737
+ var import_si2 = require("react-icons/si");
1738
+ var import_jsx_runtime25 = require("react/jsx-runtime");
1739
+ function GenreDetailExtras({
1740
+ genreUuid,
1741
+ genrePlaylist,
1742
+ wikidataId,
1743
+ scope,
1744
+ getBackendBaseUrl,
1745
+ criteriaOverviewSchema,
1746
+ renderGenreDetailExtras,
1747
+ renderArtistLink,
1748
+ renderSummary,
1749
+ renderEssentialTracks,
1750
+ family,
1751
+ ...trackSlots
1752
+ }) {
1753
+ const trackNoun = (0, import_track_noun6.useTrackNoun)();
1754
+ const { data: overview, isPending } = (0, import_genre_tree9.useFetchGenreOverview)(
1755
+ genreUuid,
1756
+ scope,
1757
+ getBackendBaseUrl,
1758
+ criteriaOverviewSchema
1759
+ );
1760
+ if (isPending) {
1761
+ return /* @__PURE__ */ (0, import_jsx_runtime25.jsxs)(import_jsx_runtime25.Fragment, { children: [
1762
+ /* @__PURE__ */ (0, import_jsx_runtime25.jsxs)("div", { className: "gtv-info-panel-children", "aria-busy": "true", "data-testid": "genre-detail-extras-skeleton", children: [
1763
+ /* @__PURE__ */ (0, import_jsx_runtime25.jsx)("span", { className: "gtv-info-panel-children-title", children: "Summary" }),
1764
+ /* @__PURE__ */ (0, import_jsx_runtime25.jsx)(import_ui8.Skeleton, { className: "h-4 w-3/4" })
1765
+ ] }),
1766
+ family
1767
+ ] });
1768
+ }
1769
+ if (!overview) return /* @__PURE__ */ (0, import_jsx_runtime25.jsx)(import_jsx_runtime25.Fragment, { children: family });
1770
+ const { summary, summarySourceUrl, essentialSongs } = overview;
1771
+ const wikidataUrl = wikidataId ? `https://www.wikidata.org/wiki/${encodeURIComponent(wikidataId)}` : null;
1772
+ return /* @__PURE__ */ (0, import_jsx_runtime25.jsxs)(import_jsx_runtime25.Fragment, { children: [
1773
+ (summarySourceUrl || wikidataUrl) && /* @__PURE__ */ (0, import_jsx_runtime25.jsxs)("div", { className: "gtv-info-panel-children text-[0.8125rem] leading-[1.45] text-[var(--color-text)]", children: [
1774
+ /* @__PURE__ */ (0, import_jsx_runtime25.jsx)("span", { className: "gtv-info-panel-children-title", children: "Links" }),
1775
+ /* @__PURE__ */ (0, import_jsx_runtime25.jsxs)("div", { className: "mt-1 flex flex-wrap gap-x-3 gap-y-1 text-xs", children: [
1776
+ summarySourceUrl && /* @__PURE__ */ (0, import_jsx_runtime25.jsxs)(
1777
+ "a",
1778
+ {
1779
+ href: summarySourceUrl,
1780
+ target: "_blank",
1781
+ rel: "noopener noreferrer",
1782
+ className: "inline-flex items-center gap-1.5 text-[var(--color-text-muted)] hover:text-[var(--color-text)]",
1783
+ children: [
1784
+ /* @__PURE__ */ (0, import_jsx_runtime25.jsx)(import_si2.SiWikipedia, { "aria-hidden": "true", className: "h-3.5 w-3.5" }),
1785
+ "Wikipedia"
1786
+ ]
1787
+ }
1788
+ ),
1789
+ wikidataUrl && /* @__PURE__ */ (0, import_jsx_runtime25.jsxs)(
1790
+ "a",
1791
+ {
1792
+ href: wikidataUrl,
1793
+ target: "_blank",
1794
+ rel: "noopener noreferrer",
1795
+ className: "inline-flex items-center gap-1.5 text-[var(--color-text-muted)] hover:text-[var(--color-text)]",
1796
+ children: [
1797
+ /* @__PURE__ */ (0, import_jsx_runtime25.jsx)(import_si2.SiWikidata, { "aria-hidden": "true", className: "h-3.5 w-3.5" }),
1798
+ "Wikidata"
1799
+ ]
1800
+ }
1801
+ )
1802
+ ] })
1803
+ ] }),
1804
+ renderSummary ? renderSummary(overview) : /* @__PURE__ */ (0, import_jsx_runtime25.jsxs)("div", { className: "gtv-info-panel-children text-[0.8125rem] leading-[1.45] text-[var(--color-text)]", children: [
1805
+ /* @__PURE__ */ (0, import_jsx_runtime25.jsx)("span", { className: "gtv-info-panel-children-title", children: "Summary" }),
1806
+ /* @__PURE__ */ (0, import_jsx_runtime25.jsx)(ClampedText, { className: "mt-1", children: summary ?? "\u2014" }, genreUuid)
1807
+ ] }),
1808
+ family,
1809
+ renderGenreDetailExtras?.(overview),
1810
+ renderEssentialTracks ? renderEssentialTracks(overview) : essentialSongs && /* @__PURE__ */ (0, import_jsx_runtime25.jsxs)("div", { className: "gtv-info-panel-children text-[0.8125rem] leading-[1.45] text-[var(--color-text)]", children: [
1811
+ /* @__PURE__ */ (0, import_jsx_runtime25.jsxs)("span", { className: "gtv-info-panel-children-title", children: [
1812
+ "Essential ",
1813
+ trackNoun.plural
1814
+ ] }),
1815
+ essentialSongs.length > 0 ? /* @__PURE__ */ (0, import_jsx_runtime25.jsx)("ul", { className: "mt-1 divide-y divide-[var(--color-border)]", children: essentialSongs.map((track) => /* @__PURE__ */ (0, import_jsx_runtime25.jsx)("li", { className: "py-1", children: track.title }, track.uuid)) }) : /* @__PURE__ */ (0, import_jsx_runtime25.jsx)("p", { className: "mt-1", children: "\u2014" })
1816
+ ] }),
1817
+ /* @__PURE__ */ (0, import_jsx_runtime25.jsx)(
1818
+ GenrePlaylistTracks,
1819
+ {
1820
+ genreUuid,
1821
+ genrePlaylist,
1822
+ scope,
1823
+ getBackendBaseUrl,
1824
+ renderArtistLink,
1825
+ ...trackSlots
1826
+ }
1827
+ )
1828
+ ] });
1829
+ }
1830
+
1831
+ // src/genre-tree/GenreInfoPanelBody.tsx
1832
+ var import_jsx_runtime26 = require("react/jsx-runtime");
1833
+ var import_react11 = require("react");
1834
+ function GenreNodeActionBand({ genrePlaylist, playback, ...bandProps }) {
1835
+ const { isPlaying, setIsPlaying } = (0, import_player6.usePlayer)();
1836
+ const isCurrentTrackList = playback.playingNodeId === genrePlaylist.uuid;
1837
+ const handlePlayPause = () => {
1838
+ if (isCurrentTrackList) {
1839
+ setIsPlaying(!isPlaying);
1840
+ return;
1841
+ }
1842
+ playback.play(genrePlaylist).catch((error) => {
1843
+ console.error("Failed to load genre playlist tracks:", error);
1844
+ });
1709
1845
  };
1710
- return /* @__PURE__ */ (0, import_jsx_runtime24.jsxs)(
1711
- "button",
1846
+ return /* @__PURE__ */ (0, import_jsx_runtime26.jsx)(
1847
+ GenreActionBand,
1712
1848
  {
1713
- type: "button",
1714
- onClick: openSharePopup,
1715
- className: "group flex flex-col items-center gap-1 w-16 text-xs text-center text-[var(--color-text)] focus-visible:outline-none",
1716
- "aria-label": `Share ${target.name}`,
1717
- title: `Share ${target.name}`,
1718
- children: [
1719
- /* @__PURE__ */ (0, import_jsx_runtime24.jsx)("span", { className: "flex items-center justify-center w-10 h-10 rounded-full group-focus-visible:ring-2 group-focus-visible:ring-[var(--color-focus-ring)] bg-[var(--color-surface-muted)] transition-colors group-hover:bg-[var(--color-border)]", children: /* @__PURE__ */ (0, import_jsx_runtime24.jsx)(import_lucide_react10.Share2, { "aria-hidden": "true", className: "w-5 h-5" }) }),
1720
- "Share"
1721
- ]
1849
+ ...bandProps,
1850
+ onPlayPause: genrePlaylist.tracksCount > 0 ? handlePlayPause : void 0,
1851
+ isPlaying: isCurrentTrackList && isPlaying
1722
1852
  }
1723
1853
  );
1724
1854
  }
1725
-
1726
- // src/genre-tree/GenreActionBand.tsx
1727
- var import_jsx_runtime25 = require("react/jsx-runtime");
1728
- function GenreActionBand({
1729
- node,
1730
- onPlayPause,
1731
- isPlaying = false,
1732
- onZoomIn,
1733
- onAddChild,
1734
- onRename,
1735
- onChangeParent,
1736
- side,
1737
- onToggleSide,
1738
- toggleSideDisabledReason,
1739
- additionalActions,
1740
- shareTarget,
1741
- shareUrl
1855
+ function GenreInfoPanelBody({
1856
+ toolbar,
1857
+ family,
1858
+ genreUuid,
1859
+ genrePlaylist,
1860
+ wikidataId,
1861
+ scope,
1862
+ getBackendBaseUrl,
1863
+ playback,
1864
+ band,
1865
+ extras
1742
1866
  }) {
1743
- const items = [];
1744
- const adminItems = [];
1745
- if (onPlayPause) {
1746
- items.push({
1747
- key: "play-pause",
1748
- label: isPlaying ? "Pause" : "Play",
1749
- icon: isPlaying ? /* @__PURE__ */ (0, import_jsx_runtime25.jsx)(import_lucide_react11.Pause, { "aria-hidden": "true", className: "w-5 h-5" }) : /* @__PURE__ */ (0, import_jsx_runtime25.jsx)(import_lucide_react11.Play, { "aria-hidden": "true", className: "w-5 h-5" }),
1750
- onClick: onPlayPause
1751
- });
1752
- }
1753
- if (onZoomIn) {
1754
- items.push({
1755
- key: "zoom-in",
1756
- label: "Focus",
1757
- icon: /* @__PURE__ */ (0, import_jsx_runtime25.jsx)(import_lucide_react11.ZoomIn, { "aria-hidden": "true", className: "w-5 h-5" }),
1758
- onClick: onZoomIn
1759
- });
1760
- }
1761
- if (onAddChild) {
1762
- adminItems.push({ key: "add-child", label: "Add", icon: /* @__PURE__ */ (0, import_jsx_runtime25.jsx)(import_lucide_react11.Plus, { "aria-hidden": "true", className: "w-5 h-5" }), onClick: onAddChild });
1763
- }
1764
- if (onRename) {
1765
- adminItems.push({ key: "rename", label: "Rename", icon: /* @__PURE__ */ (0, import_jsx_runtime25.jsx)(import_lucide_react11.Pencil, { "aria-hidden": "true", className: "w-5 h-5" }), onClick: onRename });
1766
- }
1767
- if (onChangeParent) {
1768
- adminItems.push({ key: "change-parent", label: "Parent", icon: /* @__PURE__ */ (0, import_jsx_runtime25.jsx)(import_lucide_react11.MoveUpRight, { "aria-hidden": "true", className: "w-5 h-5" }), onClick: onChangeParent });
1769
- }
1770
- if (onToggleSide) {
1771
- adminItems.push({
1772
- key: "toggle-side",
1773
- label: side === "pop" ? "Make core" : "Make pop",
1774
- icon: /* @__PURE__ */ (0, import_jsx_runtime25.jsx)(import_lucide_react11.ArrowLeftRight, { "aria-hidden": "true", className: "w-5 h-5" }),
1775
- onClick: onToggleSide,
1776
- disabled: !!toggleSideDisabledReason,
1777
- title: toggleSideDisabledReason
1778
- });
1779
- }
1780
- for (const action of additionalActions?.(node) ?? []) {
1781
- items.push({
1782
- key: action.key,
1783
- label: action.label(node),
1784
- icon: action.icon(node),
1785
- onClick: (event) => action.onClick(event, node),
1786
- disabled: action.enabled ? !action.enabled(node) : false
1787
- });
1788
- }
1789
- if (items.length === 0 && (!shareTarget || !shareUrl) && adminItems.length === 0) return null;
1790
- const renderItems = (list) => list.map((item) => /* @__PURE__ */ (0, import_jsx_runtime25.jsxs)(
1791
- "button",
1792
- {
1793
- type: "button",
1794
- disabled: item.disabled,
1795
- title: item.title,
1796
- onClick: item.onClick,
1797
- className: "group flex flex-col items-center gap-1 w-16 text-xs text-center text-[var(--color-text)] focus-visible:outline-none disabled:opacity-50",
1798
- children: [
1799
- /* @__PURE__ */ (0, import_jsx_runtime25.jsx)("span", { className: "flex items-center justify-center w-10 h-10 rounded-full group-focus-visible:ring-2 group-focus-visible:ring-[var(--color-focus-ring)] bg-[var(--color-surface-muted)] transition-colors group-hover:bg-[var(--color-border)] group-disabled:group-hover:bg-[var(--color-surface-muted)]", children: item.icon }),
1800
- item.label
1801
- ]
1802
- },
1803
- item.key
1804
- ));
1805
- return /* @__PURE__ */ (0, import_jsx_runtime25.jsxs)("div", { className: "genre-action-band border-b border-[var(--color-border)]", children: [
1806
- (items.length > 0 || shareTarget && shareUrl) && /* @__PURE__ */ (0, import_jsx_runtime25.jsxs)("div", { className: "flex flex-wrap gap-1 px-3.5 py-3", children: [
1807
- renderItems(items),
1808
- shareTarget && shareUrl && /* @__PURE__ */ (0, import_jsx_runtime25.jsx)(ShareActionButton, { target: shareTarget, url: shareUrl })
1809
- ] }),
1810
- adminItems.length > 0 && /* @__PURE__ */ (0, import_jsx_runtime25.jsxs)(
1811
- "section",
1867
+ return /* @__PURE__ */ (0, import_jsx_runtime26.jsxs)(import_jsx_runtime26.Fragment, { children: [
1868
+ toolbar,
1869
+ /* @__PURE__ */ (0, import_jsx_runtime26.jsx)(GenreNodeActionBand, { ...band, genrePlaylist, playback }),
1870
+ /* @__PURE__ */ (0, import_react11.createElement)(
1871
+ GenreDetailExtras,
1812
1872
  {
1813
- "aria-label": "Admin actions",
1814
- className: `px-3.5 py-3 ${items.length > 0 ? "border-t border-[var(--color-border)]" : ""}`,
1815
- children: [
1816
- /* @__PURE__ */ (0, import_jsx_runtime25.jsx)("h3", { className: "mb-2 text-xs font-semibold uppercase tracking-wide text-[var(--color-text-muted)]", children: "Admin" }),
1817
- /* @__PURE__ */ (0, import_jsx_runtime25.jsx)("div", { className: "flex flex-wrap gap-1", children: renderItems(adminItems) })
1818
- ]
1873
+ ...extras,
1874
+ key: genreUuid,
1875
+ family,
1876
+ genreUuid,
1877
+ genrePlaylist,
1878
+ wikidataId,
1879
+ scope,
1880
+ getBackendBaseUrl
1819
1881
  }
1820
1882
  )
1821
1883
  ] });
@@ -1824,14 +1886,14 @@ function GenreActionBand({
1824
1886
  // src/genre-tree/PanelEdgeTab.tsx
1825
1887
  var import_lucide_react12 = require("lucide-react");
1826
1888
  var import_tailwind_merge = require("tailwind-merge");
1827
- var import_jsx_runtime26 = require("react/jsx-runtime");
1889
+ var import_jsx_runtime27 = require("react/jsx-runtime");
1828
1890
  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)]";
1829
1891
  function PanelEdgeTab({ side, edge, center, controls, className: className2 }) {
1830
1892
  if (!edge && !center) return null;
1831
1893
  const isLeft = side === "left";
1832
1894
  const EdgeIcon = isLeft ? import_lucide_react12.ChevronLeft : import_lucide_react12.ChevronRight;
1833
1895
  const CenterIcon = isLeft ? import_lucide_react12.ChevronRight : import_lucide_react12.ChevronLeft;
1834
- const renderButton = (action, icon) => /* @__PURE__ */ (0, import_jsx_runtime26.jsx)(
1896
+ const renderButton = (action, icon) => /* @__PURE__ */ (0, import_jsx_runtime27.jsx)(
1835
1897
  "button",
1836
1898
  {
1837
1899
  type: "button",
@@ -1844,10 +1906,10 @@ function PanelEdgeTab({ side, edge, center, controls, className: className2 }) {
1844
1906
  children: icon
1845
1907
  }
1846
1908
  );
1847
- const edgeButton = edge && renderButton(edge, /* @__PURE__ */ (0, import_jsx_runtime26.jsx)(EdgeIcon, { "aria-hidden": "true", className: "h-5 w-5" }));
1848
- const centerButton = center && renderButton(center, /* @__PURE__ */ (0, import_jsx_runtime26.jsx)(CenterIcon, { "aria-hidden": "true", className: "h-5 w-5" }));
1909
+ const edgeButton = edge && renderButton(edge, /* @__PURE__ */ (0, import_jsx_runtime27.jsx)(EdgeIcon, { "aria-hidden": "true", className: "h-5 w-5" }));
1910
+ const centerButton = center && renderButton(center, /* @__PURE__ */ (0, import_jsx_runtime27.jsx)(CenterIcon, { "aria-hidden": "true", className: "h-5 w-5" }));
1849
1911
  const buttons = isLeft ? [edgeButton, centerButton] : [centerButton, edgeButton];
1850
- return /* @__PURE__ */ (0, import_jsx_runtime26.jsxs)(
1912
+ return /* @__PURE__ */ (0, import_jsx_runtime27.jsxs)(
1851
1913
  "div",
1852
1914
  {
1853
1915
  className: (0, import_tailwind_merge.twMerge)(
@@ -1857,17 +1919,30 @@ function PanelEdgeTab({ side, edge, center, controls, className: className2 }) {
1857
1919
  ),
1858
1920
  children: [
1859
1921
  buttons[0],
1860
- buttons[0] && buttons[1] && /* @__PURE__ */ (0, import_jsx_runtime26.jsx)("div", { role: "presentation", className: "h-px shrink-0 bg-[var(--color-border)]" }),
1922
+ buttons[0] && buttons[1] && /* @__PURE__ */ (0, import_jsx_runtime27.jsx)("div", { role: "presentation", className: "h-px shrink-0 bg-[var(--color-border)]" }),
1861
1923
  buttons[1]
1862
1924
  ]
1863
1925
  }
1864
1926
  );
1865
1927
  }
1866
1928
 
1929
+ // src/genre-tree/useLeftDetailPanelLayout.ts
1930
+ var import_react12 = require("react");
1931
+ function useLeftDetailPanelLayout(isPanelShown, enabled = true) {
1932
+ (0, import_react12.useEffect)(() => {
1933
+ if (!enabled || isPanelShown) return void 0;
1934
+ const root = document.documentElement;
1935
+ root.style.setProperty("--layout-left-w", "0px");
1936
+ return () => {
1937
+ root.style.removeProperty("--layout-left-w");
1938
+ };
1939
+ }, [enabled, isPanelShown]);
1940
+ }
1941
+
1867
1942
  // src/genre-tree/ChangeParentPopup.tsx
1868
- var import_react11 = require("react");
1943
+ var import_react13 = require("react");
1869
1944
  var import_genre_tree10 = require("@behindthemusictree/app-kit/genre-tree");
1870
- var import_jsx_runtime27 = require("react/jsx-runtime");
1945
+ var import_jsx_runtime28 = require("react/jsx-runtime");
1871
1946
  function ChangeParentPopup({
1872
1947
  genrePlaylist,
1873
1948
  genrePlaylists,
@@ -1877,7 +1952,7 @@ function ChangeParentPopup({
1877
1952
  onParentChanged
1878
1953
  }) {
1879
1954
  const { mutate, isPending, formErrors } = (0, import_genre_tree10.useUpdateGenre)(scope, getBackendBaseUrl);
1880
- const candidates = (0, import_react11.useMemo)(
1955
+ const candidates = (0, import_react13.useMemo)(
1881
1956
  () => (0, import_genre_tree10.getReparentCandidates)(genrePlaylists, genrePlaylist.uuid),
1882
1957
  [genrePlaylists, genrePlaylist.uuid]
1883
1958
  );
@@ -1895,19 +1970,19 @@ function ChangeParentPopup({
1895
1970
  }
1896
1971
  );
1897
1972
  };
1898
- (0, import_react11.useEffect)(() => {
1973
+ (0, import_react13.useEffect)(() => {
1899
1974
  const handleKeyDown = (event) => {
1900
1975
  if (event.key === "Escape") onClose();
1901
1976
  };
1902
1977
  window.addEventListener("keydown", handleKeyDown);
1903
1978
  return () => window.removeEventListener("keydown", handleKeyDown);
1904
1979
  }, [onClose]);
1905
- return /* @__PURE__ */ (0, import_jsx_runtime27.jsxs)(BasePopup_default, { title: `Change parent of ${genrePlaylist.name}`, onClose, showCancelButton: true, fitContent: true, children: [
1906
- /* @__PURE__ */ (0, import_jsx_runtime27.jsxs)("p", { className: "mb-3 text-sm text-[var(--color-text-muted)]", children: [
1980
+ return /* @__PURE__ */ (0, import_jsx_runtime28.jsxs)(BasePopup_default, { title: `Change parent of ${genrePlaylist.name}`, onClose, showCancelButton: true, fitContent: true, children: [
1981
+ /* @__PURE__ */ (0, import_jsx_runtime28.jsxs)("p", { className: "mb-3 text-sm text-[var(--color-text-muted)]", children: [
1907
1982
  "Current parent: ",
1908
1983
  genrePlaylist.parent?.name ?? "none"
1909
1984
  ] }),
1910
- /* @__PURE__ */ (0, import_jsx_runtime27.jsx)("div", { "aria-busy": isPending, className: isPending ? "pointer-events-none opacity-60" : void 0, children: /* @__PURE__ */ (0, import_jsx_runtime27.jsx)(
1985
+ /* @__PURE__ */ (0, import_jsx_runtime28.jsx)("div", { "aria-busy": isPending, className: isPending ? "pointer-events-none opacity-60" : void 0, children: /* @__PURE__ */ (0, import_jsx_runtime28.jsx)(
1911
1986
  GenreSearch,
1912
1987
  {
1913
1988
  genrePlaylists: candidates,
@@ -1915,7 +1990,7 @@ function ChangeParentPopup({
1915
1990
  placeholder: "Search a new parent\u2026"
1916
1991
  }
1917
1992
  ) }),
1918
- formErrors.length > 0 && /* @__PURE__ */ (0, import_jsx_runtime27.jsx)("p", { role: "alert", className: "mt-3 text-sm text-[var(--color-red-500)]", children: formErrors.map(({ message }) => message).join(" ") })
1993
+ formErrors.length > 0 && /* @__PURE__ */ (0, import_jsx_runtime28.jsx)("p", { role: "alert", className: "mt-3 text-sm text-[var(--color-red-500)]", children: formErrors.map(({ message }) => message).join(" ") })
1919
1994
  ] });
1920
1995
  }
1921
1996
 
@@ -1930,7 +2005,7 @@ var import_ui10 = require("@behindthemusictree/ui");
1930
2005
  var import_lucide_react13 = require("lucide-react");
1931
2006
  var import_si3 = require("react-icons/si");
1932
2007
  var import_ui9 = require("@behindthemusictree/ui");
1933
- var import_jsx_runtime28 = require("react/jsx-runtime");
2008
+ var import_jsx_runtime29 = require("react/jsx-runtime");
1934
2009
  var LINK_KINDS = {
1935
2010
  official: { label: "Official website", Icon: import_lucide_react13.Globe },
1936
2011
  wikipedia: { label: "Wikipedia", Icon: import_si3.SiWikipedia },
@@ -1967,7 +2042,7 @@ function artistLinks(artist) {
1967
2042
  return links;
1968
2043
  }
1969
2044
  function ArtistLinks({ links }) {
1970
- return /* @__PURE__ */ (0, import_jsx_runtime28.jsx)("ul", { className: "flex flex-wrap gap-1.5", children: links.map(({ key, url, Icon, label }) => /* @__PURE__ */ (0, import_jsx_runtime28.jsx)("li", { children: /* @__PURE__ */ (0, import_jsx_runtime28.jsx)(import_ui9.Chip, { href: url, icon: /* @__PURE__ */ (0, import_jsx_runtime28.jsx)(Icon, { className: "h-3.5 w-3.5 shrink-0", "aria-hidden": "true" }), children: label }) }, key)) });
2045
+ return /* @__PURE__ */ (0, import_jsx_runtime29.jsx)("ul", { className: "flex flex-wrap gap-1.5", children: links.map(({ key, url, Icon, label }) => /* @__PURE__ */ (0, import_jsx_runtime29.jsx)("li", { children: /* @__PURE__ */ (0, import_jsx_runtime29.jsx)(import_ui9.Chip, { href: url, icon: /* @__PURE__ */ (0, import_jsx_runtime29.jsx)(Icon, { className: "h-3.5 w-3.5 shrink-0", "aria-hidden": "true" }), children: label }) }, key)) });
1971
2046
  }
1972
2047
 
1973
2048
  // src/genre-tree/artist/formatYears.ts
@@ -1978,7 +2053,7 @@ function formatYears(beginYear, endYear, ended) {
1978
2053
  }
1979
2054
 
1980
2055
  // src/genre-tree/artist/ArtistHeader.tsx
1981
- var import_jsx_runtime29 = require("react/jsx-runtime");
2056
+ var import_jsx_runtime30 = require("react/jsx-runtime");
1982
2057
  var countryNames = new Intl.DisplayNames(["en"], { type: "region" });
1983
2058
  function ArtistHeader({ artist }) {
1984
2059
  const fields = [];
@@ -1988,35 +2063,35 @@ function ArtistHeader({ artist }) {
1988
2063
  const years = formatYears(artist.beginYear, artist.endYear, artist.ended);
1989
2064
  if (years) fields.push({ label: "Active", value: years });
1990
2065
  const links = artistLinks(artist);
1991
- if (links.length > 0) fields.push({ label: "Links", value: /* @__PURE__ */ (0, import_jsx_runtime29.jsx)(ArtistLinks, { links }) });
2066
+ if (links.length > 0) fields.push({ label: "Links", value: /* @__PURE__ */ (0, import_jsx_runtime30.jsx)(ArtistLinks, { links }) });
1992
2067
  if (fields.length === 0) return null;
1993
- return /* @__PURE__ */ (0, import_jsx_runtime29.jsx)(import_ui10.FieldList, { items: fields });
2068
+ return /* @__PURE__ */ (0, import_jsx_runtime30.jsx)(import_ui10.FieldList, { items: fields });
1994
2069
  }
1995
2070
 
1996
2071
  // src/genre-tree/artist/ArtistPhoto.tsx
1997
- var import_jsx_runtime30 = require("react/jsx-runtime");
2072
+ var import_jsx_runtime31 = require("react/jsx-runtime");
1998
2073
  function ExternalLink({ href, children }) {
1999
- return /* @__PURE__ */ (0, import_jsx_runtime30.jsx)("a", { href, target: "_blank", rel: "noopener noreferrer", className: "underline-offset-2 hover:underline", children });
2074
+ return /* @__PURE__ */ (0, import_jsx_runtime31.jsx)("a", { href, target: "_blank", rel: "noopener noreferrer", className: "underline-offset-2 hover:underline", children });
2000
2075
  }
2001
2076
  function ArtistPhoto({ image, name }) {
2002
- return /* @__PURE__ */ (0, import_jsx_runtime30.jsxs)("figure", { className: "flex flex-col gap-1.5 px-6 py-2.5", children: [
2003
- /* @__PURE__ */ (0, import_jsx_runtime30.jsx)("img", { src: image.url, alt: name, className: "h-64 w-full object-contain bg-[var(--color-surface-muted)] rounded" }),
2004
- /* @__PURE__ */ (0, import_jsx_runtime30.jsxs)("figcaption", { className: "text-xs text-[var(--color-text-muted)]", children: [
2077
+ return /* @__PURE__ */ (0, import_jsx_runtime31.jsxs)("figure", { className: "flex flex-col gap-1.5 px-6 py-2.5", children: [
2078
+ /* @__PURE__ */ (0, import_jsx_runtime31.jsx)("img", { src: image.url, alt: name, className: "h-64 w-full object-contain bg-[var(--color-surface-muted)] rounded" }),
2079
+ /* @__PURE__ */ (0, import_jsx_runtime31.jsxs)("figcaption", { className: "text-xs text-[var(--color-text-muted)]", children: [
2005
2080
  "Photo: ",
2006
- /* @__PURE__ */ (0, import_jsx_runtime30.jsx)(ExternalLink, { href: image.pageUrl, children: image.author ?? "Wikimedia Commons" }),
2081
+ /* @__PURE__ */ (0, import_jsx_runtime31.jsx)(ExternalLink, { href: image.pageUrl, children: image.author ?? "Wikimedia Commons" }),
2007
2082
  ",",
2008
2083
  " ",
2009
- image.licenseUrl ? /* @__PURE__ */ (0, import_jsx_runtime30.jsx)(ExternalLink, { href: image.licenseUrl, children: image.license }) : image.license
2084
+ image.licenseUrl ? /* @__PURE__ */ (0, import_jsx_runtime31.jsx)(ExternalLink, { href: image.licenseUrl, children: image.license }) : image.license
2010
2085
  ] })
2011
2086
  ] });
2012
2087
  }
2013
2088
 
2014
2089
  // src/genre-tree/artist/ArtistMembers.tsx
2015
2090
  var import_ui11 = require("@behindthemusictree/ui");
2016
- var import_jsx_runtime31 = require("react/jsx-runtime");
2091
+ var import_jsx_runtime32 = require("react/jsx-runtime");
2017
2092
  function MembershipList({ title, memberships, renderArtistLink }) {
2018
2093
  if (memberships.length === 0) return null;
2019
- return /* @__PURE__ */ (0, import_jsx_runtime31.jsx)(import_ui11.PanelGroup, { title, children: /* @__PURE__ */ (0, import_jsx_runtime31.jsx)("ul", { children: memberships.map((membership, index) => {
2094
+ return /* @__PURE__ */ (0, import_jsx_runtime32.jsx)(import_ui11.PanelGroup, { title, children: /* @__PURE__ */ (0, import_jsx_runtime32.jsx)("ul", { children: memberships.map((membership, index) => {
2020
2095
  const details = [
2021
2096
  formatYears(membership.beginYear, membership.endYear, membership.ended),
2022
2097
  ...membership.roles
@@ -2024,9 +2099,9 @@ function MembershipList({ title, memberships, renderArtistLink }) {
2024
2099
  const detailsText = details.filter(Boolean).join(" \xB7 ");
2025
2100
  return (
2026
2101
  // A member can have several periods in the same group.
2027
- /* @__PURE__ */ (0, import_jsx_runtime31.jsxs)("li", { className: "truncate py-0.5", title: detailsText || void 0, children: [
2028
- /* @__PURE__ */ (0, import_jsx_runtime31.jsx)("span", { className: "font-medium text-[var(--color-text)]", children: renderArtistLink(membership.artist) }),
2029
- detailsText && /* @__PURE__ */ (0, import_jsx_runtime31.jsxs)("span", { className: "text-[var(--color-text-muted)]", children: [
2102
+ /* @__PURE__ */ (0, import_jsx_runtime32.jsxs)("li", { className: "truncate py-0.5", title: detailsText || void 0, children: [
2103
+ /* @__PURE__ */ (0, import_jsx_runtime32.jsx)("span", { className: "font-medium text-[var(--color-text)]", children: renderArtistLink(membership.artist) }),
2104
+ detailsText && /* @__PURE__ */ (0, import_jsx_runtime32.jsxs)("span", { className: "text-[var(--color-text-muted)]", children: [
2030
2105
  " \u2014 ",
2031
2106
  detailsText
2032
2107
  ] })
@@ -2037,9 +2112,9 @@ function MembershipList({ title, memberships, renderArtistLink }) {
2037
2112
  function ArtistMembers({ title, memberships, renderArtistLink }) {
2038
2113
  if (memberships.length === 0) return null;
2039
2114
  const isCurrent = (membership) => !membership.ended && membership.endYear === null;
2040
- return /* @__PURE__ */ (0, import_jsx_runtime31.jsxs)(import_ui11.PanelSection, { title, children: [
2041
- /* @__PURE__ */ (0, import_jsx_runtime31.jsx)(MembershipList, { title: "Current", memberships: memberships.filter(isCurrent), renderArtistLink }),
2042
- /* @__PURE__ */ (0, import_jsx_runtime31.jsx)(
2115
+ return /* @__PURE__ */ (0, import_jsx_runtime32.jsxs)(import_ui11.PanelSection, { title, children: [
2116
+ /* @__PURE__ */ (0, import_jsx_runtime32.jsx)(MembershipList, { title: "Current", memberships: memberships.filter(isCurrent), renderArtistLink }),
2117
+ /* @__PURE__ */ (0, import_jsx_runtime32.jsx)(
2043
2118
  MembershipList,
2044
2119
  {
2045
2120
  title: "Past",
@@ -2052,7 +2127,7 @@ function ArtistMembers({ title, memberships, renderArtistLink }) {
2052
2127
 
2053
2128
  // src/genre-tree/artist/ArtistDiscography.tsx
2054
2129
  var import_ui12 = require("@behindthemusictree/ui");
2055
- var import_jsx_runtime32 = require("react/jsx-runtime");
2130
+ var import_jsx_runtime33 = require("react/jsx-runtime");
2056
2131
  var GROUPS = ["Albums", "EPs", "Singles", "Live", "Compilations", "Other"];
2057
2132
  var PRIMARY_GROUPS = { album: "Albums", ep: "EPs", single: "Singles" };
2058
2133
  function discographyGroup({ primaryType, secondaryTypes }) {
@@ -2072,55 +2147,55 @@ function byYear(a, b) {
2072
2147
  function ArtistDiscography({ albums }) {
2073
2148
  if (albums.length === 0) return null;
2074
2149
  const sorted = [...albums].sort(byYear);
2075
- return /* @__PURE__ */ (0, import_jsx_runtime32.jsx)(import_ui12.PanelSection, { title: "Discography", children: GROUPS.map((group) => {
2150
+ return /* @__PURE__ */ (0, import_jsx_runtime33.jsx)(import_ui12.PanelSection, { title: "Discography", children: GROUPS.map((group) => {
2076
2151
  const groupAlbums = sorted.filter((album) => discographyGroup(album) === group);
2077
2152
  if (groupAlbums.length === 0) return null;
2078
- return /* @__PURE__ */ (0, import_jsx_runtime32.jsx)(import_ui12.PanelGroup, { title: group, children: /* @__PURE__ */ (0, import_jsx_runtime32.jsx)("ul", { children: groupAlbums.map((album) => /* @__PURE__ */ (0, import_jsx_runtime32.jsxs)("li", { className: "grid grid-cols-[2.5rem_minmax(0,1fr)] gap-2 py-0.5", children: [
2079
- /* @__PURE__ */ (0, import_jsx_runtime32.jsx)("span", { className: "tabular-nums text-[var(--color-text-muted)]", children: album.year ?? "\u2014" }),
2080
- /* @__PURE__ */ (0, import_jsx_runtime32.jsx)("span", { className: "truncate text-[var(--color-text)]", title: album.name, children: album.name })
2153
+ return /* @__PURE__ */ (0, import_jsx_runtime33.jsx)(import_ui12.PanelGroup, { title: group, children: /* @__PURE__ */ (0, import_jsx_runtime33.jsx)("ul", { children: groupAlbums.map((album) => /* @__PURE__ */ (0, import_jsx_runtime33.jsxs)("li", { className: "grid grid-cols-[2.5rem_minmax(0,1fr)] gap-2 py-0.5", children: [
2154
+ /* @__PURE__ */ (0, import_jsx_runtime33.jsx)("span", { className: "tabular-nums text-[var(--color-text-muted)]", children: album.year ?? "\u2014" }),
2155
+ /* @__PURE__ */ (0, import_jsx_runtime33.jsx)("span", { className: "truncate text-[var(--color-text)]", title: album.name, children: album.name })
2081
2156
  ] }, album.uuid)) }) }, group);
2082
2157
  }) });
2083
2158
  }
2084
2159
 
2085
2160
  // src/genre-tree/artist/ArtistSongs.tsx
2086
- var import_react12 = require("react");
2161
+ var import_react14 = require("react");
2087
2162
  var import_react_query2 = require("@tanstack/react-query");
2088
2163
  var import_lucide_react14 = require("lucide-react");
2089
2164
  var import_ui13 = require("@behindthemusictree/ui");
2090
- var import_player9 = require("@behindthemusictree/app-kit/player");
2165
+ var import_player10 = require("@behindthemusictree/app-kit/player");
2091
2166
  var import_genre_tree14 = require("@behindthemusictree/app-kit/genre-tree");
2092
2167
  var import_transport8 = require("@behindthemusictree/app-kit/transport");
2093
2168
 
2094
2169
  // src/genre-tree/track-list-sidebar/TrackItem.tsx
2095
- var import_player8 = require("@behindthemusictree/app-kit/player");
2170
+ var import_player9 = require("@behindthemusictree/app-kit/player");
2096
2171
  var import_genre_tree11 = require("@behindthemusictree/app-kit/genre-tree");
2097
2172
 
2098
2173
  // src/genre-tree/TrackPositionPlayPause.tsx
2099
2174
  var import_fa5 = require("react-icons/fa");
2100
- var import_player6 = require("@behindthemusictree/app-kit/player");
2101
2175
  var import_player7 = require("@behindthemusictree/app-kit/player");
2102
- var import_jsx_runtime33 = require("react/jsx-runtime");
2176
+ var import_player8 = require("@behindthemusictree/app-kit/player");
2177
+ var import_jsx_runtime34 = require("react/jsx-runtime");
2103
2178
  function TrackPositionPlayPause({
2104
2179
  position,
2105
2180
  uuid,
2106
2181
  handlePlayPauseClick,
2107
2182
  isSelected = false
2108
2183
  }) {
2109
- const { playerTrackObject, playbackConfirmedTrackId, playState, isLoading: isPlayerLoading } = (0, import_player6.usePlayer)();
2184
+ const { playerTrackObject, playbackConfirmedTrackId, playState, isLoading: isPlayerLoading } = (0, import_player7.usePlayer)();
2110
2185
  const isThisTrack = playerTrackObject?.track.id === uuid;
2111
2186
  const isConfirmedPlayable = playbackConfirmedTrackId === uuid;
2112
- const isAwaitingPlayback = (isSelected || isThisTrack) && !isConfirmedPlayable && !(isThisTrack && playerTrackObject?.loadError) && (!isThisTrack || isPlayerLoading || playState === import_player7.PlayStates.LOADING || playState === import_player7.PlayStates.PLAYING);
2113
- const isPlaying = isThisTrack && isConfirmedPlayable && playState === import_player7.PlayStates.PLAYING;
2114
- const isPaused = isThisTrack && isConfirmedPlayable && playState === import_player7.PlayStates.PAUSED;
2187
+ const isAwaitingPlayback = (isSelected || isThisTrack) && !isConfirmedPlayable && !(isThisTrack && playerTrackObject?.loadError) && (!isThisTrack || isPlayerLoading || playState === import_player8.PlayStates.LOADING || playState === import_player8.PlayStates.PLAYING);
2188
+ const isPlaying = isThisTrack && isConfirmedPlayable && playState === import_player8.PlayStates.PLAYING;
2189
+ const isPaused = isThisTrack && isConfirmedPlayable && playState === import_player8.PlayStates.PAUSED;
2115
2190
  const isCurrentTrackActive = isPlaying || isPaused;
2116
- return /* @__PURE__ */ (0, import_jsx_runtime33.jsxs)(
2191
+ return /* @__PURE__ */ (0, import_jsx_runtime34.jsxs)(
2117
2192
  "div",
2118
2193
  {
2119
2194
  className: "track-position-play-pause relative flex items-center justify-center gap-2 text-lg w-16 cursor-pointer",
2120
2195
  style: { minWidth: "4rem", maxWidth: "4rem" },
2121
2196
  onClick: handlePlayPauseClick,
2122
2197
  children: [
2123
- /* @__PURE__ */ (0, import_jsx_runtime33.jsx)("div", { className: "track-position-play-pause__status group-hover:hidden flex items-center justify-center", children: isAwaitingPlayback ? /* @__PURE__ */ (0, import_jsx_runtime33.jsx)("span", { className: "track-position-play-pause__spinner", role: "status", "aria-label": "Loading track" }) : isCurrentTrackActive ? /* @__PURE__ */ (0, import_jsx_runtime33.jsxs)(
2198
+ /* @__PURE__ */ (0, import_jsx_runtime34.jsx)("div", { className: "track-position-play-pause__status group-hover:hidden flex items-center justify-center", children: isAwaitingPlayback ? /* @__PURE__ */ (0, import_jsx_runtime34.jsx)("span", { className: "track-position-play-pause__spinner", role: "status", "aria-label": "Loading track" }) : isCurrentTrackActive ? /* @__PURE__ */ (0, import_jsx_runtime34.jsxs)(
2124
2199
  "span",
2125
2200
  {
2126
2201
  className: "track-position-play-pause__bars",
@@ -2128,18 +2203,18 @@ function TrackPositionPlayPause({
2128
2203
  role: "img",
2129
2204
  "aria-label": isPlaying ? "Playing" : "Paused",
2130
2205
  children: [
2131
- /* @__PURE__ */ (0, import_jsx_runtime33.jsx)("span", { className: "track-position-play-pause__bar" }),
2132
- /* @__PURE__ */ (0, import_jsx_runtime33.jsx)("span", { className: "track-position-play-pause__bar" }),
2133
- /* @__PURE__ */ (0, import_jsx_runtime33.jsx)("span", { className: "track-position-play-pause__bar" })
2206
+ /* @__PURE__ */ (0, import_jsx_runtime34.jsx)("span", { className: "track-position-play-pause__bar" }),
2207
+ /* @__PURE__ */ (0, import_jsx_runtime34.jsx)("span", { className: "track-position-play-pause__bar" }),
2208
+ /* @__PURE__ */ (0, import_jsx_runtime34.jsx)("span", { className: "track-position-play-pause__bar" })
2134
2209
  ]
2135
2210
  }
2136
2211
  ) : position }),
2137
- /* @__PURE__ */ (0, import_jsx_runtime33.jsx)(
2212
+ /* @__PURE__ */ (0, import_jsx_runtime34.jsx)(
2138
2213
  "div",
2139
2214
  {
2140
2215
  className: `hidden group-hover:flex items-center justify-center ${isCurrentTrackActive ? "" : "absolute inset-0"}`,
2141
2216
  "aria-hidden": "true",
2142
- children: isAwaitingPlayback ? /* @__PURE__ */ (0, import_jsx_runtime33.jsx)("span", { className: "track-position-play-pause__spinner" }) : isPlaying ? /* @__PURE__ */ (0, import_jsx_runtime33.jsx)(import_fa5.FaPause, {}) : /* @__PURE__ */ (0, import_jsx_runtime33.jsx)(import_fa5.FaPlay, {})
2217
+ children: isAwaitingPlayback ? /* @__PURE__ */ (0, import_jsx_runtime34.jsx)("span", { className: "track-position-play-pause__spinner" }) : isPlaying ? /* @__PURE__ */ (0, import_jsx_runtime34.jsx)(import_fa5.FaPause, {}) : /* @__PURE__ */ (0, import_jsx_runtime34.jsx)(import_fa5.FaPlay, {})
2143
2218
  }
2144
2219
  )
2145
2220
  ]
@@ -2150,7 +2225,7 @@ function TrackPositionPlayPause({
2150
2225
  // src/genre-tree/track-list-sidebar/TrackItem.tsx
2151
2226
  var import_genre_tree12 = require("@behindthemusictree/app-kit/genre-tree");
2152
2227
  var import_genre_tree13 = require("@behindthemusictree/app-kit/genre-tree");
2153
- var import_jsx_runtime34 = require("react/jsx-runtime");
2228
+ var import_jsx_runtime35 = require("react/jsx-runtime");
2154
2229
  function TrackItem({
2155
2230
  track,
2156
2231
  position,
@@ -2158,7 +2233,7 @@ function TrackItem({
2158
2233
  renderActions,
2159
2234
  renderArtistLink
2160
2235
  }) {
2161
- const { handlePlayPauseAction, playerTrackObject, loadTrackForPlayer } = (0, import_player8.usePlayer)();
2236
+ const { handlePlayPauseAction, playerTrackObject, loadTrackForPlayer } = (0, import_player9.usePlayer)();
2162
2237
  const { trackList, selectedTrack, toTrackAtPosition, runtimeUnplayableTrackIds } = (0, import_genre_tree11.useTrackList)();
2163
2238
  const scope = trackList?.origin?.scope ?? null;
2164
2239
  const isCurrentTrack = playerTrackObject?.track.id === track.uuid;
@@ -2173,12 +2248,12 @@ function TrackItem({
2173
2248
  loadTrackForPlayer(track.uuid);
2174
2249
  }
2175
2250
  };
2176
- return /* @__PURE__ */ (0, import_jsx_runtime34.jsxs)(
2251
+ return /* @__PURE__ */ (0, import_jsx_runtime35.jsxs)(
2177
2252
  "div",
2178
2253
  {
2179
2254
  className: `track-item flex h-14 text-[var(--color-text)] ${isUnplayable ? "opacity-50" : "hover:bg-[var(--color-surface-muted)] group"}`,
2180
2255
  children: [
2181
- isUnplayable ? /* @__PURE__ */ (0, import_jsx_runtime34.jsx)(
2256
+ isUnplayable ? /* @__PURE__ */ (0, import_jsx_runtime35.jsx)(
2182
2257
  "div",
2183
2258
  {
2184
2259
  className: "flex items-center justify-center text-lg w-16",
@@ -2186,7 +2261,7 @@ function TrackItem({
2186
2261
  "aria-disabled": "true",
2187
2262
  children: position
2188
2263
  }
2189
- ) : /* @__PURE__ */ (0, import_jsx_runtime34.jsx)(
2264
+ ) : /* @__PURE__ */ (0, import_jsx_runtime35.jsx)(
2190
2265
  TrackPositionPlayPause,
2191
2266
  {
2192
2267
  position,
@@ -2195,12 +2270,12 @@ function TrackItem({
2195
2270
  isSelected: selectedTrack?.uuid === track.uuid
2196
2271
  }
2197
2272
  ),
2198
- /* @__PURE__ */ (0, import_jsx_runtime34.jsxs)("div", { className: "title-artist-container flex flex-col items-start justify-center min-w-0 flex-1", children: [
2199
- /* @__PURE__ */ (0, import_jsx_runtime34.jsx)("div", { className: "flex max-w-full items-center gap-2", children: /* @__PURE__ */ (0, import_jsx_runtime34.jsx)("div", { className: "title text-lg font-bold text-[var(--color-text)] max-w-full truncate", children: (0, import_genre_tree12.isSong)(track) ? /* @__PURE__ */ (0, import_jsx_runtime34.jsx)(MusicBrainzRecordingLink, { song: track, className: "hover:underline focus-visible:underline", children: track.title }) : track.title }) }),
2200
- track.artists && track.artists.length > 0 ? /* @__PURE__ */ (0, import_jsx_runtime34.jsx)("div", { className: "artist text-base max-w-full truncate text-[var(--color-text-muted)]", children: /* @__PURE__ */ (0, import_jsx_runtime34.jsx)(import_genre_tree13.ArtistNames, { artists: track.artists, renderArtistLink }) }) : ""
2273
+ /* @__PURE__ */ (0, import_jsx_runtime35.jsxs)("div", { className: "title-artist-container flex flex-col items-start justify-center min-w-0 flex-1", children: [
2274
+ /* @__PURE__ */ (0, import_jsx_runtime35.jsx)("div", { className: "flex max-w-full items-center gap-2", children: /* @__PURE__ */ (0, import_jsx_runtime35.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_runtime35.jsx)(MusicBrainzRecordingLink, { song: track, className: "hover:underline focus-visible:underline", children: track.title }) : track.title }) }),
2275
+ track.artists && track.artists.length > 0 ? /* @__PURE__ */ (0, import_jsx_runtime35.jsx)("div", { className: "artist text-base max-w-full truncate text-[var(--color-text-muted)]", children: /* @__PURE__ */ (0, import_jsx_runtime35.jsx)(import_genre_tree13.ArtistNames, { artists: track.artists, renderArtistLink }) }) : ""
2201
2276
  ] }),
2202
- /* @__PURE__ */ (0, import_jsx_runtime34.jsx)("div", { className: "genre-name-container flex items-center justify-start w-[21.6%] min-w-0 shrink-0", children: track.genre ? /* @__PURE__ */ (0, import_jsx_runtime34.jsx)("div", { className: "genre-name max-w-full whitespace-normal break-words p-1 text-xs font-bold", children: track.genre.name }) : "" }),
2203
- renderDuration && /* @__PURE__ */ (0, import_jsx_runtime34.jsx)(
2277
+ /* @__PURE__ */ (0, import_jsx_runtime35.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_runtime35.jsx)("div", { className: "genre-name max-w-full whitespace-normal break-words p-1 text-xs font-bold", children: track.genre.name }) : "" }),
2278
+ renderDuration && /* @__PURE__ */ (0, import_jsx_runtime35.jsx)(
2204
2279
  "div",
2205
2280
  {
2206
2281
  className: "duration flex text-base w-16 items-center justify-center",
@@ -2208,25 +2283,25 @@ function TrackItem({
2208
2283
  children: renderDuration(track)
2209
2284
  }
2210
2285
  ),
2211
- renderActions && /* @__PURE__ */ (0, import_jsx_runtime34.jsx)("div", { className: "edit flex text-base w-6 items-center justify-center mr-2 cursor-pointer", children: renderActions(track) })
2286
+ renderActions && /* @__PURE__ */ (0, import_jsx_runtime35.jsx)("div", { className: "edit flex text-base w-6 items-center justify-center mr-2 cursor-pointer", children: renderActions(track) })
2212
2287
  ]
2213
2288
  }
2214
2289
  );
2215
2290
  }
2216
2291
 
2217
2292
  // src/genre-tree/artist/ArtistSongs.tsx
2218
- var import_jsx_runtime35 = require("react/jsx-runtime");
2293
+ var import_jsx_runtime36 = require("react/jsx-runtime");
2219
2294
  var SongPageSchema2 = (0, import_transport8.PaginatedResponseSchema)(import_genre_tree14.SongDetailedSchema);
2220
2295
  var SORTS = [
2221
2296
  { ordering: "title", label: "Title" },
2222
2297
  { ordering: "-albumYear", label: "Year" }
2223
2298
  ];
2224
2299
  function ArtistSongs({ artist, getBackendBaseUrl, renderArtistLink }) {
2225
- const [page, setPage] = (0, import_react12.useState)(1);
2226
- const [ordering, setOrdering] = (0, import_react12.useState)("title");
2300
+ const [page, setPage] = (0, import_react14.useState)(1);
2301
+ const [ordering, setOrdering] = (0, import_react14.useState)("title");
2227
2302
  const { fetch } = (0, import_transport8.useFetchWrapper)(getBackendBaseUrl);
2228
2303
  const { playNewTrackListFromArtist } = (0, import_genre_tree14.useTrackList)();
2229
- const { playerTrackObject } = (0, import_player9.usePlayer)();
2304
+ const { playerTrackObject } = (0, import_player10.usePlayer)();
2230
2305
  const { data, isPending, isError } = (0, import_transport8.useQueryWithParse)({
2231
2306
  queryKey: ["referenceSongs", "artist", artist.uuid, ordering, page],
2232
2307
  queryFn: () => fetch(import_genre_tree14.libraryEndpoints.reference.songs.list(), true, false, void 0, {
@@ -2248,21 +2323,21 @@ function ArtistSongs({ artist, getBackendBaseUrl, renderArtistLink }) {
2248
2323
  event.stopPropagation();
2249
2324
  void play(song.uuid);
2250
2325
  };
2251
- return /* @__PURE__ */ (0, import_jsx_runtime35.jsx)(import_ui13.PanelSection, { title: "Songs", children: isPending ? /* @__PURE__ */ (0, import_jsx_runtime35.jsx)(import_ui13.Skeleton, { className: "h-14", "aria-busy": "true" }) : isError ? /* @__PURE__ */ (0, import_jsx_runtime35.jsx)("p", { className: "text-red-600", children: "Could not load songs." }) : data.results.length === 0 ? /* @__PURE__ */ (0, import_jsx_runtime35.jsx)("p", { className: "text-[var(--color-text-muted)]", children: "No songs yet." }) : /* @__PURE__ */ (0, import_jsx_runtime35.jsxs)(import_jsx_runtime35.Fragment, { children: [
2252
- /* @__PURE__ */ (0, import_jsx_runtime35.jsxs)("div", { className: "flex flex-wrap items-center justify-between gap-2 mb-2", children: [
2253
- /* @__PURE__ */ (0, import_jsx_runtime35.jsxs)(
2326
+ return /* @__PURE__ */ (0, import_jsx_runtime36.jsx)(import_ui13.PanelSection, { title: "Songs", children: isPending ? /* @__PURE__ */ (0, import_jsx_runtime36.jsx)(import_ui13.Skeleton, { className: "h-14", "aria-busy": "true" }) : isError ? /* @__PURE__ */ (0, import_jsx_runtime36.jsx)("p", { className: "text-red-600", children: "Could not load songs." }) : data.results.length === 0 ? /* @__PURE__ */ (0, import_jsx_runtime36.jsx)("p", { className: "text-[var(--color-text-muted)]", children: "No songs yet." }) : /* @__PURE__ */ (0, import_jsx_runtime36.jsxs)(import_jsx_runtime36.Fragment, { children: [
2327
+ /* @__PURE__ */ (0, import_jsx_runtime36.jsxs)("div", { className: "flex flex-wrap items-center justify-between gap-2 mb-2", children: [
2328
+ /* @__PURE__ */ (0, import_jsx_runtime36.jsxs)(
2254
2329
  "button",
2255
2330
  {
2256
2331
  type: "button",
2257
2332
  onClick: () => void play(),
2258
2333
  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",
2259
2334
  children: [
2260
- /* @__PURE__ */ (0, import_jsx_runtime35.jsx)(import_lucide_react14.Play, { className: "h-3.5 w-3.5", "aria-hidden": "true" }),
2335
+ /* @__PURE__ */ (0, import_jsx_runtime36.jsx)(import_lucide_react14.Play, { className: "h-3.5 w-3.5", "aria-hidden": "true" }),
2261
2336
  "Play all"
2262
2337
  ]
2263
2338
  }
2264
2339
  ),
2265
- /* @__PURE__ */ (0, import_jsx_runtime35.jsx)("div", { role: "group", "aria-label": "Sort songs by", className: "flex gap-1 text-xs", children: SORTS.map((sort) => /* @__PURE__ */ (0, import_jsx_runtime35.jsx)(
2340
+ /* @__PURE__ */ (0, import_jsx_runtime36.jsx)("div", { role: "group", "aria-label": "Sort songs by", className: "flex gap-1 text-xs", children: SORTS.map((sort) => /* @__PURE__ */ (0, import_jsx_runtime36.jsx)(
2266
2341
  "button",
2267
2342
  {
2268
2343
  type: "button",
@@ -2277,7 +2352,7 @@ function ArtistSongs({ artist, getBackendBaseUrl, renderArtistLink }) {
2277
2352
  sort.ordering
2278
2353
  )) })
2279
2354
  ] }),
2280
- /* @__PURE__ */ (0, import_jsx_runtime35.jsx)("ul", { className: "overflow-hidden mb-2 bg-zinc-950 rounded-lg", children: data.results.map((song, index) => /* @__PURE__ */ (0, import_jsx_runtime35.jsx)("li", { onClickCapture: (event) => handleRowClickCapture(event, song), children: /* @__PURE__ */ (0, import_jsx_runtime35.jsx)(
2355
+ /* @__PURE__ */ (0, import_jsx_runtime36.jsx)("ul", { className: "overflow-hidden mb-2 bg-zinc-950 rounded-lg", children: data.results.map((song, index) => /* @__PURE__ */ (0, import_jsx_runtime36.jsx)("li", { onClickCapture: (event) => handleRowClickCapture(event, song), children: /* @__PURE__ */ (0, import_jsx_runtime36.jsx)(
2281
2356
  TrackItem,
2282
2357
  {
2283
2358
  track: song,
@@ -2285,12 +2360,12 @@ function ArtistSongs({ artist, getBackendBaseUrl, renderArtistLink }) {
2285
2360
  renderArtistLink
2286
2361
  }
2287
2362
  ) }, song.uuid)) }),
2288
- /* @__PURE__ */ (0, import_jsx_runtime35.jsx)(import_ui13.Pagination, { currentPage: page, totalPages: data.totalPages, onPageChange: setPage })
2363
+ /* @__PURE__ */ (0, import_jsx_runtime36.jsx)(import_ui13.Pagination, { currentPage: page, totalPages: data.totalPages, onPageChange: setPage })
2289
2364
  ] }) });
2290
2365
  }
2291
2366
 
2292
2367
  // src/genre-tree/artist/useArtistPanel.tsx
2293
- var import_jsx_runtime36 = require("react/jsx-runtime");
2368
+ var import_jsx_runtime37 = require("react/jsx-runtime");
2294
2369
  function useArtistPanel({
2295
2370
  uuid,
2296
2371
  scope,
@@ -2310,26 +2385,26 @@ function useArtistPanel({
2310
2385
  return {
2311
2386
  ...panel,
2312
2387
  title: "Artist",
2313
- content: /* @__PURE__ */ (0, import_jsx_runtime36.jsxs)("div", { "aria-busy": "true", className: "flex flex-col gap-2.5 px-6 py-2.5", children: [
2314
- /* @__PURE__ */ (0, import_jsx_runtime36.jsx)(import_ui14.Skeleton, { className: "h-3 w-16" }),
2315
- /* @__PURE__ */ (0, import_jsx_runtime36.jsx)(import_ui14.Skeleton, { className: "h-4 w-32" }),
2316
- /* @__PURE__ */ (0, import_jsx_runtime36.jsx)(import_ui14.Skeleton, { className: "h-3 w-16" }),
2317
- /* @__PURE__ */ (0, import_jsx_runtime36.jsx)(import_ui14.Skeleton, { className: "h-4 w-24" }),
2318
- /* @__PURE__ */ (0, import_jsx_runtime36.jsx)(import_ui14.Skeleton, { className: "h-16 mt-2.5" }),
2319
- /* @__PURE__ */ (0, import_jsx_runtime36.jsx)(import_ui14.Skeleton, { className: "h-16" })
2388
+ content: /* @__PURE__ */ (0, import_jsx_runtime37.jsxs)("div", { "aria-busy": "true", className: "flex flex-col gap-2.5 px-6 py-2.5", children: [
2389
+ /* @__PURE__ */ (0, import_jsx_runtime37.jsx)(import_ui14.Skeleton, { className: "h-3 w-16" }),
2390
+ /* @__PURE__ */ (0, import_jsx_runtime37.jsx)(import_ui14.Skeleton, { className: "h-4 w-32" }),
2391
+ /* @__PURE__ */ (0, import_jsx_runtime37.jsx)(import_ui14.Skeleton, { className: "h-3 w-16" }),
2392
+ /* @__PURE__ */ (0, import_jsx_runtime37.jsx)(import_ui14.Skeleton, { className: "h-4 w-24" }),
2393
+ /* @__PURE__ */ (0, import_jsx_runtime37.jsx)(import_ui14.Skeleton, { className: "h-16 mt-2.5" }),
2394
+ /* @__PURE__ */ (0, import_jsx_runtime37.jsx)(import_ui14.Skeleton, { className: "h-16" })
2320
2395
  ] })
2321
2396
  };
2322
2397
  case "not-found":
2323
2398
  return {
2324
2399
  ...panel,
2325
2400
  title: "Artist not found",
2326
- content: /* @__PURE__ */ (0, import_jsx_runtime36.jsx)("p", { className: "px-6 py-2.5 text-[var(--color-text-muted)]", children: "This artist does not exist." })
2401
+ content: /* @__PURE__ */ (0, import_jsx_runtime37.jsx)("p", { className: "px-6 py-2.5 text-[var(--color-text-muted)]", children: "This artist does not exist." })
2327
2402
  };
2328
2403
  case "error":
2329
2404
  return {
2330
2405
  ...panel,
2331
2406
  title: "Artist",
2332
- content: /* @__PURE__ */ (0, import_jsx_runtime36.jsx)("p", { className: "px-6 py-2.5 text-red-600", children: "Could not load this artist." })
2407
+ content: /* @__PURE__ */ (0, import_jsx_runtime37.jsx)("p", { className: "px-6 py-2.5 text-red-600", children: "Could not load this artist." })
2333
2408
  };
2334
2409
  case "success": {
2335
2410
  const { artist } = result;
@@ -2338,13 +2413,13 @@ function useArtistPanel({
2338
2413
  return {
2339
2414
  ...panel,
2340
2415
  title: artist.name,
2341
- content: /* @__PURE__ */ (0, import_jsx_runtime36.jsxs)(import_jsx_runtime36.Fragment, { children: [
2342
- shareUrl && /* @__PURE__ */ (0, import_jsx_runtime36.jsx)("div", { className: "genre-action-band border-b border-[var(--color-border)]", children: /* @__PURE__ */ (0, import_jsx_runtime36.jsx)("div", { className: "flex flex-wrap gap-1 px-3.5 py-3", children: shareTarget && /* @__PURE__ */ (0, import_jsx_runtime36.jsx)(ShareActionButton, { target: shareTarget, url: shareUrl }) }) }),
2343
- artist.image && /* @__PURE__ */ (0, import_jsx_runtime36.jsx)(ArtistPhoto, { image: artist.image, name: artist.name }),
2344
- /* @__PURE__ */ (0, import_jsx_runtime36.jsx)(ArtistHeader, { artist }),
2345
- /* @__PURE__ */ (0, import_jsx_runtime36.jsx)(ArtistMembers, { title: "Members", memberships: artist.members, renderArtistLink }),
2346
- /* @__PURE__ */ (0, import_jsx_runtime36.jsx)(ArtistMembers, { title: "Member of", memberships: artist.memberOf, renderArtistLink }),
2347
- artist.tracksCount > 0 && /* @__PURE__ */ (0, import_jsx_runtime36.jsx)(
2416
+ content: /* @__PURE__ */ (0, import_jsx_runtime37.jsxs)(import_jsx_runtime37.Fragment, { children: [
2417
+ shareUrl && /* @__PURE__ */ (0, import_jsx_runtime37.jsx)("div", { className: "genre-action-band border-b border-[var(--color-border)]", children: /* @__PURE__ */ (0, import_jsx_runtime37.jsx)("div", { className: "flex flex-wrap gap-1 px-3.5 py-3", children: shareTarget && /* @__PURE__ */ (0, import_jsx_runtime37.jsx)(ShareActionButton, { target: shareTarget, url: shareUrl }) }) }),
2418
+ artist.image && /* @__PURE__ */ (0, import_jsx_runtime37.jsx)(ArtistPhoto, { image: artist.image, name: artist.name }),
2419
+ /* @__PURE__ */ (0, import_jsx_runtime37.jsx)(ArtistHeader, { artist }),
2420
+ /* @__PURE__ */ (0, import_jsx_runtime37.jsx)(ArtistMembers, { title: "Members", memberships: artist.members, renderArtistLink }),
2421
+ /* @__PURE__ */ (0, import_jsx_runtime37.jsx)(ArtistMembers, { title: "Member of", memberships: artist.memberOf, renderArtistLink }),
2422
+ artist.tracksCount > 0 && /* @__PURE__ */ (0, import_jsx_runtime37.jsx)(
2348
2423
  ArtistSongs,
2349
2424
  {
2350
2425
  artist: { uuid: artist.uuid, name: artist.name },
@@ -2353,7 +2428,7 @@ function useArtistPanel({
2353
2428
  }
2354
2429
  )
2355
2430
  ] }),
2356
- extraContent: artist.albums.length > 0 ? /* @__PURE__ */ (0, import_jsx_runtime36.jsx)(ArtistDiscography, { albums: artist.albums }) : void 0
2431
+ extraContent: artist.albums.length > 0 ? /* @__PURE__ */ (0, import_jsx_runtime37.jsx)(ArtistDiscography, { albums: artist.albums }) : void 0
2357
2432
  };
2358
2433
  }
2359
2434
  }
@@ -2362,27 +2437,27 @@ function useArtistPanel({
2362
2437
  // src/genre-tree/SizeMetricToggle.tsx
2363
2438
  var import_md2 = require("react-icons/md");
2364
2439
  var import_ui15 = require("@behindthemusictree/ui");
2365
- var import_jsx_runtime37 = require("react/jsx-runtime");
2440
+ var import_jsx_runtime38 = require("react/jsx-runtime");
2366
2441
  function SizeMetricToggle({
2367
2442
  value,
2368
2443
  onChange
2369
2444
  }) {
2370
- return /* @__PURE__ */ (0, import_jsx_runtime37.jsx)(
2445
+ return /* @__PURE__ */ (0, import_jsx_runtime38.jsx)(
2371
2446
  import_ui15.SegmentedControl,
2372
2447
  {
2373
2448
  "aria-label": "Size nodes by",
2374
2449
  value,
2375
2450
  onChange,
2376
2451
  options: [
2377
- { value: "songs", label: /* @__PURE__ */ (0, import_jsx_runtime37.jsx)(import_md2.MdMusicNote, { size: 18, "aria-hidden": true }), "aria-label": "Size by songs", title: "Node size by song count" },
2378
- { value: "views", label: /* @__PURE__ */ (0, import_jsx_runtime37.jsx)(import_md2.MdVisibility, { size: 18, "aria-hidden": true }), "aria-label": "Size by views", title: "Node size by song views" }
2452
+ { value: "songs", label: /* @__PURE__ */ (0, import_jsx_runtime38.jsx)(import_md2.MdMusicNote, { size: 18, "aria-hidden": true }), "aria-label": "Size by songs", title: "Node size by song count" },
2453
+ { value: "views", label: /* @__PURE__ */ (0, import_jsx_runtime38.jsx)(import_md2.MdVisibility, { size: 18, "aria-hidden": true }), "aria-label": "Size by views", title: "Node size by song views" }
2379
2454
  ]
2380
2455
  }
2381
2456
  );
2382
2457
  }
2383
2458
 
2384
2459
  // src/genre-tree/GenreTreeView.tsx
2385
- var import_jsx_runtime38 = require("react/jsx-runtime");
2460
+ var import_jsx_runtime39 = require("react/jsx-runtime");
2386
2461
  var PREVIOUS_BUTTON_CSS = `
2387
2462
  .genre-tree-view .search-overlay .gtv-info-panel-previous { display: none; }
2388
2463
  .genre-tree-view:has(.search-overlay .gtv-info-panel-previous) .genre-search input { padding-left: 2.75rem; }
@@ -2392,27 +2467,6 @@ var PREVIOUS_BUTTON_CSS = `
2392
2467
  .genre-search-previous { animation: genre-tree-view-previous-in 200ms ease-out; }
2393
2468
  }
2394
2469
  `;
2395
- function GenreNodeActionBand({ genrePlaylist, scope, playback, ...bandProps }) {
2396
- const { isPlaying, setIsPlaying } = (0, import_player10.usePlayer)();
2397
- const isCurrentTrackList = playback.playingNodeId === genrePlaylist.uuid;
2398
- const handlePlayPause = () => {
2399
- if (isCurrentTrackList) {
2400
- setIsPlaying(!isPlaying);
2401
- return;
2402
- }
2403
- playback.play(genrePlaylist).catch((error) => {
2404
- console.error("Failed to load genre playlist tracks:", error);
2405
- });
2406
- };
2407
- return /* @__PURE__ */ (0, import_jsx_runtime38.jsx)(
2408
- GenreActionBand,
2409
- {
2410
- ...bandProps,
2411
- onPlayPause: genrePlaylist.tracksCount > 0 ? handlePlayPause : void 0,
2412
- isPlaying: isCurrentTrackList && isPlaying
2413
- }
2414
- );
2415
- }
2416
2470
  var NO_POP_ROOT_TITLE = "This genre tree has no 'Mainstream Pop' root yet";
2417
2471
  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]";
2418
2472
  function GenreTreeView({
@@ -2426,6 +2480,7 @@ function GenreTreeView({
2426
2480
  selectedNodeId: controlledSelectedNodeId,
2427
2481
  onSelectedNodeIdChange,
2428
2482
  defaultViewport,
2483
+ viewportRestore,
2429
2484
  onViewportChange,
2430
2485
  readOnly = false,
2431
2486
  className: className2,
@@ -2434,7 +2489,6 @@ function GenreTreeView({
2434
2489
  allowWheelRotation: controlledAllowWheelRotation,
2435
2490
  criteriaOverviewSchema,
2436
2491
  renderGenreDetailExtras,
2437
- controlsOrientation = "horizontal",
2438
2492
  floatingControlsLeading,
2439
2493
  sizeMetric: controlledSizeMetric,
2440
2494
  onSizeMetricChange,
@@ -2461,14 +2515,14 @@ function GenreTreeView({
2461
2515
  placement = "default",
2462
2516
  ...focus
2463
2517
  }) {
2464
- const getShareUrlRef = (0, import_react13.useRef)(getShareUrl);
2518
+ const getShareUrlRef = (0, import_react15.useRef)(getShareUrl);
2465
2519
  getShareUrlRef.current = getShareUrl;
2466
- const [internalArtistUuid, setInternalArtistUuid] = (0, import_react13.useState)(null);
2520
+ const [internalArtistUuid, setInternalArtistUuid] = (0, import_react15.useState)(null);
2467
2521
  const isArtistControlled = controlledArtistUuid !== void 0;
2468
2522
  const artistUuid = isArtistControlled ? controlledArtistUuid : internalArtistUuid;
2469
- const [internalSizeMetric, setInternalSizeMetric] = (0, import_react13.useState)("songs");
2523
+ const [internalSizeMetric, setInternalSizeMetric] = (0, import_react15.useState)("songs");
2470
2524
  const sizeMetric = controlledSizeMetric ?? internalSizeMetric;
2471
- const deferredSizeMetric = (0, import_react13.useDeferredValue)(sizeMetric);
2525
+ const deferredSizeMetric = (0, import_react15.useDeferredValue)(sizeMetric);
2472
2526
  const changeSizeMetric = (metric) => {
2473
2527
  if (controlledSizeMetric === void 0) setInternalSizeMetric(metric);
2474
2528
  onSizeMetricChange?.(metric);
@@ -2504,6 +2558,16 @@ function GenreTreeView({
2504
2558
  criteriaOverviewSchema,
2505
2559
  artistUuid
2506
2560
  });
2561
+ const restoreRevision = viewportRestore?.revision;
2562
+ const [stackedRestoreOwner, setStackedRestoreOwner] = (0, import_react15.useState)({ revision: restoreRevision, rootUuid: selectedGenrePlaylist?.root.uuid });
2563
+ let restoreRootUuid = stackedRestoreOwner.rootUuid;
2564
+ if (stackedRestoreOwner.revision !== restoreRevision) {
2565
+ restoreRootUuid = selectedGenrePlaylist?.root.uuid;
2566
+ setStackedRestoreOwner({ revision: restoreRevision, rootUuid: restoreRootUuid });
2567
+ } else if (restoreRootUuid === void 0 && selectedGenrePlaylist) {
2568
+ restoreRootUuid = selectedGenrePlaylist.root.uuid;
2569
+ setStackedRestoreOwner({ revision: restoreRevision, rootUuid: restoreRootUuid });
2570
+ }
2507
2571
  const isGenreUpdating = (0, import_react_query3.useIsMutating)({ mutationKey: import_genre_tree17.genreUpdateMutationKey }) > 0;
2508
2572
  const { mutate: updateGenre } = (0, import_genre_tree17.useUpdateGenre)(scope, getBackendBaseUrl);
2509
2573
  const busy = deferredSizeMetric !== sizeMetric || isTreeFetching || isGenreUpdating;
@@ -2511,7 +2575,7 @@ function GenreTreeView({
2511
2575
  const canSizeByViews = !isArtistFiltered && hasViewsCount(visibleGenrePlaylists);
2512
2576
  const artistResult = (0, import_genre_tree17.useArtist)(artistUuid, artistSearchBaseUrl ?? getBackendBaseUrl);
2513
2577
  const filterArtistName = artistResult.status === "success" ? artistResult.artist.name : null;
2514
- const filterArtist = (0, import_react13.useMemo)(
2578
+ const filterArtist = (0, import_react15.useMemo)(
2515
2579
  () => artistUuid !== null && filterArtistName !== null ? { uuid: artistUuid, name: filterArtistName } : null,
2516
2580
  [artistUuid, filterArtistName]
2517
2581
  );
@@ -2521,14 +2585,14 @@ function GenreTreeView({
2521
2585
  if (selectedNodeId !== null) clearSelection();
2522
2586
  changeArtist(uuid);
2523
2587
  };
2524
- const [expandedArtistUuid, setExpandedArtistUuid] = (0, import_react13.useState)(null);
2588
+ const [expandedArtistUuid, setExpandedArtistUuid] = (0, import_react15.useState)(null);
2525
2589
  const artistBaseUrl = artistSearchBaseUrl ?? getBackendBaseUrl;
2526
2590
  const artistPanel = useArtistPanel({
2527
2591
  uuid: artistUuid,
2528
2592
  scope,
2529
2593
  getBackendBaseUrl: artistBaseUrl,
2530
2594
  getShareUrl: getShareUrlRef.current,
2531
- renderArtistLink: renderArtistLink ?? ((artist) => artist.isSpecialPurpose ? artist.name : /* @__PURE__ */ (0, import_jsx_runtime38.jsx)("button", { type: "button", onClick: () => openArtist(artist.uuid), className: "hover:underline", children: artist.name })),
2595
+ renderArtistLink: renderArtistLink ?? ((artist) => artist.isSpecialPurpose ? artist.name : /* @__PURE__ */ (0, import_jsx_runtime39.jsx)("button", { type: "button", onClick: () => openArtist(artist.uuid), className: "hover:underline", children: artist.name })),
2532
2596
  expanded: controlledArtistExpanded ?? (artistUuid !== null && expandedArtistUuid === artistUuid),
2533
2597
  onExpandedChange: (on) => {
2534
2598
  if (controlledArtistExpanded === void 0) setExpandedArtistUuid(on ? artistUuid : null);
@@ -2538,8 +2602,8 @@ function GenreTreeView({
2538
2602
  });
2539
2603
  const builtInArtistPanel = overlayPanel || hasSelectedGenreNode ? void 0 : artistPanel;
2540
2604
  const activeOverlayPanel = overlayPanel ?? (builtInArtistPanel && { ...builtInArtistPanel, hideTitle: true, hideClose: true });
2541
- const [uncontrolledCollapsed, setUncontrolledCollapsed] = (0, import_react13.useState)(false);
2542
- const [uncontrolledExpanded, setUncontrolledExpanded] = (0, import_react13.useState)(false);
2605
+ const [uncontrolledCollapsed, setUncontrolledCollapsed] = (0, import_react15.useState)(false);
2606
+ const [uncontrolledExpanded, setUncontrolledExpanded] = (0, import_react15.useState)(false);
2543
2607
  const isCollapsed = collapsed ?? uncontrolledCollapsed;
2544
2608
  const isExpanded = expanded ?? uncontrolledExpanded;
2545
2609
  const changeCollapsed = (next) => {
@@ -2550,18 +2614,11 @@ function GenreTreeView({
2550
2614
  setUncontrolledExpanded(next);
2551
2615
  onExpandedChange?.(next);
2552
2616
  };
2553
- (0, import_react13.useEffect)(() => {
2617
+ (0, import_react15.useEffect)(() => {
2554
2618
  setUncontrolledCollapsed(false);
2555
2619
  }, [selectedNodeId, artistUuid]);
2556
2620
  const isDetailHidden = isCollapsed || !hasSelectedGenreNode && !activeOverlayPanel;
2557
- (0, import_react13.useEffect)(() => {
2558
- if (placement !== "regions" || !isDetailHidden) return void 0;
2559
- const root = document.documentElement;
2560
- root.style.setProperty("--layout-left-w", "0px");
2561
- return () => {
2562
- root.style.removeProperty("--layout-left-w");
2563
- };
2564
- }, [placement, isDetailHidden]);
2621
+ useLeftDetailPanelLayout(!isDetailHidden, placement === "regions");
2565
2622
  const detailOverlay = placement === "regions" && activeOverlayPanel?.onExpandedChange ? activeOverlayPanel : void 0;
2566
2623
  const detailExpanded = detailOverlay ? !!detailOverlay.expanded : isExpanded;
2567
2624
  const onDetailExpandedChange = detailOverlay ? detailOverlay.onExpandedChange : changeExpanded;
@@ -2573,14 +2630,14 @@ function GenreTreeView({
2573
2630
  hideExpandButton: placement === "regions" ? true : void 0,
2574
2631
  overlayPanel: detailOverlay ? { ...detailOverlay, collapsed: isCollapsed, hideExpandButton: true } : activeOverlayPanel
2575
2632
  };
2576
- const [isWheelRevealed, setIsWheelRevealed] = (0, import_react13.useState)(false);
2633
+ const [isWheelRevealed, setIsWheelRevealed] = (0, import_react15.useState)(false);
2577
2634
  const usesHandoff = viewMode === "wheel" || viewMode === "pop-core";
2578
2635
  const isTreeReady = !isLoading && (!usesHandoff || isWheelRevealed);
2579
- const rootRef = (0, import_react13.useRef)(null);
2636
+ const rootRef = (0, import_react15.useRef)(null);
2580
2637
  const handlePrevious = () => {
2581
2638
  rootRef.current?.querySelector(".search-overlay .gtv-info-panel-previous")?.click();
2582
2639
  };
2583
- (0, import_react13.useEffect)(() => {
2640
+ (0, import_react15.useEffect)(() => {
2584
2641
  if (!isTreeReady || viewMode !== "pop-core" || !hasSelectedGenreNode || !selectedGenrePlaylist) return;
2585
2642
  const root = rootRef.current;
2586
2643
  const centerChip = root?.querySelector(".gtv-wheel-center-node .gtv-wheel-chip");
@@ -2590,21 +2647,21 @@ function GenreTreeView({
2590
2647
  centerChip.click();
2591
2648
  }, [hasSelectedGenreNode, isTreeReady, selectedGenrePlaylist, viewMode]);
2592
2649
  const { showPopup, hidePopup } = (0, import_popup6.usePopup)();
2593
- const [searchText, setSearchText] = (0, import_react13.useState)("");
2650
+ const [searchText, setSearchText] = (0, import_react15.useState)("");
2594
2651
  const artistSearchResult = (0, import_genre_tree16.useArtistSearch)(artistBaseUrl, artistSearch ? searchText : "");
2595
2652
  const artistExtraResults = artistSearch ? {
2596
2653
  label: "Artists",
2597
2654
  items: artistSearchResult.artists.map((artist) => ({
2598
2655
  key: artist.uuid,
2599
- content: /* @__PURE__ */ (0, import_jsx_runtime38.jsxs)(import_jsx_runtime38.Fragment, { children: [
2600
- /* @__PURE__ */ (0, import_jsx_runtime38.jsxs)("span", { className: "block truncate font-medium", children: [
2656
+ content: /* @__PURE__ */ (0, import_jsx_runtime39.jsxs)(import_jsx_runtime39.Fragment, { children: [
2657
+ /* @__PURE__ */ (0, import_jsx_runtime39.jsxs)("span", { className: "block truncate font-medium", children: [
2601
2658
  artist.name,
2602
- artist.disambiguation && /* @__PURE__ */ (0, import_jsx_runtime38.jsxs)("span", { className: "font-normal text-[var(--color-neutral-500)]", children: [
2659
+ artist.disambiguation && /* @__PURE__ */ (0, import_jsx_runtime39.jsxs)("span", { className: "font-normal text-[var(--color-neutral-500)]", children: [
2603
2660
  " \xB7 ",
2604
2661
  artist.disambiguation
2605
2662
  ] })
2606
2663
  ] }),
2607
- artist.matchedAlias && /* @__PURE__ */ (0, import_jsx_runtime38.jsxs)("span", { className: "block truncate text-xs text-[var(--color-neutral-500)]", children: [
2664
+ artist.matchedAlias && /* @__PURE__ */ (0, import_jsx_runtime39.jsxs)("span", { className: "block truncate text-xs text-[var(--color-neutral-500)]", children: [
2608
2665
  "also known as ",
2609
2666
  artist.matchedAlias
2610
2667
  ] })
@@ -2630,13 +2687,13 @@ function GenreTreeView({
2630
2687
  const handleGenreSearchZoomIn = () => {
2631
2688
  if (selectedUuid) focus.onFocusGenreChange?.(selectedUuid, "zoom-in");
2632
2689
  };
2633
- const zoomInRef = (0, import_react13.useRef)(void 0);
2690
+ const zoomInRef = (0, import_react15.useRef)(void 0);
2634
2691
  zoomInRef.current = canZoomIn ? handleGenreSearchZoomIn : void 0;
2635
- const renderInfoPanelBody = (0, import_react13.useCallback)(
2692
+ const renderInfoPanelBody = (0, import_react15.useCallback)(
2636
2693
  (node, { toolbar, ancestors, family }) => {
2637
2694
  const genrePlaylist = genrePlaylistByNodeId.get(node.id);
2638
2695
  const criteria = genrePlaylist?.criteria;
2639
- if (!genrePlaylist || !criteria) return { primary: /* @__PURE__ */ (0, import_jsx_runtime38.jsxs)(import_jsx_runtime38.Fragment, { children: [
2696
+ if (!genrePlaylist || !criteria) return { primary: /* @__PURE__ */ (0, import_jsx_runtime39.jsxs)(import_jsx_runtime39.Fragment, { children: [
2640
2697
  toolbar,
2641
2698
  family
2642
2699
  ] }), aside: ancestors };
@@ -2656,21 +2713,26 @@ function GenreTreeView({
2656
2713
  (sibling) => sibling.parent?.uuid === parentPlaylist.uuid && sibling.uuid !== genrePlaylist.uuid && sibling.criteria?.side === "pop"
2657
2714
  ) : void 0;
2658
2715
  return {
2659
- primary: /* @__PURE__ */ (0, import_jsx_runtime38.jsxs)(import_jsx_runtime38.Fragment, { children: [
2660
- toolbar,
2661
- /* @__PURE__ */ (0, import_jsx_runtime38.jsx)(
2662
- GenreNodeActionBand,
2663
- {
2716
+ primary: /* @__PURE__ */ (0, import_jsx_runtime39.jsx)(
2717
+ GenreInfoPanelBody,
2718
+ {
2719
+ toolbar,
2720
+ family,
2721
+ genreUuid,
2722
+ genrePlaylist,
2723
+ wikidataId: criteria.wikidataId,
2724
+ scope,
2725
+ getBackendBaseUrl,
2726
+ playback,
2727
+ band: {
2664
2728
  node,
2665
- genrePlaylist,
2666
- scope,
2667
2729
  shareTarget: shareUrl ? shareTarget : void 0,
2668
2730
  shareUrl,
2669
2731
  onZoomIn: zoomInRef.current,
2670
2732
  onAddChild: readOnly ? void 0 : () => handleGenreCreationAction(criteria),
2671
2733
  onRename: readOnly ? void 0 : () => handleGenreRenameAction(criteria),
2672
2734
  onChangeParent: readOnly ? void 0 : () => showPopup(
2673
- /* @__PURE__ */ (0, import_jsx_runtime38.jsx)(
2735
+ /* @__PURE__ */ (0, import_jsx_runtime39.jsx)(
2674
2736
  ChangeParentPopup,
2675
2737
  {
2676
2738
  genrePlaylist: { ...genrePlaylist, criteria },
@@ -2685,19 +2747,9 @@ function GenreTreeView({
2685
2747
  side,
2686
2748
  onToggleSide: canToggleSide ? () => updateGenre({ uuid: genreUuid, data: { side: side === "pop" ? "core" : "pop" } }) : void 0,
2687
2749
  toggleSideDisabledReason: popSibling ? `${popSibling.name} is already the pop side of ${parentPlaylist?.name}` : void 0,
2688
- additionalActions,
2689
- playback
2690
- }
2691
- ),
2692
- /* @__PURE__ */ (0, import_jsx_runtime38.jsx)(
2693
- GenreDetailExtras,
2694
- {
2695
- family,
2696
- genreUuid,
2697
- genrePlaylist,
2698
- wikidataId: criteria.wikidataId,
2699
- scope,
2700
- getBackendBaseUrl,
2750
+ additionalActions
2751
+ },
2752
+ extras: {
2701
2753
  criteriaOverviewSchema,
2702
2754
  renderGenreDetailExtras,
2703
2755
  renderArtistLink,
@@ -2707,10 +2759,9 @@ function GenreTreeView({
2707
2759
  renderSongLeading,
2708
2760
  renderSongActions,
2709
2761
  renderTracksToolbar
2710
- },
2711
- genreUuid
2712
- )
2713
- ] }),
2762
+ }
2763
+ }
2764
+ ),
2714
2765
  aside: ancestors
2715
2766
  };
2716
2767
  },
@@ -2741,7 +2792,7 @@ function GenreTreeView({
2741
2792
  );
2742
2793
  const isRotationControlled = controlledAllowWheelRotation !== void 0;
2743
2794
  const allowWheelRotation = controlledAllowWheelRotation ?? internalAllowWheelRotation;
2744
- const detailTab = placement === "regions" && (hasSelectedGenreNode || detailOverlay) ? /* @__PURE__ */ (0, import_jsx_runtime38.jsx)(
2795
+ const detailTab = placement === "regions" && (hasSelectedGenreNode || detailOverlay) ? /* @__PURE__ */ (0, import_jsx_runtime39.jsx)(
2745
2796
  PanelEdgeTab,
2746
2797
  {
2747
2798
  side: "left",
@@ -2751,8 +2802,8 @@ function GenreTreeView({
2751
2802
  ) : null;
2752
2803
  const renderArtistFilter = () => {
2753
2804
  if (isLoading || !filterArtist) return null;
2754
- return /* @__PURE__ */ (0, import_jsx_runtime38.jsxs)("span", { className: "inline-flex items-center overflow-hidden rounded-full bg-[var(--color-neutral-100)] text-sm", children: [
2755
- /* @__PURE__ */ (0, import_jsx_runtime38.jsx)(
2805
+ return /* @__PURE__ */ (0, import_jsx_runtime39.jsxs)("span", { className: "inline-flex items-center overflow-hidden rounded-full bg-[var(--color-neutral-100)] text-sm", children: [
2806
+ /* @__PURE__ */ (0, import_jsx_runtime39.jsx)(
2756
2807
  "button",
2757
2808
  {
2758
2809
  type: "button",
@@ -2764,7 +2815,7 @@ function GenreTreeView({
2764
2815
  children: filterArtist.name
2765
2816
  }
2766
2817
  ),
2767
- /* @__PURE__ */ (0, import_jsx_runtime38.jsx)(
2818
+ /* @__PURE__ */ (0, import_jsx_runtime39.jsx)(
2768
2819
  "button",
2769
2820
  {
2770
2821
  type: "button",
@@ -2776,8 +2827,8 @@ function GenreTreeView({
2776
2827
  )
2777
2828
  ] });
2778
2829
  };
2779
- const actions = /* @__PURE__ */ (0, import_jsx_runtime38.jsxs)(import_jsx_runtime38.Fragment, { children: [
2780
- !isLoading && /* @__PURE__ */ (0, import_jsx_runtime38.jsx)(
2830
+ const actions = /* @__PURE__ */ (0, import_jsx_runtime39.jsxs)(import_jsx_runtime39.Fragment, { children: [
2831
+ !isLoading && /* @__PURE__ */ (0, import_jsx_runtime39.jsx)(
2781
2832
  GenreSearch,
2782
2833
  {
2783
2834
  genrePlaylists: visibleGenrePlaylists,
@@ -2789,9 +2840,9 @@ function GenreTreeView({
2789
2840
  extraResults: artistExtraResults
2790
2841
  }
2791
2842
  ),
2792
- !isLoading && filterArtist && /* @__PURE__ */ (0, import_jsx_runtime38.jsx)("div", { className: "hidden w-full justify-center max-md:flex", children: renderArtistFilter() }),
2793
- /* @__PURE__ */ (0, import_jsx_runtime38.jsxs)("div", { className: "contents max-md:flex max-md:w-full max-md:items-center max-md:gap-2 max-md:overflow-x-auto max-md:[scrollbar-width:none] max-md:[&>*]:shrink-0", children: [
2794
- !isLoading && !isControlled && /* @__PURE__ */ (0, import_jsx_runtime38.jsx)(
2843
+ !isLoading && filterArtist && /* @__PURE__ */ (0, import_jsx_runtime39.jsx)("div", { className: "hidden w-full justify-center max-md:flex", children: renderArtistFilter() }),
2844
+ /* @__PURE__ */ (0, import_jsx_runtime39.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: [
2845
+ !isLoading && !isControlled && /* @__PURE__ */ (0, import_jsx_runtime39.jsx)(
2795
2846
  import_ui16.SegmentedControl,
2796
2847
  {
2797
2848
  "aria-label": "Tree view mode",
@@ -2805,13 +2856,13 @@ function GenreTreeView({
2805
2856
  ]
2806
2857
  }
2807
2858
  ),
2808
- !isLoading && showDisplayOptions && /* @__PURE__ */ (0, import_jsx_runtime38.jsx)(
2859
+ !isLoading && showDisplayOptions && /* @__PURE__ */ (0, import_jsx_runtime39.jsx)(
2809
2860
  "div",
2810
2861
  {
2811
2862
  className: "flex items-center gap-1",
2812
2863
  role: "group",
2813
2864
  "aria-label": "Tree display options",
2814
- children: !isRotationControlled && viewMode !== "stacked" && viewMode !== "outline" && /* @__PURE__ */ (0, import_jsx_runtime38.jsx)(
2865
+ children: !isRotationControlled && viewMode !== "stacked" && viewMode !== "outline" && /* @__PURE__ */ (0, import_jsx_runtime39.jsx)(
2815
2866
  import_ui16.Button,
2816
2867
  {
2817
2868
  variant: allowWheelRotation ? "default" : "outline",
@@ -2822,7 +2873,7 @@ function GenreTreeView({
2822
2873
  )
2823
2874
  }
2824
2875
  ),
2825
- !isLoading && !readOnly && showAddRoot && /* @__PURE__ */ (0, import_jsx_runtime38.jsx)(
2876
+ !isLoading && !readOnly && showAddRoot && /* @__PURE__ */ (0, import_jsx_runtime39.jsx)(
2826
2877
  import_ui16.IconTextButton,
2827
2878
  {
2828
2879
  icon: import_lucide_react15.Plus,
@@ -2832,42 +2883,42 @@ function GenreTreeView({
2832
2883
  )
2833
2884
  ] })
2834
2885
  ] });
2835
- return /* @__PURE__ */ (0, import_jsx_runtime38.jsxs)(
2886
+ return /* @__PURE__ */ (0, import_jsx_runtime39.jsxs)(
2836
2887
  "div",
2837
2888
  {
2838
2889
  ref: rootRef,
2839
2890
  className: ["genre-tree-view relative flex flex-col h-full", placement === "regions" && REGIONS_CLASS, className2].filter(Boolean).join(" "),
2840
2891
  children: [
2841
- /* @__PURE__ */ (0, import_jsx_runtime38.jsx)("style", { children: PREVIOUS_BUTTON_CSS }),
2842
- /* @__PURE__ */ (0, import_jsx_runtime38.jsx)(
2892
+ /* @__PURE__ */ (0, import_jsx_runtime39.jsx)("style", { children: PREVIOUS_BUTTON_CSS }),
2893
+ /* @__PURE__ */ (0, import_jsx_runtime39.jsx)(
2843
2894
  "div",
2844
2895
  {
2845
2896
  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"}`,
2846
2897
  children: actions
2847
2898
  }
2848
2899
  ),
2849
- !isLoading && filterArtist && /* @__PURE__ */ (0, import_jsx_runtime38.jsx)(
2900
+ !isLoading && filterArtist && /* @__PURE__ */ (0, import_jsx_runtime39.jsx)(
2850
2901
  "div",
2851
2902
  {
2852
2903
  className: `absolute left-1/2 z-30 hidden -translate-x-1/2 md:block ${placement === "regions" ? "top-[calc(var(--layout-gutter)+var(--layout-panel-inset))]" : "top-5"}`,
2853
2904
  children: renderArtistFilter()
2854
2905
  }
2855
2906
  ),
2856
- detailTab && /* @__PURE__ */ (0, import_jsx_runtime38.jsx)(
2907
+ detailTab && /* @__PURE__ */ (0, import_jsx_runtime39.jsx)(
2857
2908
  "div",
2858
2909
  {
2859
2910
  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))]"}`,
2860
2911
  children: detailTab
2861
2912
  }
2862
2913
  ),
2863
- /* @__PURE__ */ (0, import_jsx_runtime38.jsx)("div", { className: "content-container flex-1 min-h-0 flex flex-row gap-4", children: /* @__PURE__ */ (0, import_jsx_runtime38.jsxs)("div", { className: "tree-view-container relative flex-1 min-w-0 flex flex-col h-full", children: [
2864
- !isLoading && viewMode === "outline" && floatingControlsLeading && /* @__PURE__ */ (0, import_jsx_runtime38.jsx)("div", { className: "absolute bottom-3 right-3 z-10", children: floatingControlsLeading }),
2865
- isLoading ? /* @__PURE__ */ (0, import_jsx_runtime38.jsx)(import_genre_tree_view4.GenreTreeViewSkeleton, { viewMode }) : viewMode === "wheel" ? (
2914
+ /* @__PURE__ */ (0, import_jsx_runtime39.jsx)("div", { className: "content-container flex-1 min-h-0 flex flex-row gap-4", children: /* @__PURE__ */ (0, import_jsx_runtime39.jsxs)("div", { className: "tree-view-container relative flex-1 min-w-0 flex flex-col h-full", children: [
2915
+ !isLoading && viewMode === "outline" && floatingControlsLeading && /* @__PURE__ */ (0, import_jsx_runtime39.jsx)("div", { className: "absolute bottom-3 right-3 z-10", children: floatingControlsLeading }),
2916
+ isLoading ? /* @__PURE__ */ (0, import_jsx_runtime39.jsx)(import_genre_tree_view4.GenreTreeViewSkeleton, { viewMode }) : viewMode === "wheel" ? (
2866
2917
  // The info panel's top edge aligns with the tree's; the search bar floats `--layout-panel-inset` below it,
2867
2918
  // and the header inset (the bar's height plus that inset) extends the header's fill behind the bar,
2868
2919
  // Google Maps style; the 8px bottom padding mirrors the inset above the bar, and the hidden title's height is zeroed.
2869
2920
  // The panel's own Zoom in button is hidden: the search bar carries it.
2870
- /* @__PURE__ */ (0, import_jsx_runtime38.jsx)("div", { className: "tree-container search-overlay relative flex-1 w-full min-h-0 [--gtv-info-panel-header-inset:calc(var(--layout-search-h,2.5rem)+var(--layout-panel-inset,0.5rem))] [&_.gtv-info-panel-header]:pb-2 [&_.gtv-info-panel-title--hidden]:h-0 [&_.gtv-toolbar]:hidden", children: /* @__PURE__ */ (0, import_jsx_runtime38.jsx)(GenreTreeWheelHandoff, { skeleton: /* @__PURE__ */ (0, import_jsx_runtime38.jsx)(import_genre_tree_view4.GenreTreeWheelSkeleton, {}), onRevealedChange: setIsWheelRevealed, children: /* @__PURE__ */ (0, import_jsx_runtime38.jsx)(
2921
+ /* @__PURE__ */ (0, import_jsx_runtime39.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_runtime39.jsx)(GenreTreeWheelHandoff, { skeleton: /* @__PURE__ */ (0, import_jsx_runtime39.jsx)(import_genre_tree_view4.GenreTreeWheelSkeleton, {}), onRevealedChange: setIsWheelRevealed, children: /* @__PURE__ */ (0, import_jsx_runtime39.jsx)(
2871
2922
  GenrePlaylistTreeWheel,
2872
2923
  {
2873
2924
  scope,
@@ -2884,21 +2935,21 @@ function GenreTreeView({
2884
2935
  selectedNodeId,
2885
2936
  readOnly,
2886
2937
  allowWheelRotation,
2887
- controlsOrientation,
2888
2938
  sizeMetric: isArtistFiltered || !canSizeByViews ? "songs" : deferredSizeMetric,
2889
2939
  busy,
2890
- floatingControlsLeading: floatingControlsLeading || canSizeByViews ? /* @__PURE__ */ (0, import_jsx_runtime38.jsxs)("div", { className: "flex items-center gap-2", children: [
2940
+ floatingControlsLeading: floatingControlsLeading || canSizeByViews ? /* @__PURE__ */ (0, import_jsx_runtime39.jsxs)("div", { className: "flex items-center gap-2", children: [
2891
2941
  floatingControlsLeading,
2892
- canSizeByViews && /* @__PURE__ */ (0, import_jsx_runtime38.jsx)(SizeMetricToggle, { value: sizeMetric, onChange: changeSizeMetric })
2942
+ canSizeByViews && /* @__PURE__ */ (0, import_jsx_runtime39.jsx)(SizeMetricToggle, { value: sizeMetric, onChange: changeSizeMetric })
2893
2943
  ] }) : void 0,
2894
2944
  defaultViewport,
2945
+ viewportRestore,
2895
2946
  onViewportChange,
2896
2947
  ...infoPanelCuration
2897
2948
  }
2898
2949
  ) }) })
2899
2950
  ) : viewMode === "pop-core" ? (
2900
2951
  // Same as the wheel: the header inset extends its fill behind the floating search bar, Google Maps style.
2901
- /* @__PURE__ */ (0, import_jsx_runtime38.jsx)("div", { className: "tree-container search-overlay relative flex-1 w-full min-h-0 [--gtv-info-panel-header-inset:calc(var(--layout-search-h,2.5rem)+var(--layout-panel-inset,0.5rem))] [&_.gtv-info-panel-header]:pb-2 [&_.gtv-info-panel-title--hidden]:h-0 [&_.gtv-toolbar]:hidden [&_.gtv-info-panel-zoom-in]:hidden", children: /* @__PURE__ */ (0, import_jsx_runtime38.jsx)(GenreTreeWheelHandoff, { skeleton: /* @__PURE__ */ (0, import_jsx_runtime38.jsx)(import_genre_tree_view4.GenreTreeWheelSkeleton, {}), onRevealedChange: setIsWheelRevealed, children: /* @__PURE__ */ (0, import_jsx_runtime38.jsx)(
2952
+ /* @__PURE__ */ (0, import_jsx_runtime39.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_runtime39.jsx)(GenreTreeWheelHandoff, { skeleton: /* @__PURE__ */ (0, import_jsx_runtime39.jsx)(import_genre_tree_view4.GenreTreeWheelSkeleton, {}), onRevealedChange: setIsWheelRevealed, children: /* @__PURE__ */ (0, import_jsx_runtime39.jsx)(
2902
2953
  GenrePlaylistTreeWheelRadialPopCore,
2903
2954
  {
2904
2955
  scope,
@@ -2915,20 +2966,20 @@ function GenreTreeView({
2915
2966
  selectedNodeId,
2916
2967
  readOnly,
2917
2968
  allowWheelRotation,
2918
- controlsOrientation,
2919
2969
  sizeMetric: isArtistFiltered || !canSizeByViews ? "songs" : deferredSizeMetric,
2920
2970
  busy,
2921
- floatingControlsLeading: floatingControlsLeading || canSizeByViews ? /* @__PURE__ */ (0, import_jsx_runtime38.jsxs)("div", { className: "flex items-center gap-2", children: [
2971
+ floatingControlsLeading: floatingControlsLeading || canSizeByViews ? /* @__PURE__ */ (0, import_jsx_runtime39.jsxs)("div", { className: "flex items-center gap-2", children: [
2922
2972
  floatingControlsLeading,
2923
- canSizeByViews && /* @__PURE__ */ (0, import_jsx_runtime38.jsx)(SizeMetricToggle, { value: sizeMetric, onChange: changeSizeMetric })
2973
+ canSizeByViews && /* @__PURE__ */ (0, import_jsx_runtime39.jsx)(SizeMetricToggle, { value: sizeMetric, onChange: changeSizeMetric })
2924
2974
  ] }) : void 0,
2925
2975
  defaultViewport,
2976
+ viewportRestore,
2926
2977
  onViewportChange,
2927
2978
  ...infoPanelCuration,
2928
2979
  ...focus
2929
2980
  }
2930
2981
  ) }) })
2931
- ) : viewMode === "outline" ? /* @__PURE__ */ (0, import_jsx_runtime38.jsx)("div", { className: "tree-container flex-1 min-h-0 w-full overflow-y-auto relative [--gtv-info-panel-header-inset:calc(var(--layout-search-h,2.5rem)+var(--layout-panel-inset,0.5rem)+0.75rem)] [&_.gtv-toolbar]:hidden", children: /* @__PURE__ */ (0, import_jsx_runtime38.jsx)(
2982
+ ) : viewMode === "outline" ? /* @__PURE__ */ (0, import_jsx_runtime39.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_runtime39.jsx)(
2932
2983
  GenrePlaylistTreeWheelRadialPopCore,
2933
2984
  {
2934
2985
  outline: true,
@@ -2947,13 +2998,13 @@ function GenreTreeView({
2947
2998
  readOnly,
2948
2999
  ...infoPanelCuration
2949
3000
  }
2950
- ) }) : /* @__PURE__ */ (0, import_jsx_runtime38.jsx)("div", { className: "tree-container relative flex flex-col gap-4 w-full pt-14 overflow-x-auto overflow-y-auto text-[var(--color-text)] [&_.gtv-toolbar]:hidden", children: Object.entries(groupedGenrePlaylistsByRoot).map(
3001
+ ) }) : /* @__PURE__ */ (0, import_jsx_runtime39.jsx)("div", { className: "tree-container relative flex flex-col gap-4 w-full pt-14 overflow-x-auto overflow-y-auto text-[var(--color-text)] [&_.gtv-toolbar]:hidden", children: Object.entries(groupedGenrePlaylistsByRoot).map(
2951
3002
  ([uuid, genrePlaylistTreePerRoot], rootIndex) => {
2952
- return /* @__PURE__ */ (0, import_jsx_runtime38.jsx)(
3003
+ return /* @__PURE__ */ (0, import_jsx_runtime39.jsx)(
2953
3004
  "div",
2954
3005
  {
2955
3006
  className: "tree-per-root-container relative shrink-0 h-[28rem] mt-2 mr-16 p-2 bg-[var(--color-neutral-100)] rounded-lg",
2956
- children: /* @__PURE__ */ (0, import_jsx_runtime38.jsx)("div", { className: "graph-container relative z-10 h-full", children: /* @__PURE__ */ (0, import_jsx_runtime38.jsx)(
3007
+ children: /* @__PURE__ */ (0, import_jsx_runtime39.jsx)("div", { className: "graph-container relative z-10 h-full", children: /* @__PURE__ */ (0, import_jsx_runtime39.jsx)(
2957
3008
  GenrePlaylistTreePerRoot,
2958
3009
  {
2959
3010
  scope,
@@ -2971,8 +3022,8 @@ function GenreTreeView({
2971
3022
  selectedNodeId: uuid === selectedGenrePlaylist?.root.uuid ? selectedNodeId : null,
2972
3023
  readOnly,
2973
3024
  wheelZoom: "modifier",
2974
- controlsOrientation,
2975
3025
  defaultViewport: uuid === selectedGenrePlaylist?.root.uuid ? defaultViewport : void 0,
3026
+ viewportRestore: uuid === selectedGenrePlaylist?.root.uuid && uuid === restoreRootUuid ? viewportRestore : void 0,
2976
3027
  onViewportChange: uuid === selectedGenrePlaylist?.root.uuid ? onViewportChange : void 0,
2977
3028
  ...infoPanelCuration,
2978
3029
  overlayPanel: rootIndex === 0 ? activeOverlayPanel : void 0
@@ -2989,17 +3040,126 @@ function GenreTreeView({
2989
3040
  );
2990
3041
  }
2991
3042
 
2992
- // src/genre-tree/index.ts
3043
+ // src/genre-tree/GenreDetailPanel.tsx
3044
+ var import_react16 = require("react");
2993
3045
  var import_genre_tree_view5 = require("@behindthemusictree/genre-tree-view");
3046
+ var import_ui17 = require("@behindthemusictree/ui");
3047
+ var import_genre_tree18 = require("@behindthemusictree/app-kit/genre-tree");
3048
+ var import_jsx_runtime40 = require("react/jsx-runtime");
3049
+ var ITEM_COUNT_LABEL = "Songs";
3050
+ var RELATIVE_ITEM_COUNT = 1;
3051
+ function toNode(genre, parentId) {
3052
+ return {
3053
+ id: genre.uuid,
3054
+ parentId,
3055
+ name: genre.name,
3056
+ itemCount: RELATIVE_ITEM_COUNT,
3057
+ side: genre.side ?? void 0,
3058
+ wikidataId: genre.wikidataId
3059
+ };
3060
+ }
3061
+ function GenreDetailPanel({
3062
+ genreUuid,
3063
+ scope,
3064
+ getBackendBaseUrl,
3065
+ onClose,
3066
+ onSelectGenre,
3067
+ onZoomIn,
3068
+ shareUrl,
3069
+ shareTarget,
3070
+ collapsed,
3071
+ expanded,
3072
+ onExpandedChange,
3073
+ extras
3074
+ }) {
3075
+ const { data: detail, isPending, isError } = (0, import_genre_tree18.useFetchGenreDetail)(genreUuid, scope, getBackendBaseUrl);
3076
+ const playback = useGenrePlaylistPlayback({ scope, genrePlaylists: [] });
3077
+ const view = (0, import_react16.useMemo)(() => {
3078
+ if (!detail) return null;
3079
+ const rootColor = (0, import_genre_tree_view5.getGenreTreeColor)(detail.root.wikidataId);
3080
+ const relative = (genre, parentId) => ({
3081
+ node: toNode(genre, parentId),
3082
+ fill: (0, import_genre_tree_view5.tintSurface)(rootColor),
3083
+ textColor: import_genre_tree_view5.TEXT_COLOR
3084
+ });
3085
+ const ancestors = [...detail.ascendants].sort((a, b) => b.degree - a.degree);
3086
+ return {
3087
+ node: {
3088
+ id: detail.uuid,
3089
+ parentId: detail.parent?.uuid ?? null,
3090
+ name: detail.name,
3091
+ itemCount: detail.tracksCount,
3092
+ wikidataId: detail.wikidataId
3093
+ },
3094
+ fill: rootColor,
3095
+ parentNode: detail.parent ? relative(detail.parent, null) : null,
3096
+ childNodes: detail.children.map((child) => relative(child, detail.uuid)),
3097
+ ancestorNodes: ancestors.map(
3098
+ ({ ascendant }, index) => relative(ascendant, index === 0 ? null : ancestors[index - 1].ascendant.uuid)
3099
+ )
3100
+ };
3101
+ }, [detail]);
3102
+ if (isError) {
3103
+ return /* @__PURE__ */ (0, import_jsx_runtime40.jsx)("div", { role: "alert", className: "p-4 text-sm text-[var(--color-text)]", children: "This genre could not be loaded." });
3104
+ }
3105
+ if (isPending || !detail || !view) {
3106
+ return /* @__PURE__ */ (0, import_jsx_runtime40.jsxs)("div", { className: "flex flex-col gap-3 p-4", "aria-busy": "true", "data-testid": "genre-detail-panel-skeleton", children: [
3107
+ /* @__PURE__ */ (0, import_jsx_runtime40.jsx)(import_ui17.Skeleton, { className: "h-6 w-2/3" }),
3108
+ /* @__PURE__ */ (0, import_jsx_runtime40.jsx)(import_ui17.Skeleton, { className: "h-4 w-full" }),
3109
+ /* @__PURE__ */ (0, import_jsx_runtime40.jsx)(import_ui17.Skeleton, { className: "h-4 w-3/4" })
3110
+ ] });
3111
+ }
3112
+ const renderBody = (node, sections) => ({
3113
+ primary: /* @__PURE__ */ (0, import_jsx_runtime40.jsx)(
3114
+ GenreInfoPanelBody,
3115
+ {
3116
+ toolbar: sections.toolbar,
3117
+ family: sections.family,
3118
+ genreUuid: detail.uuid,
3119
+ genrePlaylist: { ...detail.criteriaPlaylist, tracksCount: detail.tracksCount },
3120
+ wikidataId: detail.wikidataId,
3121
+ scope,
3122
+ getBackendBaseUrl,
3123
+ playback,
3124
+ band: { node, onZoomIn, shareUrl, shareTarget: shareUrl ? shareTarget : void 0 },
3125
+ extras
3126
+ }
3127
+ ),
3128
+ aside: null
3129
+ });
3130
+ return /* @__PURE__ */ (0, import_jsx_runtime40.jsx)(
3131
+ import_genre_tree_view5.InfoPanel,
3132
+ {
3133
+ node: view.node,
3134
+ fill: view.fill,
3135
+ textColor: import_genre_tree_view5.ACCENT_TEXT_COLOR,
3136
+ parentNode: view.parentNode,
3137
+ childNodes: view.childNodes,
3138
+ ancestorNodes: view.ancestorNodes,
3139
+ side: "left",
3140
+ onClose,
3141
+ onSelectNode: (nodeId) => onSelectGenre(nodeId),
3142
+ itemCountLabel: ITEM_COUNT_LABEL,
3143
+ renderInfoPanelBody: renderBody,
3144
+ expanded,
3145
+ onExpandedChange,
3146
+ collapsed,
3147
+ hideExpandButton: onExpandedChange === void 0
3148
+ }
3149
+ );
3150
+ }
3151
+
3152
+ // src/genre-tree/index.ts
3153
+ var import_genre_tree_view6 = require("@behindthemusictree/genre-tree-view");
2994
3154
 
2995
3155
  // src/genre-tree/track-list-sidebar/TrackListSidebar.tsx
2996
- var import_react14 = require("react");
3156
+ var import_react17 = require("react");
2997
3157
  var import_lucide_react16 = require("lucide-react");
2998
- var import_genre_tree18 = require("@behindthemusictree/app-kit/genre-tree");
2999
3158
  var import_genre_tree19 = require("@behindthemusictree/app-kit/genre-tree");
3159
+ var import_genre_tree20 = require("@behindthemusictree/app-kit/genre-tree");
3000
3160
  var import_track_noun7 = require("@behindthemusictree/app-kit/track-noun");
3001
- var import_jsx_runtime39 = require("react/jsx-runtime");
3002
- var useBrowserLayoutEffect = typeof window === "undefined" ? import_react14.useEffect : import_react14.useLayoutEffect;
3161
+ var import_jsx_runtime41 = require("react/jsx-runtime");
3162
+ var useBrowserLayoutEffect = typeof window === "undefined" ? import_react17.useEffect : import_react17.useLayoutEffect;
3003
3163
  function TrackListSidebar({
3004
3164
  className: className2 = "",
3005
3165
  renderDuration,
@@ -3010,18 +3170,18 @@ function TrackListSidebar({
3010
3170
  onMinimize,
3011
3171
  renderArtistLink
3012
3172
  }) {
3013
- const { trackList, loadMore, autoAdvanceScrollRequest } = (0, import_genre_tree18.useTrackList)();
3173
+ const { trackList, loadMore, autoAdvanceScrollRequest } = (0, import_genre_tree19.useTrackList)();
3014
3174
  const trackNoun = (0, import_track_noun7.useTrackNoun)();
3015
- const trackListRef = (0, import_react14.useRef)(null);
3016
- const previousOriginRef = (0, import_react14.useRef)(trackList?.origin ?? null);
3017
- const handledAutoAdvanceRequestIdRef = (0, import_react14.useRef)(null);
3018
- const shouldFollowPlaybackRef = (0, import_react14.useRef)(true);
3019
- const programmaticScrollTopRef = (0, import_react14.useRef)(null);
3020
- const userScrollIntentRef = (0, import_react14.useRef)(false);
3021
- const userScrollIntentTimerRef = (0, import_react14.useRef)(null);
3022
- const loadMoreSentinelRef = (0, import_react14.useRef)(null);
3175
+ const trackListRef = (0, import_react17.useRef)(null);
3176
+ const previousOriginRef = (0, import_react17.useRef)(trackList?.origin ?? null);
3177
+ const handledAutoAdvanceRequestIdRef = (0, import_react17.useRef)(null);
3178
+ const shouldFollowPlaybackRef = (0, import_react17.useRef)(true);
3179
+ const programmaticScrollTopRef = (0, import_react17.useRef)(null);
3180
+ const userScrollIntentRef = (0, import_react17.useRef)(false);
3181
+ const userScrollIntentTimerRef = (0, import_react17.useRef)(null);
3182
+ const loadMoreSentinelRef = (0, import_react17.useRef)(null);
3023
3183
  const hasMore = !!trackList && trackList.nextPage !== null;
3024
- (0, import_react14.useEffect)(() => {
3184
+ (0, import_react17.useEffect)(() => {
3025
3185
  const sentinel = loadMoreSentinelRef.current;
3026
3186
  if (!hasMore || !sentinel) return;
3027
3187
  const observer = new IntersectionObserver((entries) => {
@@ -3041,11 +3201,11 @@ function TrackListSidebar({
3041
3201
  programmaticScrollTopRef.current = null;
3042
3202
  if (trackListRef.current) trackListRef.current.scrollTop = 0;
3043
3203
  }, [trackList?.origin]);
3044
- (0, import_react14.useEffect)(() => {
3204
+ (0, import_react17.useEffect)(() => {
3045
3205
  const list = trackListRef.current;
3046
3206
  if (!isCollapsed && list?.scrollTop === 0) shouldFollowPlaybackRef.current = true;
3047
3207
  }, [isCollapsed]);
3048
- (0, import_react14.useEffect)(
3208
+ (0, import_react17.useEffect)(
3049
3209
  () => () => {
3050
3210
  if (userScrollIntentTimerRef.current !== null) clearTimeout(userScrollIntentTimerRef.current);
3051
3211
  },
@@ -3099,25 +3259,25 @@ function TrackListSidebar({
3099
3259
  }
3100
3260
  };
3101
3261
  const positionClasses = layout === "inline" ? "relative w-full flex h-full flex-col" : "fixed right-0 w-[50rem] flex flex-col";
3102
- return trackList ? /* @__PURE__ */ (0, import_jsx_runtime39.jsxs)(
3262
+ return trackList ? /* @__PURE__ */ (0, import_jsx_runtime41.jsxs)(
3103
3263
  "div",
3104
3264
  {
3105
3265
  "data-theme": "dark",
3106
3266
  className: `track-list-sidebar ${positionClasses} rounded-2xl bg-[var(--color-surface)] pb-1 ${className2}`,
3107
3267
  style: layout === "fixed" ? { top: "63.5px", maxHeight: "calc(100vh - 63.5px - 79px)" } : void 0,
3108
3268
  children: [
3109
- /* @__PURE__ */ (0, import_jsx_runtime39.jsxs)("div", { className: "header flex h-16 items-center px-4 py-2 text-[var(--color-text)]", children: [
3110
- /* @__PURE__ */ (0, import_jsx_runtime39.jsxs)("div", { className: "min-w-0 flex flex-1 flex-col justify-center", children: [
3111
- /* @__PURE__ */ (0, import_jsx_runtime39.jsxs)("div", { className: "origin flex text-xl", children: [
3112
- /* @__PURE__ */ (0, import_jsx_runtime39.jsx)("div", { className: "from h-auto flex flex-col justify-center items-center mr-2", children: "From" }),
3113
- /* @__PURE__ */ (0, import_jsx_runtime39.jsx)("div", { className: "name-container flex flex-col justify-center items-center text-[var(--color-text)] font-bold pr-2 max-w-[27.5rem]", children: /* @__PURE__ */ (0, import_jsx_runtime39.jsx)("div", { className: "name max-w-full truncate", children: trackList.origin.label }) })
3269
+ /* @__PURE__ */ (0, import_jsx_runtime41.jsxs)("div", { className: "header flex h-16 items-center px-4 py-2 text-[var(--color-text)]", children: [
3270
+ /* @__PURE__ */ (0, import_jsx_runtime41.jsxs)("div", { className: "min-w-0 flex flex-1 flex-col justify-center", children: [
3271
+ /* @__PURE__ */ (0, import_jsx_runtime41.jsxs)("div", { className: "origin flex text-xl", children: [
3272
+ /* @__PURE__ */ (0, import_jsx_runtime41.jsx)("div", { className: "from h-auto flex flex-col justify-center items-center mr-2", children: "From" }),
3273
+ /* @__PURE__ */ (0, import_jsx_runtime41.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_runtime41.jsx)("div", { className: "name max-w-full truncate", children: trackList.origin.label }) })
3114
3274
  ] }),
3115
- /* @__PURE__ */ (0, import_jsx_runtime39.jsxs)("div", { className: "info flex items-center text-sm text-[var(--color-text-muted)]", children: [
3116
- trackList.origin.type === import_genre_tree19.TrackListOriginType.GENRE_PLAYLIST ? "Genre \u2022 " : trackList.origin.type === import_genre_tree19.TrackListOriginType.ARTIST ? "Artist \u2022 " : `${trackNoun.singular} playlist \u2022 `,
3275
+ /* @__PURE__ */ (0, import_jsx_runtime41.jsxs)("div", { className: "info flex items-center text-sm text-[var(--color-text-muted)]", children: [
3276
+ 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 `,
3117
3277
  `${trackList.total} ${trackList.total > 1 ? trackNoun.plural : trackNoun.singular}`
3118
3278
  ] })
3119
3279
  ] }),
3120
- (isCollapsed && onExpand || !isCollapsed && onMinimize) && /* @__PURE__ */ (0, import_jsx_runtime39.jsx)(
3280
+ (isCollapsed && onExpand || !isCollapsed && onMinimize) && /* @__PURE__ */ (0, import_jsx_runtime41.jsx)(
3121
3281
  "button",
3122
3282
  {
3123
3283
  type: "button",
@@ -3126,11 +3286,11 @@ function TrackListSidebar({
3126
3286
  "aria-expanded": !isCollapsed,
3127
3287
  className: `ml-2 flex h-8 w-8 shrink-0 items-center justify-center rounded-md hover:bg-[var(--color-surface-muted)] ${isCollapsed ? "text-[var(--color-text-muted)]" : "text-white"}`,
3128
3288
  onClick: isCollapsed ? onExpand : onMinimize,
3129
- children: /* @__PURE__ */ (0, import_jsx_runtime39.jsx)(import_lucide_react16.ListMusic, { "aria-hidden": "true", className: "h-5 w-5" })
3289
+ children: /* @__PURE__ */ (0, import_jsx_runtime41.jsx)(import_lucide_react16.ListMusic, { "aria-hidden": "true", className: "h-5 w-5" })
3130
3290
  }
3131
3291
  )
3132
3292
  ] }),
3133
- !isCollapsed && /* @__PURE__ */ (0, import_jsx_runtime39.jsxs)(
3293
+ !isCollapsed && /* @__PURE__ */ (0, import_jsx_runtime41.jsxs)(
3134
3294
  "ul",
3135
3295
  {
3136
3296
  ref: trackListRef,
@@ -3140,7 +3300,7 @@ function TrackListSidebar({
3140
3300
  onPointerDown: markUserScrollIntent,
3141
3301
  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",
3142
3302
  children: [
3143
- trackList.tracks.map((track, index) => /* @__PURE__ */ (0, import_jsx_runtime39.jsx)("li", { "data-track-uuid": track.uuid, children: /* @__PURE__ */ (0, import_jsx_runtime39.jsx)(
3303
+ trackList.tracks.map((track, index) => /* @__PURE__ */ (0, import_jsx_runtime41.jsx)("li", { "data-track-uuid": track.uuid, children: /* @__PURE__ */ (0, import_jsx_runtime41.jsx)(
3144
3304
  TrackItem,
3145
3305
  {
3146
3306
  track,
@@ -3150,7 +3310,7 @@ function TrackListSidebar({
3150
3310
  renderArtistLink
3151
3311
  }
3152
3312
  ) }, track.uuid)),
3153
- hasMore && /* @__PURE__ */ (0, import_jsx_runtime39.jsx)("li", { ref: loadMoreSentinelRef, "aria-hidden": "true", className: "h-px" })
3313
+ hasMore && /* @__PURE__ */ (0, import_jsx_runtime41.jsx)("li", { ref: loadMoreSentinelRef, "aria-hidden": "true", className: "h-px" })
3154
3314
  ]
3155
3315
  }
3156
3316
  )
@@ -3160,16 +3320,16 @@ function TrackListSidebar({
3160
3320
  }
3161
3321
 
3162
3322
  // src/genre-tree/PlayerPanel.tsx
3163
- var import_react15 = require("react");
3323
+ var import_react18 = require("react");
3164
3324
  var import_tailwind_merge2 = require("tailwind-merge");
3165
3325
  var import_player12 = require("@behindthemusictree/app-kit/player");
3166
- var import_genre_tree21 = require("@behindthemusictree/app-kit/genre-tree");
3326
+ var import_genre_tree22 = require("@behindthemusictree/app-kit/genre-tree");
3167
3327
 
3168
3328
  // src/genre-tree/PlayerControls.tsx
3169
3329
  var import_lucide_react17 = require("lucide-react");
3170
- var import_genre_tree20 = require("@behindthemusictree/app-kit/genre-tree");
3330
+ var import_genre_tree21 = require("@behindthemusictree/app-kit/genre-tree");
3171
3331
  var import_player11 = require("@behindthemusictree/app-kit/player");
3172
- var import_jsx_runtime40 = require("react/jsx-runtime");
3332
+ var import_jsx_runtime42 = require("react/jsx-runtime");
3173
3333
  function PlayerControls() {
3174
3334
  const {
3175
3335
  duration,
@@ -3181,22 +3341,22 @@ function PlayerControls() {
3181
3341
  setVolume,
3182
3342
  volume
3183
3343
  } = (0, import_player11.usePlayer)();
3184
- const { hasNextTrack, hasPreviousTrack, playNextTrack, playPreviousTrack } = (0, import_genre_tree20.useTrackList)();
3344
+ const { hasNextTrack, hasPreviousTrack, playNextTrack, playPreviousTrack } = (0, import_genre_tree21.useTrackList)();
3185
3345
  const currentTime = (0, import_player11.useCurrentTime)();
3186
3346
  const isBusy = isLoading || playState === import_player11.PlayStates.LOADING;
3187
3347
  const isReady = !!playerTrackObject?.isReady && !isBusy && !playerTrackObject.loadError;
3188
3348
  const safeDuration = Number.isFinite(duration) && duration > 0 ? duration : 0;
3189
3349
  const safeCurrentTime = Number.isFinite(currentTime) ? Math.max(0, Math.min(currentTime, safeDuration)) : 0;
3190
3350
  const isPlaying = playState === import_player11.PlayStates.PLAYING;
3191
- return /* @__PURE__ */ (0, import_jsx_runtime40.jsxs)(
3351
+ return /* @__PURE__ */ (0, import_jsx_runtime42.jsxs)(
3192
3352
  "section",
3193
3353
  {
3194
3354
  "aria-label": "Player controls",
3195
3355
  className: "player-controls flex shrink-0 flex-col gap-2 border-b border-zinc-800 px-3 py-2 text-zinc-200",
3196
3356
  children: [
3197
- /* @__PURE__ */ (0, import_jsx_runtime40.jsxs)("div", { className: "flex min-w-0 items-center gap-2 text-xs tabular-nums text-zinc-400", children: [
3198
- /* @__PURE__ */ (0, import_jsx_runtime40.jsx)("span", { "aria-label": "Elapsed time", children: (0, import_genre_tree20.formatTime)(safeCurrentTime) }),
3199
- /* @__PURE__ */ (0, import_jsx_runtime40.jsx)(
3357
+ /* @__PURE__ */ (0, import_jsx_runtime42.jsxs)("div", { className: "flex min-w-0 items-center gap-2 text-xs tabular-nums text-zinc-400", children: [
3358
+ /* @__PURE__ */ (0, import_jsx_runtime42.jsx)("span", { "aria-label": "Elapsed time", children: (0, import_genre_tree21.formatTime)(safeCurrentTime) }),
3359
+ /* @__PURE__ */ (0, import_jsx_runtime42.jsx)(
3200
3360
  "input",
3201
3361
  {
3202
3362
  "aria-label": "Seek",
@@ -3210,11 +3370,11 @@ function PlayerControls() {
3210
3370
  onChange: (event) => seekTo(Number(event.currentTarget.value))
3211
3371
  }
3212
3372
  ),
3213
- /* @__PURE__ */ (0, import_jsx_runtime40.jsx)("span", { "aria-label": "Duration", children: (0, import_genre_tree20.formatTime)(safeDuration) })
3373
+ /* @__PURE__ */ (0, import_jsx_runtime42.jsx)("span", { "aria-label": "Duration", children: (0, import_genre_tree21.formatTime)(safeDuration) })
3214
3374
  ] }),
3215
- /* @__PURE__ */ (0, import_jsx_runtime40.jsxs)("div", { className: "grid min-h-9 grid-cols-[1fr_auto_1fr] items-center gap-3", children: [
3216
- /* @__PURE__ */ (0, import_jsx_runtime40.jsxs)("div", { className: "col-start-2 flex items-center justify-center gap-2", children: [
3217
- /* @__PURE__ */ (0, import_jsx_runtime40.jsx)(
3375
+ /* @__PURE__ */ (0, import_jsx_runtime42.jsxs)("div", { className: "grid min-h-9 grid-cols-[1fr_auto_1fr] items-center gap-3", children: [
3376
+ /* @__PURE__ */ (0, import_jsx_runtime42.jsxs)("div", { className: "col-start-2 flex items-center justify-center gap-2", children: [
3377
+ /* @__PURE__ */ (0, import_jsx_runtime42.jsx)(
3218
3378
  "button",
3219
3379
  {
3220
3380
  type: "button",
@@ -3223,10 +3383,10 @@ function PlayerControls() {
3223
3383
  disabled: !isReady || !hasPreviousTrack,
3224
3384
  onClick: playPreviousTrack,
3225
3385
  className: "flex h-8 w-8 shrink-0 items-center justify-center rounded-full text-zinc-300 hover:bg-zinc-800 hover:text-white disabled:cursor-not-allowed disabled:opacity-40",
3226
- children: /* @__PURE__ */ (0, import_jsx_runtime40.jsx)(import_lucide_react17.SkipBack, { "aria-hidden": "true", className: "h-4 w-4" })
3386
+ children: /* @__PURE__ */ (0, import_jsx_runtime42.jsx)(import_lucide_react17.SkipBack, { "aria-hidden": "true", className: "h-4 w-4" })
3227
3387
  }
3228
3388
  ),
3229
- /* @__PURE__ */ (0, import_jsx_runtime40.jsx)(
3389
+ /* @__PURE__ */ (0, import_jsx_runtime42.jsx)(
3230
3390
  "button",
3231
3391
  {
3232
3392
  type: "button",
@@ -3235,10 +3395,10 @@ function PlayerControls() {
3235
3395
  disabled: !isReady,
3236
3396
  onClick: handlePlayPauseAction,
3237
3397
  className: "flex h-9 w-9 shrink-0 items-center justify-center rounded-full bg-white text-zinc-950 hover:bg-zinc-200 disabled:cursor-not-allowed disabled:opacity-40",
3238
- children: isPlaying ? /* @__PURE__ */ (0, import_jsx_runtime40.jsx)(import_lucide_react17.Pause, { "aria-hidden": "true", className: "h-4 w-4", fill: "currentColor" }) : /* @__PURE__ */ (0, import_jsx_runtime40.jsx)(import_lucide_react17.Play, { "aria-hidden": "true", className: "h-4 w-4", fill: "currentColor" })
3398
+ children: isPlaying ? /* @__PURE__ */ (0, import_jsx_runtime42.jsx)(import_lucide_react17.Pause, { "aria-hidden": "true", className: "h-4 w-4", fill: "currentColor" }) : /* @__PURE__ */ (0, import_jsx_runtime42.jsx)(import_lucide_react17.Play, { "aria-hidden": "true", className: "h-4 w-4", fill: "currentColor" })
3239
3399
  }
3240
3400
  ),
3241
- /* @__PURE__ */ (0, import_jsx_runtime40.jsx)(
3401
+ /* @__PURE__ */ (0, import_jsx_runtime42.jsx)(
3242
3402
  "button",
3243
3403
  {
3244
3404
  type: "button",
@@ -3247,14 +3407,14 @@ function PlayerControls() {
3247
3407
  disabled: !isReady || !hasNextTrack,
3248
3408
  onClick: playNextTrack,
3249
3409
  className: "flex h-8 w-8 shrink-0 items-center justify-center rounded-full text-zinc-300 hover:bg-zinc-800 hover:text-white disabled:cursor-not-allowed disabled:opacity-40",
3250
- children: /* @__PURE__ */ (0, import_jsx_runtime40.jsx)(import_lucide_react17.SkipForward, { "aria-hidden": "true", className: "h-4 w-4" })
3410
+ children: /* @__PURE__ */ (0, import_jsx_runtime42.jsx)(import_lucide_react17.SkipForward, { "aria-hidden": "true", className: "h-4 w-4" })
3251
3411
  }
3252
3412
  )
3253
3413
  ] }),
3254
- /* @__PURE__ */ (0, import_jsx_runtime40.jsxs)("label", { className: "col-start-3 flex w-[4.2rem] shrink-0 items-center gap-[0.35rem] justify-self-end text-zinc-400 sm:w-[4.9rem]", children: [
3255
- volume === 0 ? /* @__PURE__ */ (0, import_jsx_runtime40.jsx)(import_lucide_react17.VolumeX, { "aria-hidden": "true", className: "h-[0.7rem] w-[0.7rem] shrink-0" }) : /* @__PURE__ */ (0, import_jsx_runtime40.jsx)(import_lucide_react17.Volume2, { "aria-hidden": "true", className: "h-[0.7rem] w-[0.7rem] shrink-0" }),
3256
- /* @__PURE__ */ (0, import_jsx_runtime40.jsx)("span", { className: "sr-only", children: "Volume" }),
3257
- /* @__PURE__ */ (0, import_jsx_runtime40.jsx)(
3414
+ /* @__PURE__ */ (0, import_jsx_runtime42.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: [
3415
+ volume === 0 ? /* @__PURE__ */ (0, import_jsx_runtime42.jsx)(import_lucide_react17.VolumeX, { "aria-hidden": "true", className: "h-[0.7rem] w-[0.7rem] shrink-0" }) : /* @__PURE__ */ (0, import_jsx_runtime42.jsx)(import_lucide_react17.Volume2, { "aria-hidden": "true", className: "h-[0.7rem] w-[0.7rem] shrink-0" }),
3416
+ /* @__PURE__ */ (0, import_jsx_runtime42.jsx)("span", { className: "sr-only", children: "Volume" }),
3417
+ /* @__PURE__ */ (0, import_jsx_runtime42.jsx)(
3258
3418
  "input",
3259
3419
  {
3260
3420
  "aria-label": "Volume",
@@ -3276,7 +3436,7 @@ function PlayerControls() {
3276
3436
  }
3277
3437
 
3278
3438
  // src/genre-tree/PlayerPanel.tsx
3279
- var import_jsx_runtime41 = require("react/jsx-runtime");
3439
+ var import_jsx_runtime43 = require("react/jsx-runtime");
3280
3440
  var PANEL_ID = "player-panel";
3281
3441
  var DEFAULT_EXTENDED_WIDTH = "calc(100vw - var(--layout-center-left) - var(--layout-gutter))";
3282
3442
  function PlayerPanel({
@@ -3288,13 +3448,13 @@ function PlayerPanel({
3288
3448
  renderArtistLink
3289
3449
  }) {
3290
3450
  const { playerTrackObject } = (0, import_player12.usePlayer)();
3291
- const { toggleTrackListSidebar, isTrackListSidebarVisible } = (0, import_genre_tree21.useTrackListSidebarVisibility)();
3292
- const [lastLoadedTrackTitle, setLastLoadedTrackTitle] = (0, import_react15.useState)("");
3451
+ const { toggleTrackListSidebar, isTrackListSidebarVisible } = (0, import_genre_tree22.useTrackListSidebarVisibility)();
3452
+ const [lastLoadedTrackTitle, setLastLoadedTrackTitle] = (0, import_react18.useState)("");
3293
3453
  const loadedTrackTitle = playerTrackObject?.track.title;
3294
- (0, import_react15.useEffect)(() => {
3454
+ (0, import_react18.useEffect)(() => {
3295
3455
  if (loadedTrackTitle) setLastLoadedTrackTitle(loadedTrackTitle);
3296
3456
  }, [loadedTrackTitle]);
3297
- const [uncontrolledExtended, setUncontrolledExtended] = (0, import_react15.useState)(false);
3457
+ const [uncontrolledExtended, setUncontrolledExtended] = (0, import_react18.useState)(false);
3298
3458
  const isExtendRequested = expanded ?? uncontrolledExtended;
3299
3459
  const setIsExtendRequested = (next) => {
3300
3460
  setUncontrolledExtended(next);
@@ -3315,7 +3475,7 @@ function PlayerPanel({
3315
3475
  onClick: () => setIsExtendRequested(true),
3316
3476
  expanded: false
3317
3477
  } : null;
3318
- return /* @__PURE__ */ (0, import_jsx_runtime41.jsxs)(
3478
+ return /* @__PURE__ */ (0, import_jsx_runtime43.jsxs)(
3319
3479
  "div",
3320
3480
  {
3321
3481
  id: PANEL_ID,
@@ -3328,7 +3488,7 @@ function PlayerPanel({
3328
3488
  ),
3329
3489
  style: isExtended ? { width: extendedWidth } : void 0,
3330
3490
  children: [
3331
- /* @__PURE__ */ (0, import_jsx_runtime41.jsx)(
3491
+ /* @__PURE__ */ (0, import_jsx_runtime43.jsx)(
3332
3492
  PanelEdgeTab,
3333
3493
  {
3334
3494
  side: "right",
@@ -3338,7 +3498,7 @@ function PlayerPanel({
3338
3498
  className: "absolute right-full top-1/2 -translate-y-1/2"
3339
3499
  }
3340
3500
  ),
3341
- /* @__PURE__ */ (0, import_jsx_runtime41.jsxs)(
3501
+ /* @__PURE__ */ (0, import_jsx_runtime43.jsxs)(
3342
3502
  "div",
3343
3503
  {
3344
3504
  className: (0, import_tailwind_merge2.twMerge)(
@@ -3346,15 +3506,15 @@ function PlayerPanel({
3346
3506
  isExtended ? "flex-row" : "flex-col"
3347
3507
  ),
3348
3508
  children: [
3349
- /* @__PURE__ */ (0, import_jsx_runtime41.jsxs)("div", { className: "flex w-[var(--layout-right-w)] shrink-0 flex-col", children: [
3350
- /* @__PURE__ */ (0, import_jsx_runtime41.jsx)("div", { className: "flex shrink-0 items-center justify-between gap-2 border-b border-zinc-800 p-2", children: /* @__PURE__ */ (0, import_jsx_runtime41.jsx)("span", { className: "min-w-0 truncate text-sm font-semibold", children: loadedTrackTitle || lastLoadedTrackTitle }) }),
3351
- /* @__PURE__ */ (0, import_jsx_runtime41.jsxs)("div", { className: "relative w-full shrink-0", children: [
3352
- /* @__PURE__ */ (0, import_jsx_runtime41.jsx)(import_player12.PlayerVideoSurface, { className: "aspect-video w-full bg-black" }),
3353
- playerTrackObject?.loadError && /* @__PURE__ */ (0, import_jsx_runtime41.jsx)("span", { className: "absolute bottom-2 left-2 right-2 text-xs text-red-400", children: playerTrackObject.loadError })
3509
+ /* @__PURE__ */ (0, import_jsx_runtime43.jsxs)("div", { className: "flex w-[var(--layout-right-w)] shrink-0 flex-col", children: [
3510
+ /* @__PURE__ */ (0, import_jsx_runtime43.jsx)("div", { className: "flex shrink-0 items-center justify-between gap-2 border-b border-zinc-800 p-2", children: /* @__PURE__ */ (0, import_jsx_runtime43.jsx)("span", { className: "min-w-0 truncate text-sm font-semibold", children: loadedTrackTitle || lastLoadedTrackTitle }) }),
3511
+ /* @__PURE__ */ (0, import_jsx_runtime43.jsxs)("div", { className: "relative w-full shrink-0", children: [
3512
+ /* @__PURE__ */ (0, import_jsx_runtime43.jsx)(import_player12.PlayerVideoSurface, { className: "aspect-video w-full bg-black" }),
3513
+ playerTrackObject?.loadError && /* @__PURE__ */ (0, import_jsx_runtime43.jsx)("span", { className: "absolute bottom-2 left-2 right-2 text-xs text-red-400", children: playerTrackObject.loadError })
3354
3514
  ] }),
3355
- /* @__PURE__ */ (0, import_jsx_runtime41.jsx)(PlayerControls, {})
3515
+ /* @__PURE__ */ (0, import_jsx_runtime43.jsx)(PlayerControls, {})
3356
3516
  ] }),
3357
- /* @__PURE__ */ (0, import_jsx_runtime41.jsx)(
3517
+ /* @__PURE__ */ (0, import_jsx_runtime43.jsx)(
3358
3518
  TrackListSidebar,
3359
3519
  {
3360
3520
  layout: "inline",
@@ -3379,33 +3539,33 @@ function PlayerPanel({
3379
3539
  // src/genre-tree/Rating.tsx
3380
3540
  var import_uuid = require("uuid");
3381
3541
  var import_ai = require("react-icons/ai");
3382
- var import_genre_tree22 = require("@behindthemusictree/app-kit/genre-tree");
3383
- var import_jsx_runtime42 = require("react/jsx-runtime");
3542
+ var import_genre_tree23 = require("@behindthemusictree/app-kit/genre-tree");
3543
+ var import_jsx_runtime44 = require("react/jsx-runtime");
3384
3544
  function Rating({ rating, handleChange }) {
3385
3545
  const instanceId = (0, import_uuid.v4)();
3386
- return /* @__PURE__ */ (0, import_jsx_runtime42.jsx)("div", { className: "flex items-center", children: [...Array(6)].map((_, i) => /* @__PURE__ */ (0, import_jsx_runtime42.jsxs)("div", { className: `relative ${i === 0 ? "mr-4" : "mr-1"}`, children: [
3387
- /* @__PURE__ */ (0, import_jsx_runtime42.jsx)(
3546
+ return /* @__PURE__ */ (0, import_jsx_runtime44.jsx)("div", { className: "flex items-center", children: [...Array(6)].map((_, i) => /* @__PURE__ */ (0, import_jsx_runtime44.jsxs)("div", { className: `relative ${i === 0 ? "mr-4" : "mr-1"}`, children: [
3547
+ /* @__PURE__ */ (0, import_jsx_runtime44.jsx)(
3388
3548
  "input",
3389
3549
  {
3390
3550
  id: `star-${instanceId}-${i}`,
3391
3551
  className: "w-6 h-6 opacity-0",
3392
3552
  type: "radio",
3393
3553
  name: "rating",
3394
- value: i === 0 ? import_genre_tree22.FORM_RATING_NULL_VALUE : i,
3554
+ value: i === 0 ? import_genre_tree23.FORM_RATING_NULL_VALUE : i,
3395
3555
  checked: rating === (i === 0 ? null : i),
3396
3556
  onChange: handleChange
3397
3557
  }
3398
3558
  ),
3399
- /* @__PURE__ */ (0, import_jsx_runtime42.jsx)("label", { htmlFor: `star-${instanceId}-${i}`, className: "cursor-pointer", children: i === 0 ? /* @__PURE__ */ (0, import_jsx_runtime42.jsxs)("div", { className: "flex", children: [
3400
- /* @__PURE__ */ (0, import_jsx_runtime42.jsx)(
3559
+ /* @__PURE__ */ (0, import_jsx_runtime44.jsx)("label", { htmlFor: `star-${instanceId}-${i}`, className: "cursor-pointer", children: i === 0 ? /* @__PURE__ */ (0, import_jsx_runtime44.jsxs)("div", { className: "flex", children: [
3560
+ /* @__PURE__ */ (0, import_jsx_runtime44.jsx)(
3401
3561
  import_ai.AiOutlineStar,
3402
3562
  {
3403
- className: `w-7 h-7 absolute top-0 left-0 ${rating === import_genre_tree22.FORM_RATING_NULL_VALUE ? "text-[var(--color-accent-rating)]" : "text-[var(--color-text-muted)]"}`
3563
+ 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)]"}`
3404
3564
  }
3405
3565
  ),
3406
- /* @__PURE__ */ (0, import_jsx_runtime42.jsx)("span", { className: "flex-grow w-8" }),
3566
+ /* @__PURE__ */ (0, import_jsx_runtime44.jsx)("span", { className: "flex-grow w-8" }),
3407
3567
  " "
3408
- ] }) : /* @__PURE__ */ (0, import_jsx_runtime42.jsx)(
3568
+ ] }) : /* @__PURE__ */ (0, import_jsx_runtime44.jsx)(
3409
3569
  import_ai.AiFillStar,
3410
3570
  {
3411
3571
  className: `w-7 h-7 absolute top-0 left-0 ${(rating ?? -1) >= i ? "text-[var(--color-accent-rating)]" : "text-[var(--color-text-muted)]"}`
@@ -3419,6 +3579,9 @@ function Rating({ rating, handleChange }) {
3419
3579
  AuthErrorPopup,
3420
3580
  AuthPopup,
3421
3581
  BasePopup,
3582
+ GenreDetailPanel,
3583
+ GenreInfoPanelBody,
3584
+ GenreNodeActionBand,
3422
3585
  GenrePlaylistTreePerRoot,
3423
3586
  GenreSearch,
3424
3587
  GenreTreeOutlineSkeleton,
@@ -3438,6 +3601,8 @@ function Rating({ rating, handleChange }) {
3438
3601
  TrackItem,
3439
3602
  TrackListSidebar,
3440
3603
  TrackPositionPlayPause,
3441
- TrackUploadPopup
3604
+ TrackUploadPopup,
3605
+ useGenrePlaylistPlayback,
3606
+ useLeftDetailPanelLayout
3442
3607
  });
3443
3608
  //# sourceMappingURL=index.cjs.map