@behindthemusictree/app-kit-ui 36.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.
@@ -20,6 +20,7 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
20
20
  // src/genre-tree/index.ts
21
21
  var genre_tree_exports = {};
22
22
  __export(genre_tree_exports, {
23
+ EntityDetailPanel: () => EntityDetailPanel,
23
24
  GenreDetailPanel: () => GenreDetailPanel,
24
25
  GenreInfoPanelBody: () => GenreInfoPanelBody,
25
26
  GenreNodeActionBand: () => GenreNodeActionBand,
@@ -39,6 +40,7 @@ __export(genre_tree_exports, {
39
40
  TrackPositionPlayPause: () => TrackPositionPlayPause,
40
41
  searchStatus: () => searchStatus,
41
42
  useArtistSearchGroup: () => useArtistSearchGroup,
43
+ useEntityPanel: () => useEntityPanel,
42
44
  useGenrePlaylistPlayback: () => useGenrePlaylistPlayback,
43
45
  useGenreRemoteSearchGroup: () => useGenreRemoteSearchGroup,
44
46
  useSongSearchGroup: () => useSongSearchGroup
@@ -722,6 +724,7 @@ function SearchBox({
722
724
  groups,
723
725
  onPicked,
724
726
  onClear,
727
+ clearLabel,
725
728
  leading,
726
729
  className: className2,
727
730
  onFocus
@@ -753,6 +756,7 @@ function SearchBox({
753
756
  status,
754
757
  loading,
755
758
  onClear,
759
+ clearLabel,
756
760
  leading,
757
761
  inputClassName: "focus-visible:ring-0"
758
762
  }
@@ -848,6 +852,8 @@ function GenreSearch({
848
852
  onSelect,
849
853
  selectedName = null,
850
854
  onClear,
855
+ showClear,
856
+ clearLabel,
851
857
  onPrevious,
852
858
  placeholder,
853
859
  onQueryChange,
@@ -895,7 +901,8 @@ function GenreSearch({
895
901
  if (groupKey !== "genres") setQuery("");
896
902
  onQueryChange?.("");
897
903
  },
898
- onClear: canClear ? () => {
904
+ clearLabel,
905
+ onClear: showClear ?? canClear ? () => {
899
906
  clear();
900
907
  onQueryChange?.("");
901
908
  } : void 0,
@@ -1759,10 +1766,12 @@ function useGenreDetailContent({
1759
1766
  };
1760
1767
  }, [detail, scope]);
1761
1768
  if (isError) return {
1769
+ name: null,
1762
1770
  title: "Genre unavailable",
1763
1771
  content: /* @__PURE__ */ (0, import_jsx_runtime26.jsx)("p", { role: "alert", className: "p-4 text-sm text-[var(--color-text)]", children: "This genre could not be loaded." })
1764
1772
  };
1765
1773
  if (isPending || !detail || !view) return {
1774
+ name: null,
1766
1775
  title: "Loading genre\u2026",
1767
1776
  content: /* @__PURE__ */ (0, import_jsx_runtime26.jsxs)("div", { className: "flex flex-col gap-3 p-4", "aria-busy": "true", "data-testid": "genre-detail-panel-skeleton", children: [
1768
1777
  /* @__PURE__ */ (0, import_jsx_runtime26.jsx)(import_ui7.Skeleton, { className: "h-6 w-2/3" }),
@@ -1784,6 +1793,7 @@ function useGenreDetailContent({
1784
1793
  );
1785
1794
  const ancestors = /* @__PURE__ */ (0, import_jsx_runtime26.jsx)(import_genre_tree_view4.GenreNodeAncestors, { ancestorNodes: view.ancestorNodes, onSelectNode: selectGenre });
1786
1795
  return {
1796
+ name: detail.name,
1787
1797
  title: detail.name,
1788
1798
  headerAccent: { background: view.fill, color: import_genre_tree_view4.ACCENT_TEXT_COLOR },
1789
1799
  aside: expanded ? ancestors : void 0,
@@ -1825,7 +1835,7 @@ function GenreDetailPanel({
1825
1835
  onExpandedChange,
1826
1836
  edgeTab,
1827
1837
  extras,
1828
- headerClassName
1838
+ titleInSearch
1829
1839
  }) {
1830
1840
  const panel = useGenreDetailContent({
1831
1841
  genreUuid,
@@ -1848,7 +1858,7 @@ function GenreDetailPanel({
1848
1858
  onExpandedChange,
1849
1859
  edgeTab,
1850
1860
  aside: panel.aside,
1851
- headerClassName,
1861
+ titleInSearch,
1852
1862
  headerActions: onExpandedChange ? /* @__PURE__ */ (0, import_jsx_runtime26.jsx)(import_ui7.IconButton, { "aria-label": expanded ? "Shrink panel" : "Extend panel", onClick: () => onExpandedChange(!expanded), children: expanded ? /* @__PURE__ */ (0, import_jsx_runtime26.jsx)(import_lucide_react8.Minimize2, { "aria-hidden": "true", className: "h-4 w-4" }) : /* @__PURE__ */ (0, import_jsx_runtime26.jsx)(import_lucide_react8.Maximize2, { "aria-hidden": "true", className: "h-4 w-4" }) }) : void 0,
1853
1863
  children: panel.content
1854
1864
  }
@@ -2402,15 +2412,8 @@ function SongHistory({ uuid, getBackendBaseUrl }) {
2402
2412
  }
2403
2413
  );
2404
2414
  }
2405
- function SongPanelContent({
2406
- song,
2407
- scope,
2408
- getBackendBaseUrl,
2409
- shareUrl,
2410
- renderArtistLink,
2411
- genrePath,
2412
- onSelectGenre
2413
- }) {
2415
+ function SongPanelContent(props) {
2416
+ const { song, scope, getBackendBaseUrl, shareUrl, renderArtistLink } = props;
2414
2417
  const { playNewTrackListFromTrackUuid } = (0, import_genre_tree26.useTrackList)();
2415
2418
  const { playerTrackObject, playState, handlePlayPauseAction } = (0, import_player11.usePlayer)();
2416
2419
  const unplayableReason = (0, import_genre_tree26.songUnplayableReason)(song);
@@ -2420,12 +2423,14 @@ function SongPanelContent({
2420
2423
  const musicBrainzUrl = (0, import_genre_tree26.musicBrainzRecordingUrl)(song);
2421
2424
  const fields = [];
2422
2425
  if (song.genre) {
2423
- const path = genrePath.length > 0 ? genrePath : null;
2426
+ const path = props.genrePath && props.genrePath.length > 0 ? props.genrePath : null;
2427
+ const { onSelectGenre } = props;
2428
+ const genre = song.genre;
2424
2429
  fields.push({
2425
2430
  label: "Genre",
2426
- value: path ? /* @__PURE__ */ (0, import_jsx_runtime37.jsx)("span", { className: "flex flex-wrap items-center gap-x-1", children: path.map((genrePlaylist, index) => /* @__PURE__ */ (0, import_jsx_runtime37.jsxs)("span", { children: [
2431
+ value: onSelectGenre ? /* @__PURE__ */ (0, import_jsx_runtime37.jsx)("button", { type: "button", onClick: () => onSelectGenre(genre), className: "hover:underline", children: genre.name }) : path ? /* @__PURE__ */ (0, import_jsx_runtime37.jsx)("span", { className: "flex flex-wrap items-center gap-x-1", children: path.map((genrePlaylist, index) => /* @__PURE__ */ (0, import_jsx_runtime37.jsxs)("span", { children: [
2427
2432
  index > 0 && /* @__PURE__ */ (0, import_jsx_runtime37.jsx)("span", { "aria-hidden": "true", children: "\u203A " }),
2428
- /* @__PURE__ */ (0, import_jsx_runtime37.jsx)("button", { type: "button", onClick: () => onSelectGenre(genrePlaylist), className: "hover:underline", children: genrePlaylist.name })
2433
+ /* @__PURE__ */ (0, import_jsx_runtime37.jsx)("button", { type: "button", onClick: () => props.onSelectGenrePlaylist?.(genrePlaylist), className: "hover:underline", children: genrePlaylist.name })
2429
2434
  ] }, genrePlaylist.uuid)) }) : song.genre.name
2430
2435
  });
2431
2436
  } else {
@@ -2491,11 +2496,10 @@ function useSongPanel({
2491
2496
  getBackendBaseUrl,
2492
2497
  getShareUrl,
2493
2498
  renderArtistLink,
2494
- getGenrePath,
2495
- onSelectGenre,
2496
2499
  expanded,
2497
2500
  onExpandedChange,
2498
- onClose
2501
+ onClose,
2502
+ ...navigation
2499
2503
  }) {
2500
2504
  const result = (0, import_genre_tree27.useSong)(uuid, getBackendBaseUrl);
2501
2505
  const panel = { expanded, onExpandedChange, onClose };
@@ -2539,8 +2543,10 @@ function useSongPanel({
2539
2543
  getBackendBaseUrl,
2540
2544
  shareUrl: shareUrl ?? void 0,
2541
2545
  renderArtistLink,
2542
- genrePath: song.genre ? getGenrePath(song.genre) : [],
2543
- onSelectGenre
2546
+ ...navigation.getGenrePath ? {
2547
+ genrePath: song.genre ? navigation.getGenrePath(song.genre) : [],
2548
+ onSelectGenrePlaylist: navigation.onSelectGenrePlaylist
2549
+ } : { onSelectGenre: navigation.onSelectGenre }
2544
2550
  }
2545
2551
  )
2546
2552
  };
@@ -2548,6 +2554,37 @@ function useSongPanel({
2548
2554
  }
2549
2555
  }
2550
2556
 
2557
+ // src/genre-tree/useEntityPanel.tsx
2558
+ function useEntityPanel({
2559
+ scope,
2560
+ getBackendBaseUrl,
2561
+ getArtistBackendBaseUrl,
2562
+ getShareUrl,
2563
+ renderArtistLink,
2564
+ renderSongLink,
2565
+ song,
2566
+ artist,
2567
+ ...navigation
2568
+ }) {
2569
+ const songPanel = useSongPanel({
2570
+ ...song,
2571
+ scope,
2572
+ getBackendBaseUrl,
2573
+ getShareUrl,
2574
+ renderArtistLink,
2575
+ ...navigation
2576
+ });
2577
+ const artistPanel = useArtistPanel({
2578
+ ...artist,
2579
+ scope,
2580
+ getBackendBaseUrl: getArtistBackendBaseUrl ?? getBackendBaseUrl,
2581
+ getShareUrl,
2582
+ renderArtistLink,
2583
+ renderSongLink
2584
+ });
2585
+ return songPanel ?? artistPanel;
2586
+ }
2587
+
2551
2588
  // src/genre-tree/SizeMetricToggle.tsx
2552
2589
  var import_md = require("react-icons/md");
2553
2590
  var import_ui16 = require("@behindthemusictree/ui");
@@ -2730,20 +2767,6 @@ function GenreTreeView({
2730
2767
  const [expandedArtistUuid, setExpandedArtistUuid] = (0, import_react18.useState)(null);
2731
2768
  const artistBaseUrl = artistSearchBaseUrl ?? getBackendBaseUrl;
2732
2769
  const songBaseUrl = getBackendBaseUrl;
2733
- const artistPanel = useArtistPanel({
2734
- uuid: artistUuid,
2735
- scope,
2736
- getBackendBaseUrl: artistBaseUrl,
2737
- getShareUrl: getShareUrlRef.current,
2738
- renderArtistLink: renderArtistLink ?? ((artist) => artist.isSpecialPurpose ? artist.name : /* @__PURE__ */ (0, import_jsx_runtime40.jsx)("button", { type: "button", onClick: () => openArtist(artist.uuid), className: "hover:underline", children: artist.name })),
2739
- expanded: controlledArtistExpanded ?? (artistUuid !== null && expandedArtistUuid === artistUuid),
2740
- onExpandedChange: (on) => {
2741
- if (controlledArtistExpanded === void 0) setExpandedArtistUuid(on ? artistUuid : null);
2742
- onArtistExpandedChange?.(on);
2743
- },
2744
- renderSongLink: resolvedRenderSongLink,
2745
- onClose: () => changeArtist(null)
2746
- });
2747
2770
  const resolvedRenderArtistLink = renderArtistLink ?? ((artist) => artist.isSpecialPurpose ? artist.name : /* @__PURE__ */ (0, import_jsx_runtime40.jsx)("button", { type: "button", onClick: () => openArtist(artist.uuid), className: "hover:underline", children: artist.name }));
2748
2771
  const getGenrePath = (genre) => {
2749
2772
  const path = [];
@@ -2755,27 +2778,38 @@ function GenreTreeView({
2755
2778
  }
2756
2779
  return path;
2757
2780
  };
2758
- const songPanel = useSongPanel({
2759
- uuid: songUuid,
2781
+ const entityPanel = useEntityPanel({
2760
2782
  scope,
2761
2783
  getBackendBaseUrl: songBaseUrl,
2784
+ getArtistBackendBaseUrl: artistBaseUrl,
2762
2785
  getShareUrl: getShareUrlRef.current,
2763
2786
  renderArtistLink: resolvedRenderArtistLink,
2787
+ renderSongLink: resolvedRenderSongLink,
2788
+ song: {
2789
+ uuid: songUuid,
2790
+ expanded: controlledSongExpanded ?? (songUuid !== null && expandedSongUuid === songUuid),
2791
+ onExpandedChange: (on) => {
2792
+ if (controlledSongExpanded === void 0) setExpandedSongUuid(on ? songUuid : null);
2793
+ onSongExpandedChange?.(on);
2794
+ },
2795
+ onClose: () => changeSong(null)
2796
+ },
2797
+ artist: {
2798
+ uuid: artistUuid,
2799
+ expanded: controlledArtistExpanded ?? (artistUuid !== null && expandedArtistUuid === artistUuid),
2800
+ onExpandedChange: (on) => {
2801
+ if (controlledArtistExpanded === void 0) setExpandedArtistUuid(on ? artistUuid : null);
2802
+ onArtistExpandedChange?.(on);
2803
+ },
2804
+ onClose: () => changeArtist(null)
2805
+ },
2764
2806
  getGenrePath,
2765
- onSelectGenre: (genrePlaylist2) => {
2807
+ onSelectGenrePlaylist: (genrePlaylist2) => {
2766
2808
  changeSong(null);
2767
2809
  selectGenrePlaylist(genrePlaylist2);
2768
- },
2769
- expanded: controlledSongExpanded ?? (songUuid !== null && expandedSongUuid === songUuid),
2770
- onExpandedChange: (on) => {
2771
- if (controlledSongExpanded === void 0) setExpandedSongUuid(on ? songUuid : null);
2772
- onSongExpandedChange?.(on);
2773
- },
2774
- onClose: () => changeSong(null)
2810
+ }
2775
2811
  });
2776
- const builtInSongPanel = overlayPanel || selectedGenreUuid !== null ? void 0 : songPanel;
2777
- const builtInArtistPanel = overlayPanel || selectedGenreUuid !== null || builtInSongPanel ? void 0 : artistPanel;
2778
- const builtInPanel = builtInSongPanel ?? builtInArtistPanel;
2812
+ const builtInPanel = overlayPanel || selectedGenreUuid !== null ? void 0 : entityPanel;
2779
2813
  const [uncontrolledCollapsed, setUncontrolledCollapsed] = (0, import_react18.useState)(false);
2780
2814
  const [uncontrolledExpanded, setUncontrolledExpanded] = (0, import_react18.useState)(false);
2781
2815
  const isCollapsed = collapsed ?? uncontrolledCollapsed;
@@ -2824,7 +2858,6 @@ function GenreTreeView({
2824
2858
  if (node !== null && songUuid !== null) changeSong(null);
2825
2859
  selectNode(node);
2826
2860
  };
2827
- const handleGenreSearchClear = clearSelection;
2828
2861
  const handleGenreSearchSelect = (genrePlaylist2) => {
2829
2862
  reopenPanel();
2830
2863
  if (songUuid !== null) changeSong(null);
@@ -2907,7 +2940,7 @@ function GenreTreeView({
2907
2940
  expanded: overlayPanel.expanded ?? false,
2908
2941
  onExpandedChange: overlayPanel.onExpandedChange,
2909
2942
  onClose: overlayPanel.onClose,
2910
- compact: false
2943
+ titleInSearch: false
2911
2944
  };
2912
2945
  } else if (selectedGenreUuid) {
2913
2946
  activePanel = {
@@ -2917,7 +2950,7 @@ function GenreTreeView({
2917
2950
  expanded: isExpanded,
2918
2951
  onExpandedChange: changeExpanded,
2919
2952
  onClose: clearSelection,
2920
- compact: false,
2953
+ titleInSearch: true,
2921
2954
  headerAccent: relations ? { background: relations.fill, color: relations.textColor } : genreContent.headerAccent
2922
2955
  };
2923
2956
  } else if (builtInPanel) {
@@ -2928,7 +2961,7 @@ function GenreTreeView({
2928
2961
  expanded: builtInPanel.expanded,
2929
2962
  onExpandedChange: builtInPanel.onExpandedChange,
2930
2963
  onClose: builtInPanel.onClose,
2931
- compact: true
2964
+ titleInSearch: true
2932
2965
  };
2933
2966
  }
2934
2967
  const isPanelShown = activePanel !== null && (selectedGenreUuid !== null || isTreeReady);
@@ -2996,8 +3029,10 @@ function GenreTreeView({
2996
3029
  if (songUuid) changeSong(null);
2997
3030
  selectGenre(uuid);
2998
3031
  },
2999
- selectedName: (selectedGenreUuid ? genreContent.title : null) ?? (typeof builtInPanel?.title === "string" ? builtInPanel.title : null),
3000
- onClear: builtInSongPanel ? () => changeSong(null) : builtInArtistPanel ? () => changeArtist(null) : handleGenreSearchClear,
3032
+ selectedName: selectedGenreUuid ? genreContent.name : typeof builtInPanel?.title === "string" ? builtInPanel.title : null,
3033
+ onClear: activePanel?.onClose,
3034
+ showClear: activePanel?.titleInSearch ?? false,
3035
+ clearLabel: "Close panel",
3001
3036
  onPrevious: handlePrevious,
3002
3037
  onQueryChange: onSearchQueryChange,
3003
3038
  onFocus: onSearchFocus,
@@ -3187,8 +3222,7 @@ function GenreTreeView({
3187
3222
  ref: panelRef,
3188
3223
  side: "left",
3189
3224
  title: activePanel?.title ?? "",
3190
- hideTitle: activePanel?.compact,
3191
- hideClose: activePanel?.compact,
3225
+ titleInSearch: activePanel?.titleInSearch,
3192
3226
  headerAccent: activePanel?.headerAccent,
3193
3227
  headerActions: expandButton,
3194
3228
  onClose: activePanel?.onClose ?? noop,
@@ -3197,7 +3231,7 @@ function GenreTreeView({
3197
3231
  onExpandedChange: activePanel?.onExpandedChange,
3198
3232
  aside: activePanel?.aside,
3199
3233
  edgeTab: detailTab,
3200
- headerClassName: "pt-[calc(var(--layout-search-h)+var(--layout-panel-inset)+0.5rem)]",
3234
+ headerClassName: activePanel?.titleInSearch ? void 0 : "pt-[calc(var(--layout-search-h)+var(--layout-panel-inset)+0.5rem)]",
3201
3235
  children: activePanel?.aside && !activePanel.expanded ? /* @__PURE__ */ (0, import_jsx_runtime40.jsxs)(import_jsx_runtime40.Fragment, { children: [
3202
3236
  activePanel.content,
3203
3237
  activePanel.aside
@@ -3644,8 +3678,72 @@ function Rating({ rating, handleChange }) {
3644
3678
  ) })
3645
3679
  ] }, i)) });
3646
3680
  }
3681
+
3682
+ // src/genre-tree/EntityDetailPanel.tsx
3683
+ var import_react21 = require("react");
3684
+ var import_ui19 = require("@behindthemusictree/ui");
3685
+ var import_jsx_runtime45 = require("react/jsx-runtime");
3686
+ function EntityDetailPanel({
3687
+ scope,
3688
+ getBackendBaseUrl,
3689
+ getArtistBackendBaseUrl,
3690
+ getShareUrl,
3691
+ songUuid,
3692
+ onSongUuidChange,
3693
+ artistUuid,
3694
+ onArtistUuidChange,
3695
+ renderArtistLink,
3696
+ renderSongLink,
3697
+ collapsed,
3698
+ edgeTab,
3699
+ ...navigation
3700
+ }) {
3701
+ const [expandedSongUuid, setExpandedSongUuid] = (0, import_react21.useState)(null);
3702
+ const [expandedArtistUuid, setExpandedArtistUuid] = (0, import_react21.useState)(null);
3703
+ const panel = useEntityPanel({
3704
+ scope,
3705
+ getBackendBaseUrl,
3706
+ getArtistBackendBaseUrl,
3707
+ getShareUrl,
3708
+ renderArtistLink: renderArtistLink ?? ((artist) => artist.isSpecialPurpose ? artist.name : /* @__PURE__ */ (0, import_jsx_runtime45.jsx)("button", { type: "button", onClick: () => onArtistUuidChange(artist.uuid), className: "hover:underline", children: artist.name })),
3709
+ renderSongLink: renderSongLink ?? ((song) => /* @__PURE__ */ (0, import_jsx_runtime45.jsx)("button", { type: "button", onClick: () => onSongUuidChange(song.uuid), className: "hover:underline", children: song.title })),
3710
+ song: {
3711
+ uuid: songUuid,
3712
+ expanded: songUuid !== null && expandedSongUuid === songUuid,
3713
+ onExpandedChange: (on) => setExpandedSongUuid(on ? songUuid : null),
3714
+ onClose: () => onSongUuidChange(null)
3715
+ },
3716
+ artist: {
3717
+ uuid: artistUuid,
3718
+ expanded: artistUuid !== null && expandedArtistUuid === artistUuid,
3719
+ onExpandedChange: (on) => setExpandedArtistUuid(on ? artistUuid : null),
3720
+ onClose: () => onArtistUuidChange(null)
3721
+ },
3722
+ ...navigation
3723
+ });
3724
+ if (!panel) return null;
3725
+ return /* @__PURE__ */ (0, import_jsx_runtime45.jsx)(
3726
+ import_ui19.DetailPanel,
3727
+ {
3728
+ side: "left",
3729
+ title: panel.title,
3730
+ titleInSearch: true,
3731
+ onClose: panel.onClose,
3732
+ collapsed,
3733
+ expanded: panel.expanded,
3734
+ onExpandedChange: panel.onExpandedChange,
3735
+ aside: panel.extraContent,
3736
+ edgeTab,
3737
+ children: panel.extraContent && !panel.expanded ? /* @__PURE__ */ (0, import_jsx_runtime45.jsxs)(import_jsx_runtime45.Fragment, { children: [
3738
+ panel.content,
3739
+ panel.extraContent
3740
+ ] }) : panel.content
3741
+ }
3742
+ );
3743
+ }
3647
3744
  // Annotate the CommonJS export names for ESM import in node:
3648
3745
  0 && (module.exports = {
3746
+ EntityDetailPanel,
3649
3747
  GenreDetailPanel,
3650
3748
  GenreInfoPanelBody,
3651
3749
  GenreNodeActionBand,
@@ -3665,6 +3763,7 @@ function Rating({ rating, handleChange }) {
3665
3763
  TrackPositionPlayPause,
3666
3764
  searchStatus,
3667
3765
  useArtistSearchGroup,
3766
+ useEntityPanel,
3668
3767
  useGenrePlaylistPlayback,
3669
3768
  useGenreRemoteSearchGroup,
3670
3769
  useSongSearchGroup