@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.
@@ -483,6 +483,7 @@ function SearchBox({
483
483
  groups,
484
484
  onPicked,
485
485
  onClear,
486
+ clearLabel,
486
487
  leading,
487
488
  className: className2,
488
489
  onFocus
@@ -514,6 +515,7 @@ function SearchBox({
514
515
  status,
515
516
  loading,
516
517
  onClear,
518
+ clearLabel,
517
519
  leading,
518
520
  inputClassName: "focus-visible:ring-0"
519
521
  }
@@ -614,6 +616,8 @@ function GenreSearch({
614
616
  onSelect,
615
617
  selectedName = null,
616
618
  onClear,
619
+ showClear,
620
+ clearLabel,
617
621
  onPrevious,
618
622
  placeholder,
619
623
  onQueryChange,
@@ -661,7 +665,8 @@ function GenreSearch({
661
665
  if (groupKey !== "genres") setQuery("");
662
666
  onQueryChange?.("");
663
667
  },
664
- onClear: canClear ? () => {
668
+ clearLabel,
669
+ onClear: showClear ?? canClear ? () => {
665
670
  clear();
666
671
  onQueryChange?.("");
667
672
  } : void 0,
@@ -1516,10 +1521,12 @@ function useGenreDetailContent({
1516
1521
  };
1517
1522
  }, [detail, scope]);
1518
1523
  if (isError) return {
1524
+ name: null,
1519
1525
  title: "Genre unavailable",
1520
1526
  content: /* @__PURE__ */ jsx20("p", { role: "alert", className: "p-4 text-sm text-[var(--color-text)]", children: "This genre could not be loaded." })
1521
1527
  };
1522
1528
  if (isPending || !detail || !view) return {
1529
+ name: null,
1523
1530
  title: "Loading genre\u2026",
1524
1531
  content: /* @__PURE__ */ jsxs15("div", { className: "flex flex-col gap-3 p-4", "aria-busy": "true", "data-testid": "genre-detail-panel-skeleton", children: [
1525
1532
  /* @__PURE__ */ jsx20(Skeleton3, { className: "h-6 w-2/3" }),
@@ -1541,6 +1548,7 @@ function useGenreDetailContent({
1541
1548
  );
1542
1549
  const ancestors = /* @__PURE__ */ jsx20(GenreNodeAncestors, { ancestorNodes: view.ancestorNodes, onSelectNode: selectGenre });
1543
1550
  return {
1551
+ name: detail.name,
1544
1552
  title: detail.name,
1545
1553
  headerAccent: { background: view.fill, color: ACCENT_TEXT_COLOR },
1546
1554
  aside: expanded ? ancestors : void 0,
@@ -1582,7 +1590,7 @@ function GenreDetailPanel({
1582
1590
  onExpandedChange,
1583
1591
  edgeTab,
1584
1592
  extras,
1585
- headerClassName
1593
+ titleInSearch
1586
1594
  }) {
1587
1595
  const panel = useGenreDetailContent({
1588
1596
  genreUuid,
@@ -1605,7 +1613,7 @@ function GenreDetailPanel({
1605
1613
  onExpandedChange,
1606
1614
  edgeTab,
1607
1615
  aside: panel.aside,
1608
- headerClassName,
1616
+ titleInSearch,
1609
1617
  headerActions: onExpandedChange ? /* @__PURE__ */ jsx20(IconButton, { "aria-label": expanded ? "Shrink panel" : "Extend panel", onClick: () => onExpandedChange(!expanded), children: expanded ? /* @__PURE__ */ jsx20(Minimize2, { "aria-hidden": "true", className: "h-4 w-4" }) : /* @__PURE__ */ jsx20(Maximize2, { "aria-hidden": "true", className: "h-4 w-4" }) }) : void 0,
1610
1618
  children: panel.content
1611
1619
  }
@@ -2184,15 +2192,8 @@ function SongHistory({ uuid, getBackendBaseUrl }) {
2184
2192
  }
2185
2193
  );
2186
2194
  }
2187
- function SongPanelContent({
2188
- song,
2189
- scope,
2190
- getBackendBaseUrl,
2191
- shareUrl,
2192
- renderArtistLink,
2193
- genrePath,
2194
- onSelectGenre
2195
- }) {
2195
+ function SongPanelContent(props) {
2196
+ const { song, scope, getBackendBaseUrl, shareUrl, renderArtistLink } = props;
2196
2197
  const { playNewTrackListFromTrackUuid } = useTrackList5();
2197
2198
  const { playerTrackObject, playState, handlePlayPauseAction } = usePlayer9();
2198
2199
  const unplayableReason = songUnplayableReason4(song);
@@ -2202,12 +2203,14 @@ function SongPanelContent({
2202
2203
  const musicBrainzUrl = musicBrainzRecordingUrl2(song);
2203
2204
  const fields = [];
2204
2205
  if (song.genre) {
2205
- const path = genrePath.length > 0 ? genrePath : null;
2206
+ const path = props.genrePath && props.genrePath.length > 0 ? props.genrePath : null;
2207
+ const { onSelectGenre } = props;
2208
+ const genre = song.genre;
2206
2209
  fields.push({
2207
2210
  label: "Genre",
2208
- value: path ? /* @__PURE__ */ jsx31("span", { className: "flex flex-wrap items-center gap-x-1", children: path.map((genrePlaylist, index) => /* @__PURE__ */ jsxs24("span", { children: [
2211
+ value: onSelectGenre ? /* @__PURE__ */ jsx31("button", { type: "button", onClick: () => onSelectGenre(genre), className: "hover:underline", children: genre.name }) : path ? /* @__PURE__ */ jsx31("span", { className: "flex flex-wrap items-center gap-x-1", children: path.map((genrePlaylist, index) => /* @__PURE__ */ jsxs24("span", { children: [
2209
2212
  index > 0 && /* @__PURE__ */ jsx31("span", { "aria-hidden": "true", children: "\u203A " }),
2210
- /* @__PURE__ */ jsx31("button", { type: "button", onClick: () => onSelectGenre(genrePlaylist), className: "hover:underline", children: genrePlaylist.name })
2213
+ /* @__PURE__ */ jsx31("button", { type: "button", onClick: () => props.onSelectGenrePlaylist?.(genrePlaylist), className: "hover:underline", children: genrePlaylist.name })
2211
2214
  ] }, genrePlaylist.uuid)) }) : song.genre.name
2212
2215
  });
2213
2216
  } else {
@@ -2273,11 +2276,10 @@ function useSongPanel({
2273
2276
  getBackendBaseUrl,
2274
2277
  getShareUrl,
2275
2278
  renderArtistLink,
2276
- getGenrePath,
2277
- onSelectGenre,
2278
2279
  expanded,
2279
2280
  onExpandedChange,
2280
- onClose
2281
+ onClose,
2282
+ ...navigation
2281
2283
  }) {
2282
2284
  const result = useSong(uuid, getBackendBaseUrl);
2283
2285
  const panel = { expanded, onExpandedChange, onClose };
@@ -2321,8 +2323,10 @@ function useSongPanel({
2321
2323
  getBackendBaseUrl,
2322
2324
  shareUrl: shareUrl ?? void 0,
2323
2325
  renderArtistLink,
2324
- genrePath: song.genre ? getGenrePath(song.genre) : [],
2325
- onSelectGenre
2326
+ ...navigation.getGenrePath ? {
2327
+ genrePath: song.genre ? navigation.getGenrePath(song.genre) : [],
2328
+ onSelectGenrePlaylist: navigation.onSelectGenrePlaylist
2329
+ } : { onSelectGenre: navigation.onSelectGenre }
2326
2330
  }
2327
2331
  )
2328
2332
  };
@@ -2330,6 +2334,37 @@ function useSongPanel({
2330
2334
  }
2331
2335
  }
2332
2336
 
2337
+ // src/genre-tree/useEntityPanel.tsx
2338
+ function useEntityPanel({
2339
+ scope,
2340
+ getBackendBaseUrl,
2341
+ getArtistBackendBaseUrl,
2342
+ getShareUrl,
2343
+ renderArtistLink,
2344
+ renderSongLink,
2345
+ song,
2346
+ artist,
2347
+ ...navigation
2348
+ }) {
2349
+ const songPanel = useSongPanel({
2350
+ ...song,
2351
+ scope,
2352
+ getBackendBaseUrl,
2353
+ getShareUrl,
2354
+ renderArtistLink,
2355
+ ...navigation
2356
+ });
2357
+ const artistPanel = useArtistPanel({
2358
+ ...artist,
2359
+ scope,
2360
+ getBackendBaseUrl: getArtistBackendBaseUrl ?? getBackendBaseUrl,
2361
+ getShareUrl,
2362
+ renderArtistLink,
2363
+ renderSongLink
2364
+ });
2365
+ return songPanel ?? artistPanel;
2366
+ }
2367
+
2333
2368
  // src/genre-tree/SizeMetricToggle.tsx
2334
2369
  import { MdMusicNote, MdVisibility } from "react-icons/md";
2335
2370
  import { SegmentedControl } from "@behindthemusictree/ui";
@@ -2512,20 +2547,6 @@ function GenreTreeView({
2512
2547
  const [expandedArtistUuid, setExpandedArtistUuid] = useState9(null);
2513
2548
  const artistBaseUrl = artistSearchBaseUrl ?? getBackendBaseUrl;
2514
2549
  const songBaseUrl = getBackendBaseUrl;
2515
- const artistPanel = useArtistPanel({
2516
- uuid: artistUuid,
2517
- scope,
2518
- getBackendBaseUrl: artistBaseUrl,
2519
- getShareUrl: getShareUrlRef.current,
2520
- renderArtistLink: renderArtistLink ?? ((artist) => artist.isSpecialPurpose ? artist.name : /* @__PURE__ */ jsx34("button", { type: "button", onClick: () => openArtist(artist.uuid), className: "hover:underline", children: artist.name })),
2521
- expanded: controlledArtistExpanded ?? (artistUuid !== null && expandedArtistUuid === artistUuid),
2522
- onExpandedChange: (on) => {
2523
- if (controlledArtistExpanded === void 0) setExpandedArtistUuid(on ? artistUuid : null);
2524
- onArtistExpandedChange?.(on);
2525
- },
2526
- renderSongLink: resolvedRenderSongLink,
2527
- onClose: () => changeArtist(null)
2528
- });
2529
2550
  const resolvedRenderArtistLink = renderArtistLink ?? ((artist) => artist.isSpecialPurpose ? artist.name : /* @__PURE__ */ jsx34("button", { type: "button", onClick: () => openArtist(artist.uuid), className: "hover:underline", children: artist.name }));
2530
2551
  const getGenrePath = (genre) => {
2531
2552
  const path = [];
@@ -2537,27 +2558,38 @@ function GenreTreeView({
2537
2558
  }
2538
2559
  return path;
2539
2560
  };
2540
- const songPanel = useSongPanel({
2541
- uuid: songUuid,
2561
+ const entityPanel = useEntityPanel({
2542
2562
  scope,
2543
2563
  getBackendBaseUrl: songBaseUrl,
2564
+ getArtistBackendBaseUrl: artistBaseUrl,
2544
2565
  getShareUrl: getShareUrlRef.current,
2545
2566
  renderArtistLink: resolvedRenderArtistLink,
2567
+ renderSongLink: resolvedRenderSongLink,
2568
+ song: {
2569
+ uuid: songUuid,
2570
+ expanded: controlledSongExpanded ?? (songUuid !== null && expandedSongUuid === songUuid),
2571
+ onExpandedChange: (on) => {
2572
+ if (controlledSongExpanded === void 0) setExpandedSongUuid(on ? songUuid : null);
2573
+ onSongExpandedChange?.(on);
2574
+ },
2575
+ onClose: () => changeSong(null)
2576
+ },
2577
+ artist: {
2578
+ uuid: artistUuid,
2579
+ expanded: controlledArtistExpanded ?? (artistUuid !== null && expandedArtistUuid === artistUuid),
2580
+ onExpandedChange: (on) => {
2581
+ if (controlledArtistExpanded === void 0) setExpandedArtistUuid(on ? artistUuid : null);
2582
+ onArtistExpandedChange?.(on);
2583
+ },
2584
+ onClose: () => changeArtist(null)
2585
+ },
2546
2586
  getGenrePath,
2547
- onSelectGenre: (genrePlaylist2) => {
2587
+ onSelectGenrePlaylist: (genrePlaylist2) => {
2548
2588
  changeSong(null);
2549
2589
  selectGenrePlaylist(genrePlaylist2);
2550
- },
2551
- expanded: controlledSongExpanded ?? (songUuid !== null && expandedSongUuid === songUuid),
2552
- onExpandedChange: (on) => {
2553
- if (controlledSongExpanded === void 0) setExpandedSongUuid(on ? songUuid : null);
2554
- onSongExpandedChange?.(on);
2555
- },
2556
- onClose: () => changeSong(null)
2590
+ }
2557
2591
  });
2558
- const builtInSongPanel = overlayPanel || selectedGenreUuid !== null ? void 0 : songPanel;
2559
- const builtInArtistPanel = overlayPanel || selectedGenreUuid !== null || builtInSongPanel ? void 0 : artistPanel;
2560
- const builtInPanel = builtInSongPanel ?? builtInArtistPanel;
2592
+ const builtInPanel = overlayPanel || selectedGenreUuid !== null ? void 0 : entityPanel;
2561
2593
  const [uncontrolledCollapsed, setUncontrolledCollapsed] = useState9(false);
2562
2594
  const [uncontrolledExpanded, setUncontrolledExpanded] = useState9(false);
2563
2595
  const isCollapsed = collapsed ?? uncontrolledCollapsed;
@@ -2606,7 +2638,6 @@ function GenreTreeView({
2606
2638
  if (node !== null && songUuid !== null) changeSong(null);
2607
2639
  selectNode(node);
2608
2640
  };
2609
- const handleGenreSearchClear = clearSelection;
2610
2641
  const handleGenreSearchSelect = (genrePlaylist2) => {
2611
2642
  reopenPanel();
2612
2643
  if (songUuid !== null) changeSong(null);
@@ -2689,7 +2720,7 @@ function GenreTreeView({
2689
2720
  expanded: overlayPanel.expanded ?? false,
2690
2721
  onExpandedChange: overlayPanel.onExpandedChange,
2691
2722
  onClose: overlayPanel.onClose,
2692
- compact: false
2723
+ titleInSearch: false
2693
2724
  };
2694
2725
  } else if (selectedGenreUuid) {
2695
2726
  activePanel = {
@@ -2699,7 +2730,7 @@ function GenreTreeView({
2699
2730
  expanded: isExpanded,
2700
2731
  onExpandedChange: changeExpanded,
2701
2732
  onClose: clearSelection,
2702
- compact: false,
2733
+ titleInSearch: true,
2703
2734
  headerAccent: relations ? { background: relations.fill, color: relations.textColor } : genreContent.headerAccent
2704
2735
  };
2705
2736
  } else if (builtInPanel) {
@@ -2710,7 +2741,7 @@ function GenreTreeView({
2710
2741
  expanded: builtInPanel.expanded,
2711
2742
  onExpandedChange: builtInPanel.onExpandedChange,
2712
2743
  onClose: builtInPanel.onClose,
2713
- compact: true
2744
+ titleInSearch: true
2714
2745
  };
2715
2746
  }
2716
2747
  const isPanelShown = activePanel !== null && (selectedGenreUuid !== null || isTreeReady);
@@ -2778,8 +2809,10 @@ function GenreTreeView({
2778
2809
  if (songUuid) changeSong(null);
2779
2810
  selectGenre(uuid);
2780
2811
  },
2781
- selectedName: (selectedGenreUuid ? genreContent.title : null) ?? (typeof builtInPanel?.title === "string" ? builtInPanel.title : null),
2782
- onClear: builtInSongPanel ? () => changeSong(null) : builtInArtistPanel ? () => changeArtist(null) : handleGenreSearchClear,
2812
+ selectedName: selectedGenreUuid ? genreContent.name : typeof builtInPanel?.title === "string" ? builtInPanel.title : null,
2813
+ onClear: activePanel?.onClose,
2814
+ showClear: activePanel?.titleInSearch ?? false,
2815
+ clearLabel: "Close panel",
2783
2816
  onPrevious: handlePrevious,
2784
2817
  onQueryChange: onSearchQueryChange,
2785
2818
  onFocus: onSearchFocus,
@@ -2969,8 +3002,7 @@ function GenreTreeView({
2969
3002
  ref: panelRef,
2970
3003
  side: "left",
2971
3004
  title: activePanel?.title ?? "",
2972
- hideTitle: activePanel?.compact,
2973
- hideClose: activePanel?.compact,
3005
+ titleInSearch: activePanel?.titleInSearch,
2974
3006
  headerAccent: activePanel?.headerAccent,
2975
3007
  headerActions: expandButton,
2976
3008
  onClose: activePanel?.onClose ?? noop,
@@ -2979,7 +3011,7 @@ function GenreTreeView({
2979
3011
  onExpandedChange: activePanel?.onExpandedChange,
2980
3012
  aside: activePanel?.aside,
2981
3013
  edgeTab: detailTab,
2982
- headerClassName: "pt-[calc(var(--layout-search-h)+var(--layout-panel-inset)+0.5rem)]",
3014
+ headerClassName: activePanel?.titleInSearch ? void 0 : "pt-[calc(var(--layout-search-h)+var(--layout-panel-inset)+0.5rem)]",
2983
3015
  children: activePanel?.aside && !activePanel.expanded ? /* @__PURE__ */ jsxs26(Fragment14, { children: [
2984
3016
  activePanel.content,
2985
3017
  activePanel.aside
@@ -3437,6 +3469,69 @@ function Rating({ rating, handleChange }) {
3437
3469
  ] }, i)) });
3438
3470
  }
3439
3471
 
3472
+ // src/genre-tree/EntityDetailPanel.tsx
3473
+ import { useState as useState11 } from "react";
3474
+ import { DetailPanel as DetailPanel3 } from "@behindthemusictree/ui";
3475
+ import { Fragment as Fragment15, jsx as jsx39, jsxs as jsxs31 } from "react/jsx-runtime";
3476
+ function EntityDetailPanel({
3477
+ scope,
3478
+ getBackendBaseUrl,
3479
+ getArtistBackendBaseUrl,
3480
+ getShareUrl,
3481
+ songUuid,
3482
+ onSongUuidChange,
3483
+ artistUuid,
3484
+ onArtistUuidChange,
3485
+ renderArtistLink,
3486
+ renderSongLink,
3487
+ collapsed,
3488
+ edgeTab,
3489
+ ...navigation
3490
+ }) {
3491
+ const [expandedSongUuid, setExpandedSongUuid] = useState11(null);
3492
+ const [expandedArtistUuid, setExpandedArtistUuid] = useState11(null);
3493
+ const panel = useEntityPanel({
3494
+ scope,
3495
+ getBackendBaseUrl,
3496
+ getArtistBackendBaseUrl,
3497
+ getShareUrl,
3498
+ renderArtistLink: renderArtistLink ?? ((artist) => artist.isSpecialPurpose ? artist.name : /* @__PURE__ */ jsx39("button", { type: "button", onClick: () => onArtistUuidChange(artist.uuid), className: "hover:underline", children: artist.name })),
3499
+ renderSongLink: renderSongLink ?? ((song) => /* @__PURE__ */ jsx39("button", { type: "button", onClick: () => onSongUuidChange(song.uuid), className: "hover:underline", children: song.title })),
3500
+ song: {
3501
+ uuid: songUuid,
3502
+ expanded: songUuid !== null && expandedSongUuid === songUuid,
3503
+ onExpandedChange: (on) => setExpandedSongUuid(on ? songUuid : null),
3504
+ onClose: () => onSongUuidChange(null)
3505
+ },
3506
+ artist: {
3507
+ uuid: artistUuid,
3508
+ expanded: artistUuid !== null && expandedArtistUuid === artistUuid,
3509
+ onExpandedChange: (on) => setExpandedArtistUuid(on ? artistUuid : null),
3510
+ onClose: () => onArtistUuidChange(null)
3511
+ },
3512
+ ...navigation
3513
+ });
3514
+ if (!panel) return null;
3515
+ return /* @__PURE__ */ jsx39(
3516
+ DetailPanel3,
3517
+ {
3518
+ side: "left",
3519
+ title: panel.title,
3520
+ titleInSearch: true,
3521
+ onClose: panel.onClose,
3522
+ collapsed,
3523
+ expanded: panel.expanded,
3524
+ onExpandedChange: panel.onExpandedChange,
3525
+ aside: panel.extraContent,
3526
+ edgeTab,
3527
+ children: panel.extraContent && !panel.expanded ? /* @__PURE__ */ jsxs31(Fragment15, { children: [
3528
+ panel.content,
3529
+ panel.extraContent
3530
+ ] }) : panel.content
3531
+ }
3532
+ );
3533
+ }
3534
+
3440
3535
  export {
3441
3536
  useGenrePlaylistPlayback,
3442
3537
  GenrePlaylistTreePerRoot,
@@ -3452,13 +3547,15 @@ export {
3452
3547
  GenreDetailPanel,
3453
3548
  TrackPositionPlayPause,
3454
3549
  TrackItem,
3550
+ useEntityPanel,
3455
3551
  GenreTreeView,
3456
3552
  TrackListSidebar,
3457
3553
  PlayerPanel,
3458
3554
  Rating,
3555
+ EntityDetailPanel,
3459
3556
  GenreTreeSkeleton,
3460
3557
  GenreTreeWheelSkeleton2 as GenreTreeWheelSkeleton,
3461
3558
  GenreTreeViewSkeleton2 as GenreTreeViewSkeleton,
3462
3559
  GenreTreeOutlineSkeleton
3463
3560
  };
3464
- //# sourceMappingURL=chunk-RTXLUHJY.js.map
3561
+ //# sourceMappingURL=chunk-57OI426Y.js.map