@behindthemusictree/app-kit-ui 37.0.0 → 37.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -488,4 +488,56 @@ interface RatingProps {
488
488
  }
489
489
  declare function Rating({ rating, handleChange }: RatingProps): react.JSX.Element;
490
490
 
491
- export { type ArtistSearchProps, GenreDetailPanel, type GenreDetailPanelProps, type GenreDetailSlots, type GenreFocusProps, GenreInfoPanelBody, type GenreInfoPanelBodyProps, GenreNodeActionBand, type GenrePlaylistPlayback, type GenrePlaylistTracksSlots, GenrePlaylistTreePerRoot, type GenrePlaylistTreePerRootProps, GenreSearch, type GenreSearchExtraGroup, type GenreSearchGroupsProps, type GenreSearchProps, type GenreTreeShareTarget, GenreTreeView, type GenreTreeViewProps, type GetGenreTreeShareUrl, type ParentChange, type PlayableGenrePlaylist, PlayerPanel, type PlayerPanelProps, Rating, type RatingProps, SEARCH_FAILED_STATUS, SearchBox, type SearchBoxProps, type SearchGroup, type SearchGroupItem, TrackItem, type TrackItemProps, TrackListSidebar, type TrackListSidebarProps, TrackPositionPlayPause, type TrackPositionPlayPauseProps, type UseGenrePlaylistPlaybackOptions, type UseGenreRemoteSearchGroupOptions, searchStatus, useArtistSearchGroup, useGenrePlaylistPlayback, useGenreRemoteSearchGroup, useSongSearchGroup };
491
+ /** How the song's genre is opened: through the loaded tree's playlists, or by the genre alone for a host without a tree. */
492
+ type SongGenreNavigation = {
493
+ /** Root-to-leaf playlists of a genre in the loaded tree; empty when the tree doesn't hold it. */
494
+ getGenrePath: (genre: CriteriaMinimum) => CriteriaPlaylistSimple[];
495
+ onSelectGenrePlaylist: (genrePlaylist: CriteriaPlaylistSimple) => void;
496
+ onSelectGenre?: never;
497
+ } | {
498
+ onSelectGenre: (genre: CriteriaMinimum) => void;
499
+ getGenrePath?: never;
500
+ onSelectGenrePlaylist?: never;
501
+ };
502
+
503
+ interface EntityPanelState {
504
+ uuid: string | null;
505
+ expanded: boolean;
506
+ onExpandedChange: (expanded: boolean) => void;
507
+ onClose: () => void;
508
+ }
509
+ type EntityPanelOptions = {
510
+ scope: Scope;
511
+ getBackendBaseUrl: () => string;
512
+ /** Artist requests go here when the host serves them from elsewhere; defaults to `getBackendBaseUrl`. */
513
+ getArtistBackendBaseUrl?: () => string;
514
+ getShareUrl?: GetGenreTreeShareUrl;
515
+ renderArtistLink: RenderArtistLink;
516
+ renderSongLink: RenderSongLink;
517
+ song: EntityPanelState;
518
+ artist: EntityPanelState;
519
+ } & SongGenreNavigation;
520
+ /** The song or artist detail overlay panel; a song takes precedence over an artist, undefined when neither is set. */
521
+ declare function useEntityPanel({ scope, getBackendBaseUrl, getArtistBackendBaseUrl, getShareUrl, renderArtistLink, renderSongLink, song, artist, ...navigation }: EntityPanelOptions): OverlayPanel | undefined;
522
+
523
+ type EntityDetailPanelProps = {
524
+ scope: Scope;
525
+ getBackendBaseUrl: () => string;
526
+ getArtistBackendBaseUrl?: () => string;
527
+ getShareUrl?: GetGenreTreeShareUrl;
528
+ songUuid: string | null;
529
+ onSongUuidChange: (uuid: string | null) => void;
530
+ artistUuid: string | null;
531
+ onArtistUuidChange: (uuid: string | null) => void;
532
+ /** Names render as buttons that open the artist by default. */
533
+ renderArtistLink?: RenderArtistLink;
534
+ /** Titles render as buttons that open the song by default. */
535
+ renderSongLink?: RenderSongLink;
536
+ collapsed?: boolean;
537
+ edgeTab?: ComponentProps<typeof DetailPanel>["edgeTab"];
538
+ } & SongGenreNavigation;
539
+ /** The song or artist detail in the shared left DetailPanel, for a host that has no genre tree. Renders nothing
540
+ * while neither is set; a song takes precedence over an artist. */
541
+ declare function EntityDetailPanel({ scope, getBackendBaseUrl, getArtistBackendBaseUrl, getShareUrl, songUuid, onSongUuidChange, artistUuid, onArtistUuidChange, renderArtistLink, renderSongLink, collapsed, edgeTab, ...navigation }: EntityDetailPanelProps): react.JSX.Element | null;
542
+
543
+ export { type ArtistSearchProps, EntityDetailPanel, type EntityDetailPanelProps, type EntityPanelOptions, type EntityPanelState, GenreDetailPanel, type GenreDetailPanelProps, type GenreDetailSlots, type GenreFocusProps, GenreInfoPanelBody, type GenreInfoPanelBodyProps, GenreNodeActionBand, type GenrePlaylistPlayback, type GenrePlaylistTracksSlots, GenrePlaylistTreePerRoot, type GenrePlaylistTreePerRootProps, GenreSearch, type GenreSearchExtraGroup, type GenreSearchGroupsProps, type GenreSearchProps, type GenreTreeShareTarget, GenreTreeView, type GenreTreeViewProps, type GetGenreTreeShareUrl, type ParentChange, type PlayableGenrePlaylist, PlayerPanel, type PlayerPanelProps, Rating, type RatingProps, SEARCH_FAILED_STATUS, SearchBox, type SearchBoxProps, type SearchGroup, type SearchGroupItem, type SongGenreNavigation, TrackItem, type TrackItemProps, TrackListSidebar, type TrackListSidebarProps, TrackPositionPlayPause, type TrackPositionPlayPauseProps, type UseGenrePlaylistPlaybackOptions, type UseGenreRemoteSearchGroupOptions, searchStatus, useArtistSearchGroup, useEntityPanel, useGenrePlaylistPlayback, useGenreRemoteSearchGroup, useSongSearchGroup };
@@ -1,4 +1,5 @@
1
1
  import {
2
+ EntityDetailPanel,
2
3
  GenreDetailPanel,
3
4
  GenreInfoPanelBody,
4
5
  GenreNodeActionBand,
@@ -18,12 +19,14 @@ import {
18
19
  TrackPositionPlayPause,
19
20
  searchStatus,
20
21
  useArtistSearchGroup,
22
+ useEntityPanel,
21
23
  useGenrePlaylistPlayback,
22
24
  useGenreRemoteSearchGroup,
23
25
  useSongSearchGroup
24
- } from "../chunk-FTYMRCJS.js";
26
+ } from "../chunk-57OI426Y.js";
25
27
  import "../chunk-DKQQLLMY.js";
26
28
  export {
29
+ EntityDetailPanel,
27
30
  GenreDetailPanel,
28
31
  GenreInfoPanelBody,
29
32
  GenreNodeActionBand,
@@ -43,6 +46,7 @@ export {
43
46
  TrackPositionPlayPause,
44
47
  searchStatus,
45
48
  useArtistSearchGroup,
49
+ useEntityPanel,
46
50
  useGenrePlaylistPlayback,
47
51
  useGenreRemoteSearchGroup,
48
52
  useSongSearchGroup
package/dist/index.cjs CHANGED
@@ -24,6 +24,7 @@ __export(src_exports, {
24
24
  AuthErrorPopup: () => AuthErrorPopup,
25
25
  AuthPopup: () => AuthPopup,
26
26
  BasePopup: () => BasePopup,
27
+ EntityDetailPanel: () => EntityDetailPanel,
27
28
  GenreDetailPanel: () => GenreDetailPanel,
28
29
  GenreInfoPanelBody: () => GenreInfoPanelBody,
29
30
  GenreNodeActionBand: () => GenreNodeActionBand,
@@ -50,6 +51,7 @@ __export(src_exports, {
50
51
  TrackUploadPopup: () => TrackUploadPopup,
51
52
  searchStatus: () => searchStatus,
52
53
  useArtistSearchGroup: () => useArtistSearchGroup,
54
+ useEntityPanel: () => useEntityPanel,
53
55
  useGenrePlaylistPlayback: () => useGenrePlaylistPlayback,
54
56
  useGenreRemoteSearchGroup: () => useGenreRemoteSearchGroup,
55
57
  useSongSearchGroup: () => useSongSearchGroup
@@ -2708,15 +2710,8 @@ function SongHistory({ uuid, getBackendBaseUrl }) {
2708
2710
  }
2709
2711
  );
2710
2712
  }
2711
- function SongPanelContent({
2712
- song,
2713
- scope,
2714
- getBackendBaseUrl,
2715
- shareUrl,
2716
- renderArtistLink,
2717
- genrePath,
2718
- onSelectGenre
2719
- }) {
2713
+ function SongPanelContent(props) {
2714
+ const { song, scope, getBackendBaseUrl, shareUrl, renderArtistLink } = props;
2720
2715
  const { playNewTrackListFromTrackUuid } = (0, import_genre_tree26.useTrackList)();
2721
2716
  const { playerTrackObject, playState, handlePlayPauseAction } = (0, import_player11.usePlayer)();
2722
2717
  const unplayableReason = (0, import_genre_tree26.songUnplayableReason)(song);
@@ -2726,12 +2721,14 @@ function SongPanelContent({
2726
2721
  const musicBrainzUrl = (0, import_genre_tree26.musicBrainzRecordingUrl)(song);
2727
2722
  const fields = [];
2728
2723
  if (song.genre) {
2729
- const path = genrePath.length > 0 ? genrePath : null;
2724
+ const path = props.genrePath && props.genrePath.length > 0 ? props.genrePath : null;
2725
+ const { onSelectGenre } = props;
2726
+ const genre = song.genre;
2730
2727
  fields.push({
2731
2728
  label: "Genre",
2732
- value: path ? /* @__PURE__ */ (0, import_jsx_runtime43.jsx)("span", { className: "flex flex-wrap items-center gap-x-1", children: path.map((genrePlaylist, index) => /* @__PURE__ */ (0, import_jsx_runtime43.jsxs)("span", { children: [
2729
+ value: onSelectGenre ? /* @__PURE__ */ (0, import_jsx_runtime43.jsx)("button", { type: "button", onClick: () => onSelectGenre(genre), className: "hover:underline", children: genre.name }) : path ? /* @__PURE__ */ (0, import_jsx_runtime43.jsx)("span", { className: "flex flex-wrap items-center gap-x-1", children: path.map((genrePlaylist, index) => /* @__PURE__ */ (0, import_jsx_runtime43.jsxs)("span", { children: [
2733
2730
  index > 0 && /* @__PURE__ */ (0, import_jsx_runtime43.jsx)("span", { "aria-hidden": "true", children: "\u203A " }),
2734
- /* @__PURE__ */ (0, import_jsx_runtime43.jsx)("button", { type: "button", onClick: () => onSelectGenre(genrePlaylist), className: "hover:underline", children: genrePlaylist.name })
2731
+ /* @__PURE__ */ (0, import_jsx_runtime43.jsx)("button", { type: "button", onClick: () => props.onSelectGenrePlaylist?.(genrePlaylist), className: "hover:underline", children: genrePlaylist.name })
2735
2732
  ] }, genrePlaylist.uuid)) }) : song.genre.name
2736
2733
  });
2737
2734
  } else {
@@ -2797,11 +2794,10 @@ function useSongPanel({
2797
2794
  getBackendBaseUrl,
2798
2795
  getShareUrl,
2799
2796
  renderArtistLink,
2800
- getGenrePath,
2801
- onSelectGenre,
2802
2797
  expanded,
2803
2798
  onExpandedChange,
2804
- onClose
2799
+ onClose,
2800
+ ...navigation
2805
2801
  }) {
2806
2802
  const result = (0, import_genre_tree27.useSong)(uuid, getBackendBaseUrl);
2807
2803
  const panel = { expanded, onExpandedChange, onClose };
@@ -2845,8 +2841,10 @@ function useSongPanel({
2845
2841
  getBackendBaseUrl,
2846
2842
  shareUrl: shareUrl ?? void 0,
2847
2843
  renderArtistLink,
2848
- genrePath: song.genre ? getGenrePath(song.genre) : [],
2849
- onSelectGenre
2844
+ ...navigation.getGenrePath ? {
2845
+ genrePath: song.genre ? navigation.getGenrePath(song.genre) : [],
2846
+ onSelectGenrePlaylist: navigation.onSelectGenrePlaylist
2847
+ } : { onSelectGenre: navigation.onSelectGenre }
2850
2848
  }
2851
2849
  )
2852
2850
  };
@@ -2854,6 +2852,37 @@ function useSongPanel({
2854
2852
  }
2855
2853
  }
2856
2854
 
2855
+ // src/genre-tree/useEntityPanel.tsx
2856
+ function useEntityPanel({
2857
+ scope,
2858
+ getBackendBaseUrl,
2859
+ getArtistBackendBaseUrl,
2860
+ getShareUrl,
2861
+ renderArtistLink,
2862
+ renderSongLink,
2863
+ song,
2864
+ artist,
2865
+ ...navigation
2866
+ }) {
2867
+ const songPanel = useSongPanel({
2868
+ ...song,
2869
+ scope,
2870
+ getBackendBaseUrl,
2871
+ getShareUrl,
2872
+ renderArtistLink,
2873
+ ...navigation
2874
+ });
2875
+ const artistPanel = useArtistPanel({
2876
+ ...artist,
2877
+ scope,
2878
+ getBackendBaseUrl: getArtistBackendBaseUrl ?? getBackendBaseUrl,
2879
+ getShareUrl,
2880
+ renderArtistLink,
2881
+ renderSongLink
2882
+ });
2883
+ return songPanel ?? artistPanel;
2884
+ }
2885
+
2857
2886
  // src/genre-tree/SizeMetricToggle.tsx
2858
2887
  var import_md2 = require("react-icons/md");
2859
2888
  var import_ui19 = require("@behindthemusictree/ui");
@@ -3036,20 +3065,6 @@ function GenreTreeView({
3036
3065
  const [expandedArtistUuid, setExpandedArtistUuid] = (0, import_react18.useState)(null);
3037
3066
  const artistBaseUrl = artistSearchBaseUrl ?? getBackendBaseUrl;
3038
3067
  const songBaseUrl = getBackendBaseUrl;
3039
- const artistPanel = useArtistPanel({
3040
- uuid: artistUuid,
3041
- scope,
3042
- getBackendBaseUrl: artistBaseUrl,
3043
- getShareUrl: getShareUrlRef.current,
3044
- renderArtistLink: renderArtistLink ?? ((artist) => artist.isSpecialPurpose ? artist.name : /* @__PURE__ */ (0, import_jsx_runtime46.jsx)("button", { type: "button", onClick: () => openArtist(artist.uuid), className: "hover:underline", children: artist.name })),
3045
- expanded: controlledArtistExpanded ?? (artistUuid !== null && expandedArtistUuid === artistUuid),
3046
- onExpandedChange: (on) => {
3047
- if (controlledArtistExpanded === void 0) setExpandedArtistUuid(on ? artistUuid : null);
3048
- onArtistExpandedChange?.(on);
3049
- },
3050
- renderSongLink: resolvedRenderSongLink,
3051
- onClose: () => changeArtist(null)
3052
- });
3053
3068
  const resolvedRenderArtistLink = renderArtistLink ?? ((artist) => artist.isSpecialPurpose ? artist.name : /* @__PURE__ */ (0, import_jsx_runtime46.jsx)("button", { type: "button", onClick: () => openArtist(artist.uuid), className: "hover:underline", children: artist.name }));
3054
3069
  const getGenrePath = (genre) => {
3055
3070
  const path = [];
@@ -3061,27 +3076,38 @@ function GenreTreeView({
3061
3076
  }
3062
3077
  return path;
3063
3078
  };
3064
- const songPanel = useSongPanel({
3065
- uuid: songUuid,
3079
+ const entityPanel = useEntityPanel({
3066
3080
  scope,
3067
3081
  getBackendBaseUrl: songBaseUrl,
3082
+ getArtistBackendBaseUrl: artistBaseUrl,
3068
3083
  getShareUrl: getShareUrlRef.current,
3069
3084
  renderArtistLink: resolvedRenderArtistLink,
3085
+ renderSongLink: resolvedRenderSongLink,
3086
+ song: {
3087
+ uuid: songUuid,
3088
+ expanded: controlledSongExpanded ?? (songUuid !== null && expandedSongUuid === songUuid),
3089
+ onExpandedChange: (on) => {
3090
+ if (controlledSongExpanded === void 0) setExpandedSongUuid(on ? songUuid : null);
3091
+ onSongExpandedChange?.(on);
3092
+ },
3093
+ onClose: () => changeSong(null)
3094
+ },
3095
+ artist: {
3096
+ uuid: artistUuid,
3097
+ expanded: controlledArtistExpanded ?? (artistUuid !== null && expandedArtistUuid === artistUuid),
3098
+ onExpandedChange: (on) => {
3099
+ if (controlledArtistExpanded === void 0) setExpandedArtistUuid(on ? artistUuid : null);
3100
+ onArtistExpandedChange?.(on);
3101
+ },
3102
+ onClose: () => changeArtist(null)
3103
+ },
3070
3104
  getGenrePath,
3071
- onSelectGenre: (genrePlaylist2) => {
3105
+ onSelectGenrePlaylist: (genrePlaylist2) => {
3072
3106
  changeSong(null);
3073
3107
  selectGenrePlaylist(genrePlaylist2);
3074
- },
3075
- expanded: controlledSongExpanded ?? (songUuid !== null && expandedSongUuid === songUuid),
3076
- onExpandedChange: (on) => {
3077
- if (controlledSongExpanded === void 0) setExpandedSongUuid(on ? songUuid : null);
3078
- onSongExpandedChange?.(on);
3079
- },
3080
- onClose: () => changeSong(null)
3108
+ }
3081
3109
  });
3082
- const builtInSongPanel = overlayPanel || selectedGenreUuid !== null ? void 0 : songPanel;
3083
- const builtInArtistPanel = overlayPanel || selectedGenreUuid !== null || builtInSongPanel ? void 0 : artistPanel;
3084
- const builtInPanel = builtInSongPanel ?? builtInArtistPanel;
3110
+ const builtInPanel = overlayPanel || selectedGenreUuid !== null ? void 0 : entityPanel;
3085
3111
  const [uncontrolledCollapsed, setUncontrolledCollapsed] = (0, import_react18.useState)(false);
3086
3112
  const [uncontrolledExpanded, setUncontrolledExpanded] = (0, import_react18.useState)(false);
3087
3113
  const isCollapsed = collapsed ?? uncontrolledCollapsed;
@@ -3950,12 +3976,76 @@ function Rating({ rating, handleChange }) {
3950
3976
  ) })
3951
3977
  ] }, i)) });
3952
3978
  }
3979
+
3980
+ // src/genre-tree/EntityDetailPanel.tsx
3981
+ var import_react21 = require("react");
3982
+ var import_ui22 = require("@behindthemusictree/ui");
3983
+ var import_jsx_runtime51 = require("react/jsx-runtime");
3984
+ function EntityDetailPanel({
3985
+ scope,
3986
+ getBackendBaseUrl,
3987
+ getArtistBackendBaseUrl,
3988
+ getShareUrl,
3989
+ songUuid,
3990
+ onSongUuidChange,
3991
+ artistUuid,
3992
+ onArtistUuidChange,
3993
+ renderArtistLink,
3994
+ renderSongLink,
3995
+ collapsed,
3996
+ edgeTab,
3997
+ ...navigation
3998
+ }) {
3999
+ const [expandedSongUuid, setExpandedSongUuid] = (0, import_react21.useState)(null);
4000
+ const [expandedArtistUuid, setExpandedArtistUuid] = (0, import_react21.useState)(null);
4001
+ const panel = useEntityPanel({
4002
+ scope,
4003
+ getBackendBaseUrl,
4004
+ getArtistBackendBaseUrl,
4005
+ getShareUrl,
4006
+ renderArtistLink: renderArtistLink ?? ((artist) => artist.isSpecialPurpose ? artist.name : /* @__PURE__ */ (0, import_jsx_runtime51.jsx)("button", { type: "button", onClick: () => onArtistUuidChange(artist.uuid), className: "hover:underline", children: artist.name })),
4007
+ renderSongLink: renderSongLink ?? ((song) => /* @__PURE__ */ (0, import_jsx_runtime51.jsx)("button", { type: "button", onClick: () => onSongUuidChange(song.uuid), className: "hover:underline", children: song.title })),
4008
+ song: {
4009
+ uuid: songUuid,
4010
+ expanded: songUuid !== null && expandedSongUuid === songUuid,
4011
+ onExpandedChange: (on) => setExpandedSongUuid(on ? songUuid : null),
4012
+ onClose: () => onSongUuidChange(null)
4013
+ },
4014
+ artist: {
4015
+ uuid: artistUuid,
4016
+ expanded: artistUuid !== null && expandedArtistUuid === artistUuid,
4017
+ onExpandedChange: (on) => setExpandedArtistUuid(on ? artistUuid : null),
4018
+ onClose: () => onArtistUuidChange(null)
4019
+ },
4020
+ ...navigation
4021
+ });
4022
+ if (!panel) return null;
4023
+ return /* @__PURE__ */ (0, import_jsx_runtime51.jsx)(
4024
+ import_ui22.DetailPanel,
4025
+ {
4026
+ side: "left",
4027
+ title: panel.title,
4028
+ titleInSearch: true,
4029
+ onClose: panel.onClose,
4030
+ collapsed,
4031
+ expanded: panel.expanded,
4032
+ onExpandedChange: panel.onExpandedChange,
4033
+ aside: panel.extraContent,
4034
+ edgeTab,
4035
+ children: panel.extraContent && !panel.expanded ? /* @__PURE__ */ (0, import_jsx_runtime51.jsxs)(import_jsx_runtime51.Fragment, { children: [
4036
+ panel.content,
4037
+ panel.extraContent
4038
+ ] }) : panel.content
4039
+ }
4040
+ );
4041
+ }
3953
4042
  // Annotate the CommonJS export names for ESM import in node:
3954
4043
  0 && (module.exports = {
3955
4044
  AuthCallbackHandler,
3956
4045
  AuthErrorPopup,
3957
4046
  AuthPopup,
3958
4047
  BasePopup,
4048
+ EntityDetailPanel,
3959
4049
  GenreDetailPanel,
3960
4050
  GenreInfoPanelBody,
3961
4051
  GenreNodeActionBand,
@@ -3982,6 +4072,7 @@ function Rating({ rating, handleChange }) {
3982
4072
  TrackUploadPopup,
3983
4073
  searchStatus,
3984
4074
  useArtistSearchGroup,
4075
+ useEntityPanel,
3985
4076
  useGenrePlaylistPlayback,
3986
4077
  useGenreRemoteSearchGroup,
3987
4078
  useSongSearchGroup