@behindthemusictree/app-kit-ui 35.0.0 → 37.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -722,6 +722,7 @@ function SearchBox({
722
722
  groups,
723
723
  onPicked,
724
724
  onClear,
725
+ clearLabel,
725
726
  leading,
726
727
  className: className2,
727
728
  onFocus
@@ -753,6 +754,7 @@ function SearchBox({
753
754
  status,
754
755
  loading,
755
756
  onClear,
757
+ clearLabel,
756
758
  leading,
757
759
  inputClassName: "focus-visible:ring-0"
758
760
  }
@@ -848,6 +850,8 @@ function GenreSearch({
848
850
  onSelect,
849
851
  selectedName = null,
850
852
  onClear,
853
+ showClear,
854
+ clearLabel,
851
855
  onPrevious,
852
856
  placeholder,
853
857
  onQueryChange,
@@ -895,7 +899,8 @@ function GenreSearch({
895
899
  if (groupKey !== "genres") setQuery("");
896
900
  onQueryChange?.("");
897
901
  },
898
- onClear: canClear ? () => {
902
+ clearLabel,
903
+ onClear: showClear ?? canClear ? () => {
899
904
  clear();
900
905
  onQueryChange?.("");
901
906
  } : void 0,
@@ -1417,7 +1422,7 @@ function GenrePlaylistTracks({
1417
1422
  const trackNoun = (0, import_track_noun.useTrackNoun)();
1418
1423
  const uuid = genrePlaylist?.uuid;
1419
1424
  const genreName = selectedGenreName ?? genrePlaylist?.name ?? "Genre";
1420
- const { fetch } = (0, import_transport3.useFetchWrapper)(getBackendBaseUrl);
1425
+ const { fetchBound } = (0, import_transport3.usePlaybackFetch)(getBackendBaseUrl);
1421
1426
  const playback = (0, import_transport3.usePlaybackContext)();
1422
1427
  const { data, isPending, isError, refetch, hasNextPage, isFetchingNextPage, fetchNextPage } = (0, import_react_query.useInfiniteQuery)({
1423
1428
  queryKey: [...scope === "me" ? uuid ? import_genre_tree12.genrePlaylistQueryKeys.me.tracks(uuid) : ["missing-personal-genre-playlist", genreUuid] : artist ? [...import_genre_tree13.libraryQueryKeys.reference.songs.byGenre(getBackendBaseUrl(), genreUuid, selection), "artist", artist.uuid] : import_genre_tree13.libraryQueryKeys.reference.songs.byGenre(getBackendBaseUrl(), genreUuid, selection), ...playback.key ? [playback.key] : []],
@@ -1426,7 +1431,7 @@ function GenrePlaylistTracks({
1426
1431
  if (!uuid) throw new Error("Personal genre has no playlist");
1427
1432
  return fetchGenrePlaylistTracksPage(uuid, scope, Number(pageParam));
1428
1433
  }
1429
- const response = await fetch(
1434
+ const response = await fetchBound(
1430
1435
  import_genre_tree13.libraryEndpoints.reference.songs.list(),
1431
1436
  true,
1432
1437
  false,
@@ -1438,7 +1443,9 @@ function GenrePlaylistTracks({
1438
1443
  },
1439
1444
  initialPageParam: scope === "me" ? 1 : "",
1440
1445
  getNextPageParam: (lastPage) => lastPage.nextPage ?? void 0,
1441
- enabled: playback.ready && (scope !== "me" || uuid !== void 0 && sessionRestored && !!session?.accessToken)
1446
+ enabled: playback.ready && (scope !== "me" || uuid !== void 0 && sessionRestored && !!session?.accessToken),
1447
+ retry: (failureCount, error) => failureCount < 1 && error instanceof DOMException && error.name === "AbortError",
1448
+ retryDelay: 0
1442
1449
  });
1443
1450
  const [sentinel, setSentinel] = (0, import_react12.useState)(null);
1444
1451
  (0, import_react12.useEffect)(() => {
@@ -1757,10 +1764,12 @@ function useGenreDetailContent({
1757
1764
  };
1758
1765
  }, [detail, scope]);
1759
1766
  if (isError) return {
1767
+ name: null,
1760
1768
  title: "Genre unavailable",
1761
1769
  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." })
1762
1770
  };
1763
1771
  if (isPending || !detail || !view) return {
1772
+ name: null,
1764
1773
  title: "Loading genre\u2026",
1765
1774
  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: [
1766
1775
  /* @__PURE__ */ (0, import_jsx_runtime26.jsx)(import_ui7.Skeleton, { className: "h-6 w-2/3" }),
@@ -1782,6 +1791,7 @@ function useGenreDetailContent({
1782
1791
  );
1783
1792
  const ancestors = /* @__PURE__ */ (0, import_jsx_runtime26.jsx)(import_genre_tree_view4.GenreNodeAncestors, { ancestorNodes: view.ancestorNodes, onSelectNode: selectGenre });
1784
1793
  return {
1794
+ name: detail.name,
1785
1795
  title: detail.name,
1786
1796
  headerAccent: { background: view.fill, color: import_genre_tree_view4.ACCENT_TEXT_COLOR },
1787
1797
  aside: expanded ? ancestors : void 0,
@@ -1823,7 +1833,7 @@ function GenreDetailPanel({
1823
1833
  onExpandedChange,
1824
1834
  edgeTab,
1825
1835
  extras,
1826
- headerClassName
1836
+ titleInSearch
1827
1837
  }) {
1828
1838
  const panel = useGenreDetailContent({
1829
1839
  genreUuid,
@@ -1846,7 +1856,7 @@ function GenreDetailPanel({
1846
1856
  onExpandedChange,
1847
1857
  edgeTab,
1848
1858
  aside: panel.aside,
1849
- headerClassName,
1859
+ titleInSearch,
1850
1860
  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,
1851
1861
  children: panel.content
1852
1862
  }
@@ -2214,12 +2224,12 @@ var SORTS = [
2214
2224
  function ArtistSongs({ artist, getBackendBaseUrl, renderArtistLink, renderSongLink }) {
2215
2225
  const [page, setPage] = (0, import_react16.useState)(1);
2216
2226
  const [ordering, setOrdering] = (0, import_react16.useState)("title");
2217
- const { fetch } = (0, import_transport7.useFetchWrapper)(getBackendBaseUrl);
2218
2227
  const { playNewTrackListFromArtist } = (0, import_genre_tree24.useTrackList)();
2219
2228
  const { playerTrackObject } = (0, import_player10.usePlayer)();
2220
- const { data, isPending, isError } = (0, import_transport7.useQueryWithParse)({
2229
+ const { data, isPending, isError } = (0, import_transport7.usePlaybackQuery)({
2230
+ getBackendBaseUrl,
2221
2231
  queryKey: ["referenceSongs", "artist", artist.uuid, ordering, page],
2222
- queryFn: () => fetch(import_genre_tree24.libraryEndpoints.reference.songs.list(), true, false, void 0, {
2232
+ queryFn: (fetchBound) => fetchBound(import_genre_tree24.libraryEndpoints.reference.songs.list(), true, false, void 0, {
2223
2233
  artist: artist.uuid,
2224
2234
  ordering,
2225
2235
  page
@@ -2657,6 +2667,8 @@ function GenreTreeView({
2657
2667
  };
2658
2668
  const {
2659
2669
  isLoading,
2670
+ isError,
2671
+ refetch,
2660
2672
  isFetching: isTreeFetching,
2661
2673
  genrePlaylistByNodeId,
2662
2674
  visibleGenrePlaylists,
@@ -2789,7 +2801,7 @@ function GenreTreeView({
2789
2801
  }, [selectedGenreUuid, artistUuid, songUuid]);
2790
2802
  const [isWheelRevealed, setIsWheelRevealed] = (0, import_react18.useState)(false);
2791
2803
  const usesHandoff = viewMode === "wheel" || viewMode === "pop-core";
2792
- const isTreeReady = !isLoading && (!usesHandoff || isWheelRevealed);
2804
+ const isTreeReady = !isLoading && !isError && (!usesHandoff || isWheelRevealed);
2793
2805
  const [resolveStyle, setResolveStyle] = (0, import_react18.useState)(null);
2794
2806
  const handleNodeStyleResolverChange = (0, import_react18.useCallback)(
2795
2807
  (resolver) => setResolveStyle(() => resolver),
@@ -2820,7 +2832,6 @@ function GenreTreeView({
2820
2832
  if (node !== null && songUuid !== null) changeSong(null);
2821
2833
  selectNode(node);
2822
2834
  };
2823
- const handleGenreSearchClear = clearSelection;
2824
2835
  const handleGenreSearchSelect = (genrePlaylist2) => {
2825
2836
  reopenPanel();
2826
2837
  if (songUuid !== null) changeSong(null);
@@ -2903,7 +2914,7 @@ function GenreTreeView({
2903
2914
  expanded: overlayPanel.expanded ?? false,
2904
2915
  onExpandedChange: overlayPanel.onExpandedChange,
2905
2916
  onClose: overlayPanel.onClose,
2906
- compact: false
2917
+ titleInSearch: false
2907
2918
  };
2908
2919
  } else if (selectedGenreUuid) {
2909
2920
  activePanel = {
@@ -2913,7 +2924,7 @@ function GenreTreeView({
2913
2924
  expanded: isExpanded,
2914
2925
  onExpandedChange: changeExpanded,
2915
2926
  onClose: clearSelection,
2916
- compact: false,
2927
+ titleInSearch: true,
2917
2928
  headerAccent: relations ? { background: relations.fill, color: relations.textColor } : genreContent.headerAccent
2918
2929
  };
2919
2930
  } else if (builtInPanel) {
@@ -2924,7 +2935,7 @@ function GenreTreeView({
2924
2935
  expanded: builtInPanel.expanded,
2925
2936
  onExpandedChange: builtInPanel.onExpandedChange,
2926
2937
  onClose: builtInPanel.onClose,
2927
- compact: true
2938
+ titleInSearch: true
2928
2939
  };
2929
2940
  }
2930
2941
  const isPanelShown = activePanel !== null && (selectedGenreUuid !== null || isTreeReady);
@@ -2969,7 +2980,7 @@ function GenreTreeView({
2969
2980
  ] });
2970
2981
  };
2971
2982
  const actions = /* @__PURE__ */ (0, import_jsx_runtime40.jsxs)(import_jsx_runtime40.Fragment, { children: [
2972
- !isLoading && /* @__PURE__ */ (0, import_jsx_runtime40.jsx)(
2983
+ !isLoading && !isError && /* @__PURE__ */ (0, import_jsx_runtime40.jsx)(
2973
2984
  GenreTreeSearch,
2974
2985
  {
2975
2986
  artistBaseUrl,
@@ -2992,17 +3003,19 @@ function GenreTreeView({
2992
3003
  if (songUuid) changeSong(null);
2993
3004
  selectGenre(uuid);
2994
3005
  },
2995
- selectedName: (selectedGenreUuid ? genreContent.title : null) ?? (typeof builtInPanel?.title === "string" ? builtInPanel.title : null),
2996
- onClear: builtInSongPanel ? () => changeSong(null) : builtInArtistPanel ? () => changeArtist(null) : handleGenreSearchClear,
3006
+ selectedName: selectedGenreUuid ? genreContent.name : typeof builtInPanel?.title === "string" ? builtInPanel.title : null,
3007
+ onClear: activePanel?.onClose,
3008
+ showClear: activePanel?.titleInSearch ?? false,
3009
+ clearLabel: "Close panel",
2997
3010
  onPrevious: handlePrevious,
2998
3011
  onQueryChange: onSearchQueryChange,
2999
3012
  onFocus: onSearchFocus,
3000
3013
  extraGroups: extraSearchGroups
3001
3014
  }
3002
3015
  ),
3003
- !isLoading && filterArtist && /* @__PURE__ */ (0, import_jsx_runtime40.jsx)("div", { className: "hidden w-full justify-center max-md:flex", children: renderArtistFilter() }),
3016
+ !isLoading && !isError && filterArtist && /* @__PURE__ */ (0, import_jsx_runtime40.jsx)("div", { className: "hidden w-full justify-center max-md:flex", children: renderArtistFilter() }),
3004
3017
  /* @__PURE__ */ (0, import_jsx_runtime40.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: [
3005
- !isLoading && !isControlled && /* @__PURE__ */ (0, import_jsx_runtime40.jsx)(
3018
+ !isLoading && !isError && !isControlled && /* @__PURE__ */ (0, import_jsx_runtime40.jsx)(
3006
3019
  import_ui17.SegmentedControl,
3007
3020
  {
3008
3021
  "aria-label": "Tree view mode",
@@ -3016,7 +3029,7 @@ function GenreTreeView({
3016
3029
  ]
3017
3030
  }
3018
3031
  ),
3019
- !isLoading && showDisplayOptions && /* @__PURE__ */ (0, import_jsx_runtime40.jsx)(
3032
+ !isLoading && !isError && showDisplayOptions && /* @__PURE__ */ (0, import_jsx_runtime40.jsx)(
3020
3033
  "div",
3021
3034
  {
3022
3035
  className: "flex items-center gap-1",
@@ -3033,7 +3046,7 @@ function GenreTreeView({
3033
3046
  )
3034
3047
  }
3035
3048
  ),
3036
- !isLoading && !readOnly && showAddRoot && /* @__PURE__ */ (0, import_jsx_runtime40.jsx)(
3049
+ !isLoading && !isError && !readOnly && showAddRoot && /* @__PURE__ */ (0, import_jsx_runtime40.jsx)(
3037
3050
  import_ui17.IconTextButton,
3038
3051
  {
3039
3052
  icon: import_lucide_react12.Plus,
@@ -3056,7 +3069,7 @@ function GenreTreeView({
3056
3069
  children: actions
3057
3070
  }
3058
3071
  ),
3059
- !isLoading && filterArtist && /* @__PURE__ */ (0, import_jsx_runtime40.jsx)(
3072
+ !isLoading && !isError && filterArtist && /* @__PURE__ */ (0, import_jsx_runtime40.jsx)(
3060
3073
  "div",
3061
3074
  {
3062
3075
  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"}`,
@@ -3064,8 +3077,12 @@ function GenreTreeView({
3064
3077
  }
3065
3078
  ),
3066
3079
  /* @__PURE__ */ (0, import_jsx_runtime40.jsx)("div", { className: "content-container flex-1 min-h-0 flex flex-row gap-4", children: /* @__PURE__ */ (0, import_jsx_runtime40.jsxs)("div", { className: "tree-view-container relative flex-1 min-w-0 flex flex-col h-full", children: [
3067
- !isLoading && viewMode === "outline" && floatingControlsLeading && /* @__PURE__ */ (0, import_jsx_runtime40.jsx)("div", { className: "absolute bottom-3 right-3 z-10", children: floatingControlsLeading }),
3068
- isLoading ? /* @__PURE__ */ (0, import_jsx_runtime40.jsx)(import_genre_tree_view5.GenreTreeViewSkeleton, { viewMode }) : viewMode === "wheel" ? (
3080
+ !isLoading && !isError && viewMode === "outline" && floatingControlsLeading && /* @__PURE__ */ (0, import_jsx_runtime40.jsx)("div", { className: "absolute bottom-3 right-3 z-10", children: floatingControlsLeading }),
3081
+ isLoading ? /* @__PURE__ */ (0, import_jsx_runtime40.jsx)(import_genre_tree_view5.GenreTreeViewSkeleton, { viewMode }) : isError ? /* @__PURE__ */ (0, import_jsx_runtime40.jsxs)("p", { role: "alert", className: "m-auto text-center", children: [
3082
+ "Couldn't load the genre tree.",
3083
+ " ",
3084
+ /* @__PURE__ */ (0, import_jsx_runtime40.jsx)("button", { type: "button", className: "underline", onClick: refetch, children: "Retry" })
3085
+ ] }) : viewMode === "wheel" ? (
3069
3086
  // The info panel's top edge aligns with the tree's; the search bar floats `--layout-panel-inset` below it,
3070
3087
  // and the header inset (the bar's height plus that inset) extends the header's fill behind the bar,
3071
3088
  // Google Maps style; the 8px bottom padding mirrors the inset above the bar, and the hidden title's height is zeroed.
@@ -3179,8 +3196,7 @@ function GenreTreeView({
3179
3196
  ref: panelRef,
3180
3197
  side: "left",
3181
3198
  title: activePanel?.title ?? "",
3182
- hideTitle: activePanel?.compact,
3183
- hideClose: activePanel?.compact,
3199
+ titleInSearch: activePanel?.titleInSearch,
3184
3200
  headerAccent: activePanel?.headerAccent,
3185
3201
  headerActions: expandButton,
3186
3202
  onClose: activePanel?.onClose ?? noop,
@@ -3189,7 +3205,7 @@ function GenreTreeView({
3189
3205
  onExpandedChange: activePanel?.onExpandedChange,
3190
3206
  aside: activePanel?.aside,
3191
3207
  edgeTab: detailTab,
3192
- headerClassName: "pt-[calc(var(--layout-search-h)+var(--layout-panel-inset)+0.5rem)]",
3208
+ headerClassName: activePanel?.titleInSearch ? void 0 : "pt-[calc(var(--layout-search-h)+var(--layout-panel-inset)+0.5rem)]",
3193
3209
  children: activePanel?.aside && !activePanel.expanded ? /* @__PURE__ */ (0, import_jsx_runtime40.jsxs)(import_jsx_runtime40.Fragment, { children: [
3194
3210
  activePanel.content,
3195
3211
  activePanel.aside