@behindthemusictree/app-kit-ui 24.0.1 → 26.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.
@@ -4,9 +4,10 @@ import {
4
4
  } from "./chunk-VDXUW7PP.js";
5
5
 
6
6
  // src/genre-tree/GenreTreeView.tsx
7
- import { useCallback as useCallback5, useEffect as useEffect4, useMemo as useMemo6, useRef as useRef4, useState as useState4 } from "react";
7
+ import { useCallback as useCallback5, useDeferredValue, useEffect as useEffect4, useMemo as useMemo6, useRef as useRef4, useState as useState4 } from "react";
8
+ import { useIsMutating } from "@tanstack/react-query";
8
9
  import { Plus as Plus2 } from "lucide-react";
9
- import { IconTextButton, Button, SegmentedControl } from "@behindthemusictree/ui";
10
+ import { IconTextButton, Button, SegmentedControl as SegmentedControl2 } from "@behindthemusictree/ui";
10
11
  import {
11
12
  GenreTreeWheelSkeleton,
12
13
  GenreTreeViewSkeleton
@@ -14,7 +15,7 @@ import {
14
15
  import {
15
16
  useArtistSearch
16
17
  } from "@behindthemusictree/app-kit/genre-tree";
17
- import { useGenreTreeViewState, useArtist as useArtist2 } from "@behindthemusictree/app-kit/genre-tree";
18
+ import { useGenreTreeViewState, useArtist as useArtist2, genreUpdateMutationKey } from "@behindthemusictree/app-kit/genre-tree";
18
19
  import { usePlayer as usePlayer8 } from "@behindthemusictree/app-kit/player";
19
20
  import { usePopup as usePopup4 } from "@behindthemusictree/app-kit/popup";
20
21
 
@@ -511,6 +512,11 @@ import {
511
512
  import { usePlayer as usePlayer3 } from "@behindthemusictree/app-kit/player";
512
513
  import { usePopup as usePopup3 } from "@behindthemusictree/app-kit/popup";
513
514
  import { ErrorCode as ErrorCode3 } from "@behindthemusictree/app-kit/transport";
515
+
516
+ // src/genre-tree/playlist-tree/size-metric.ts
517
+ var genrePlaylistSize = (genrePlaylist, metric) => metric === "views" ? genrePlaylist.viewsCount : genrePlaylist.tracksCount;
518
+
519
+ // src/genre-tree/playlist-tree/TreeWheel.tsx
514
520
  import { capitalize as capitalize3, useTrackNoun as useTrackNoun4 } from "@behindthemusictree/app-kit/track-noun";
515
521
  import { jsx as jsx7 } from "react/jsx-runtime";
516
522
  function GenrePlaylistTreeWheel({
@@ -531,6 +537,9 @@ function GenrePlaylistTreeWheel({
531
537
  allowWheelRotation,
532
538
  showToolbar,
533
539
  controlsOrientation,
540
+ sizeMetric = "songs",
541
+ floatingControlsLeading,
542
+ busy,
534
543
  defaultViewport,
535
544
  onViewportChange,
536
545
  playback,
@@ -546,12 +555,12 @@ function GenrePlaylistTreeWheel({
546
555
  id: genrePlaylist.uuid,
547
556
  parentId: genrePlaylist.parent?.uuid ?? null,
548
557
  name: genrePlaylist.name,
549
- itemCount: genrePlaylist.tracksCount,
558
+ itemCount: genrePlaylistSize(genrePlaylist, sizeMetric),
550
559
  actionable: Boolean(genrePlaylist.criteria),
551
560
  side: genrePlaylist.criteria?.side ?? void 0,
552
561
  wikidataId: genrePlaylist.criteria?.wikidataId ?? null
553
562
  })),
554
- [genrePlaylists]
563
+ [genrePlaylists, sizeMetric]
555
564
  );
556
565
  const handlePlayPause = useCallback3(
557
566
  (nodeId) => {
@@ -603,7 +612,7 @@ function GenrePlaylistTreeWheel({
603
612
  {
604
613
  className,
605
614
  nodes,
606
- itemCountLabel: `${capitalize3(trackNoun.singular)} count`,
615
+ itemCountLabel: sizeMetric === "views" ? "Views" : `${capitalize3(trackNoun.singular)} count`,
607
616
  playingNodeId,
608
617
  playState: isPlaying ? "playing" : "paused",
609
618
  onPlayPause: handlePlayPause,
@@ -621,6 +630,8 @@ function GenrePlaylistTreeWheel({
621
630
  allowWheelRotation,
622
631
  showToolbar,
623
632
  controlsOrientation,
633
+ floatingControlsLeading,
634
+ busy,
624
635
  defaultViewport,
625
636
  onViewportChange,
626
637
  ...infoPanelCuration,
@@ -657,6 +668,9 @@ function GenrePlaylistTreeWheelRadialPopCore(props) {
657
668
  allowWheelRotation,
658
669
  showToolbar,
659
670
  controlsOrientation,
671
+ sizeMetric = "songs",
672
+ floatingControlsLeading,
673
+ busy,
660
674
  outline = false,
661
675
  defaultViewport,
662
676
  onViewportChange,
@@ -664,10 +678,6 @@ function GenrePlaylistTreeWheelRadialPopCore(props) {
664
678
  } = props;
665
679
  const focus = props;
666
680
  const infoPanelCuration = {
667
- editMode: props.editMode,
668
- onEditModeChange: props.onEditModeChange,
669
- renderTitleEditor: props.renderTitleEditor,
670
- renderParentsEditor: props.renderParentsEditor,
671
681
  renderDangerZone: props.renderDangerZone,
672
682
  expanded: props.expanded,
673
683
  onExpandedChange: props.onExpandedChange,
@@ -684,14 +694,15 @@ function GenrePlaylistTreeWheelRadialPopCore(props) {
684
694
  id: genrePlaylist.uuid,
685
695
  parentId: genrePlaylist.parent?.uuid ?? null,
686
696
  name: genrePlaylist.name,
687
- itemCount: genrePlaylist.tracksCount,
697
+ itemCount: genrePlaylistSize(genrePlaylist, sizeMetric),
688
698
  actionable: Boolean(genrePlaylist.criteria),
689
699
  detached: !genrePlaylist.criteria,
690
700
  side: genrePlaylist.criteria?.side ?? void 0,
691
701
  wikidataId: genrePlaylist.criteria?.wikidataId ?? null,
692
- umbrella: genrePlaylist.isUmbrella
702
+ umbrella: genrePlaylist.isUmbrella,
703
+ scene: genrePlaylist.scene ?? void 0
693
704
  })),
694
- [genrePlaylists]
705
+ [genrePlaylists, sizeMetric]
695
706
  );
696
707
  const handlePlayPause = useCallback4(
697
708
  (nodeId) => {
@@ -733,7 +744,7 @@ function GenrePlaylistTreeWheelRadialPopCore(props) {
733
744
  const treeProps = {
734
745
  className,
735
746
  nodes,
736
- itemCountLabel: `${capitalize4(trackNoun.singular)} count`,
747
+ itemCountLabel: sizeMetric === "views" ? "Views" : `${capitalize4(trackNoun.singular)} count`,
737
748
  playingNodeId,
738
749
  playState: isPlaying ? "playing" : "paused",
739
750
  onPlayPause: handlePlayPause,
@@ -758,6 +769,8 @@ function GenrePlaylistTreeWheelRadialPopCore(props) {
758
769
  ...treeProps,
759
770
  allowWheelRotation,
760
771
  controlsOrientation,
772
+ floatingControlsLeading,
773
+ busy,
761
774
  defaultViewport,
762
775
  onViewportChange,
763
776
  ...focus.onFocusGenreChange ? { focusNodeId: focus.focusGenreUuid, onFocusChange: focus.onFocusGenreChange } : { focusNodeId: void 0, onFocusChange: void 0 }
@@ -801,7 +814,7 @@ function GenreTreeWheelHandoff({ skeleton, children, onRevealedChange }) {
801
814
  }
802
815
 
803
816
  // src/genre-tree/GenreSearch.tsx
804
- import { ArrowLeft, Search, X } from "lucide-react";
817
+ import { ArrowLeft, LoaderCircle, Search, X } from "lucide-react";
805
818
  import { Input } from "@behindthemusictree/ui";
806
819
  import { useGenreSearch } from "@behindthemusictree/app-kit/genre-tree";
807
820
  import { jsx as jsx10, jsxs as jsxs6 } from "react/jsx-runtime";
@@ -823,6 +836,7 @@ function GenreSearch({
823
836
  });
824
837
  const extraItems = query !== "" && query !== selectedName ? extraResults?.items ?? [] : [];
825
838
  const label = extraResults ? "Search" : "Search a genre";
839
+ const loading = query !== "" && query !== selectedName && (extraResults?.loading ?? false);
826
840
  const showList = results.length > 0 || extraItems.length > 0;
827
841
  const heading = "px-3 pt-2 pb-1 text-xs font-medium uppercase text-[var(--color-text-muted)]";
828
842
  return /* @__PURE__ */ jsxs6("div", { className: "genre-search relative w-[calc(var(--layout-detail-panel-w,25.5rem)-2*var(--layout-panel-inset,1rem))] shrink-0", children: [
@@ -848,11 +862,34 @@ function GenreSearch({
848
862
  },
849
863
  placeholder: placeholder ?? `${label}\u2026`,
850
864
  "aria-label": label,
865
+ "aria-busy": loading,
851
866
  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"
852
867
  }
853
868
  ),
854
869
  /* @__PURE__ */ jsxs6("div", { className: "absolute right-3 top-1/2 flex items-center -translate-y-1/2", children: [
855
- /* @__PURE__ */ jsx10("span", { className: "p-2 text-[var(--color-text-muted)] pointer-events-none", children: /* @__PURE__ */ jsx10(Search, { "aria-hidden": "true", className: "w-6 h-6" }) }),
870
+ /* @__PURE__ */ jsxs6(
871
+ "span",
872
+ {
873
+ "data-loading": loading,
874
+ className: "relative p-2 text-[var(--color-text-muted)] pointer-events-none",
875
+ children: [
876
+ /* @__PURE__ */ jsx10(
877
+ Search,
878
+ {
879
+ "aria-hidden": "true",
880
+ className: `w-6 h-6 transition-opacity duration-200 motion-reduce:transition-none ${loading ? "opacity-0" : ""}`
881
+ }
882
+ ),
883
+ /* @__PURE__ */ jsx10(
884
+ LoaderCircle,
885
+ {
886
+ "aria-hidden": "true",
887
+ className: `absolute inset-2 w-6 h-6 transition-opacity duration-200 motion-reduce:transition-none ${loading ? "opacity-100 delay-300 animate-spin motion-reduce:animate-none" : "opacity-0"}`
888
+ }
889
+ )
890
+ ]
891
+ }
892
+ ),
856
893
  canClear && /* @__PURE__ */ jsx10(
857
894
  "button",
858
895
  {
@@ -1038,13 +1075,28 @@ function ChangeParentPopup({
1038
1075
  genrePlaylists,
1039
1076
  scope,
1040
1077
  getBackendBaseUrl,
1041
- onClose
1078
+ onClose,
1079
+ onParentChanged
1042
1080
  }) {
1043
1081
  const { mutate, isPending, formErrors } = useUpdateGenre(scope, getBackendBaseUrl);
1044
1082
  const candidates = useMemo5(
1045
1083
  () => getReparentCandidates(genrePlaylists, genrePlaylist.uuid),
1046
1084
  [genrePlaylists, genrePlaylist.uuid]
1047
1085
  );
1086
+ const handleSelect = (newParent) => {
1087
+ const genre = genrePlaylist.criteria.uuid;
1088
+ const parentPlaylistUuid = genrePlaylist.parent?.uuid;
1089
+ const from = parentPlaylistUuid ? genrePlaylists.find(({ uuid }) => uuid === parentPlaylistUuid)?.criteria?.uuid ?? null : null;
1090
+ mutate(
1091
+ { uuid: genre, data: { parent: newParent.criteria.uuid } },
1092
+ {
1093
+ onSuccess: () => {
1094
+ onClose();
1095
+ onParentChanged?.({ genre, from, to: newParent.criteria.uuid });
1096
+ }
1097
+ }
1098
+ );
1099
+ };
1048
1100
  useEffect3(() => {
1049
1101
  const handleKeyDown = (event) => {
1050
1102
  if (event.key === "Escape") onClose();
@@ -1061,7 +1113,7 @@ function ChangeParentPopup({
1061
1113
  GenreSearch,
1062
1114
  {
1063
1115
  genrePlaylists: candidates,
1064
- onSelect: (newParent) => mutate({ uuid: genrePlaylist.criteria.uuid, data: { parent: newParent.criteria.uuid } }, { onSuccess: onClose }),
1116
+ onSelect: handleSelect,
1065
1117
  placeholder: "Search a new parent\u2026"
1066
1118
  }
1067
1119
  ) }),
@@ -1506,8 +1558,30 @@ function useArtistPanel({
1506
1558
  }
1507
1559
  }
1508
1560
 
1561
+ // src/genre-tree/SizeMetricToggle.tsx
1562
+ import { MdMusicNote, MdVisibility } from "react-icons/md";
1563
+ import { SegmentedControl } from "@behindthemusictree/ui";
1564
+ import { jsx as jsx23 } from "react/jsx-runtime";
1565
+ function SizeMetricToggle({
1566
+ value,
1567
+ onChange
1568
+ }) {
1569
+ return /* @__PURE__ */ jsx23(
1570
+ SegmentedControl,
1571
+ {
1572
+ "aria-label": "Size nodes by",
1573
+ value,
1574
+ onChange,
1575
+ options: [
1576
+ { value: "songs", label: /* @__PURE__ */ jsx23(MdMusicNote, { size: 18, "aria-hidden": true }), "aria-label": "Size by songs", title: "Node size by song count" },
1577
+ { value: "views", label: /* @__PURE__ */ jsx23(MdVisibility, { size: 18, "aria-hidden": true }), "aria-label": "Size by views", title: "Node size by song views" }
1578
+ ]
1579
+ }
1580
+ );
1581
+ }
1582
+
1509
1583
  // src/genre-tree/GenreTreeView.tsx
1510
- import { Fragment as Fragment6, jsx as jsx23, jsxs as jsxs17 } from "react/jsx-runtime";
1584
+ import { Fragment as Fragment6, jsx as jsx24, jsxs as jsxs17 } from "react/jsx-runtime";
1511
1585
  var PREVIOUS_BUTTON_CSS = `
1512
1586
  .genre-tree-view .search-overlay .gtv-info-panel-previous { display: none; }
1513
1587
  .genre-tree-view:has(.search-overlay .gtv-info-panel-previous) .genre-search input { padding-left: 2.75rem; }
@@ -1529,7 +1603,7 @@ function GenreNodeActionBand({ genrePlaylist, scope, playback, ...bandProps }) {
1529
1603
  console.error("Failed to load genre playlist tracks:", error);
1530
1604
  });
1531
1605
  };
1532
- return /* @__PURE__ */ jsx23(
1606
+ return /* @__PURE__ */ jsx24(
1533
1607
  GenreActionBand,
1534
1608
  {
1535
1609
  ...bandProps,
@@ -1544,6 +1618,7 @@ function GenreTreeView({
1544
1618
  scope,
1545
1619
  handleGenreCreationAction,
1546
1620
  handleGenreRenameAction,
1621
+ onParentChanged,
1547
1622
  getBackendBaseUrl,
1548
1623
  additionalActions,
1549
1624
  viewMode: controlledViewMode,
@@ -1559,6 +1634,8 @@ function GenreTreeView({
1559
1634
  criteriaOverviewSchema,
1560
1635
  renderGenreDetailExtras,
1561
1636
  controlsOrientation = "horizontal",
1637
+ sizeMetric: controlledSizeMetric,
1638
+ onSizeMetricChange,
1562
1639
  renderArtistLink,
1563
1640
  artistUuid: controlledArtistUuid,
1564
1641
  onArtistUuidChange,
@@ -1572,10 +1649,6 @@ function GenreTreeView({
1572
1649
  renderSongLeading,
1573
1650
  renderSongActions,
1574
1651
  renderTracksToolbar,
1575
- editMode,
1576
- onEditModeChange,
1577
- renderTitleEditor,
1578
- renderParentsEditor,
1579
1652
  renderDangerZone,
1580
1653
  expanded,
1581
1654
  onExpandedChange,
@@ -1588,12 +1661,20 @@ function GenreTreeView({
1588
1661
  const [internalArtistUuid, setInternalArtistUuid] = useState4(null);
1589
1662
  const isArtistControlled = controlledArtistUuid !== void 0;
1590
1663
  const artistUuid = isArtistControlled ? controlledArtistUuid : internalArtistUuid;
1664
+ const [internalSizeMetric, setInternalSizeMetric] = useState4("songs");
1665
+ const sizeMetric = controlledSizeMetric ?? internalSizeMetric;
1666
+ const deferredSizeMetric = useDeferredValue(sizeMetric);
1667
+ const changeSizeMetric = (metric) => {
1668
+ if (controlledSizeMetric === void 0) setInternalSizeMetric(metric);
1669
+ onSizeMetricChange?.(metric);
1670
+ };
1591
1671
  const changeArtist = (uuid) => {
1592
1672
  if (!isArtistControlled) setInternalArtistUuid(uuid);
1593
1673
  onArtistUuidChange?.(uuid);
1594
1674
  };
1595
1675
  const {
1596
1676
  isLoading,
1677
+ isFetching: isTreeFetching,
1597
1678
  genrePlaylistByNodeId,
1598
1679
  visibleGenrePlaylists,
1599
1680
  groupedGenrePlaylistsByRoot,
@@ -1618,6 +1699,8 @@ function GenreTreeView({
1618
1699
  criteriaOverviewSchema,
1619
1700
  artistUuid
1620
1701
  });
1702
+ const isGenreUpdating = useIsMutating({ mutationKey: genreUpdateMutationKey }) > 0;
1703
+ const busy = deferredSizeMetric !== sizeMetric || isTreeFetching || isGenreUpdating;
1621
1704
  const artistResult = useArtist2(artistUuid, artistSearchBaseUrl ?? getBackendBaseUrl);
1622
1705
  const filterArtistName = artistResult.status === "success" ? artistResult.artist.name : null;
1623
1706
  const filterArtist = useMemo6(
@@ -1634,7 +1717,7 @@ function GenreTreeView({
1634
1717
  const artistPanel = useArtistPanel({
1635
1718
  uuid: artistUuid,
1636
1719
  getBackendBaseUrl: artistBaseUrl,
1637
- renderArtistLink: renderArtistLink ?? ((artist) => artist.isSpecialPurpose ? artist.name : /* @__PURE__ */ jsx23("button", { type: "button", onClick: () => openArtist(artist.uuid), className: "hover:underline", children: artist.name })),
1720
+ renderArtistLink: renderArtistLink ?? ((artist) => artist.isSpecialPurpose ? artist.name : /* @__PURE__ */ jsx24("button", { type: "button", onClick: () => openArtist(artist.uuid), className: "hover:underline", children: artist.name })),
1638
1721
  expanded: controlledArtistExpanded ?? (artistUuid !== null && expandedArtistUuid === artistUuid),
1639
1722
  onExpandedChange: (on) => {
1640
1723
  if (controlledArtistExpanded === void 0) setExpandedArtistUuid(on ? artistUuid : null);
@@ -1672,10 +1755,6 @@ function GenreTreeView({
1672
1755
  const detailExpanded = detailOverlay ? !!detailOverlay.expanded : isExpanded;
1673
1756
  const onDetailExpandedChange = detailOverlay ? detailOverlay.onExpandedChange : changeExpanded;
1674
1757
  const infoPanelCuration = {
1675
- editMode,
1676
- onEditModeChange,
1677
- renderTitleEditor,
1678
- renderParentsEditor,
1679
1758
  renderDangerZone,
1680
1759
  expanded: placement === "regions" ? isExpanded : expanded,
1681
1760
  onExpandedChange: placement === "regions" ? changeExpanded : onExpandedChange,
@@ -1715,7 +1794,8 @@ function GenreTreeView({
1715
1794
  onArtistSelect?.(artist);
1716
1795
  }
1717
1796
  })),
1718
- status: artistSearchResult.isError ? "Search failed" : artistSearchResult.settled && artistSearchResult.artists.length === 0 ? "No artists found" : ""
1797
+ status: artistSearchResult.isError ? "Search failed" : artistSearchResult.settled && artistSearchResult.artists.length === 0 ? "No artists found" : "",
1798
+ loading: artistSearchResult.pending
1719
1799
  } : void 0;
1720
1800
  const handleSelectedNodeChange = (node) => {
1721
1801
  selectNode(node);
@@ -1744,7 +1824,7 @@ function GenreTreeView({
1744
1824
  return {
1745
1825
  primary: /* @__PURE__ */ jsxs17(Fragment6, { children: [
1746
1826
  toolbar,
1747
- /* @__PURE__ */ jsx23(
1827
+ /* @__PURE__ */ jsx24(
1748
1828
  GenreNodeActionBand,
1749
1829
  {
1750
1830
  node,
@@ -1754,14 +1834,15 @@ function GenreTreeView({
1754
1834
  onAddChild: readOnly ? void 0 : () => handleGenreCreationAction(criteria),
1755
1835
  onRename: readOnly ? void 0 : () => handleGenreRenameAction(criteria),
1756
1836
  onChangeParent: readOnly ? void 0 : () => showPopup(
1757
- /* @__PURE__ */ jsx23(
1837
+ /* @__PURE__ */ jsx24(
1758
1838
  ChangeParentPopup,
1759
1839
  {
1760
1840
  genrePlaylist: { ...genrePlaylist, criteria },
1761
1841
  genrePlaylists: visibleGenrePlaylists,
1762
1842
  scope,
1763
1843
  getBackendBaseUrl,
1764
- onClose: hidePopup
1844
+ onClose: hidePopup,
1845
+ onParentChanged
1765
1846
  }
1766
1847
  )
1767
1848
  ),
@@ -1769,7 +1850,7 @@ function GenreTreeView({
1769
1850
  playback
1770
1851
  }
1771
1852
  ),
1772
- /* @__PURE__ */ jsx23(
1853
+ /* @__PURE__ */ jsx24(
1773
1854
  GenreDetailExtras,
1774
1855
  {
1775
1856
  family,
@@ -1801,6 +1882,7 @@ function GenreTreeView({
1801
1882
  handleGenreRenameAction,
1802
1883
  showPopup,
1803
1884
  hidePopup,
1885
+ onParentChanged,
1804
1886
  visibleGenrePlaylists,
1805
1887
  playback,
1806
1888
  filterArtist,
@@ -1818,7 +1900,7 @@ function GenreTreeView({
1818
1900
  );
1819
1901
  const isRotationControlled = controlledAllowWheelRotation !== void 0;
1820
1902
  const allowWheelRotation = controlledAllowWheelRotation ?? internalAllowWheelRotation;
1821
- const detailTab = placement === "regions" && (selectedNodeId || detailOverlay) ? /* @__PURE__ */ jsx23(
1903
+ const detailTab = placement === "regions" && (selectedNodeId || detailOverlay) ? /* @__PURE__ */ jsx24(
1822
1904
  PanelEdgeTab,
1823
1905
  {
1824
1906
  side: "left",
@@ -1827,7 +1909,7 @@ function GenreTreeView({
1827
1909
  }
1828
1910
  ) : null;
1829
1911
  const actions = /* @__PURE__ */ jsxs17(Fragment6, { children: [
1830
- !isLoading && /* @__PURE__ */ jsx23(
1912
+ !isLoading && /* @__PURE__ */ jsx24(
1831
1913
  GenreSearch,
1832
1914
  {
1833
1915
  genrePlaylists: visibleGenrePlaylists,
@@ -1840,7 +1922,7 @@ function GenreTreeView({
1840
1922
  }
1841
1923
  ),
1842
1924
  !isLoading && filterArtist && /* @__PURE__ */ jsxs17("span", { className: "inline-flex items-center overflow-hidden rounded-full bg-[var(--color-neutral-100)] text-sm", children: [
1843
- /* @__PURE__ */ jsx23(
1925
+ /* @__PURE__ */ jsx24(
1844
1926
  "button",
1845
1927
  {
1846
1928
  type: "button",
@@ -1852,7 +1934,7 @@ function GenreTreeView({
1852
1934
  children: filterArtist.name
1853
1935
  }
1854
1936
  ),
1855
- /* @__PURE__ */ jsx23(
1937
+ /* @__PURE__ */ jsx24(
1856
1938
  "button",
1857
1939
  {
1858
1940
  type: "button",
@@ -1863,8 +1945,8 @@ function GenreTreeView({
1863
1945
  }
1864
1946
  )
1865
1947
  ] }),
1866
- !isLoading && !isControlled && /* @__PURE__ */ jsx23(
1867
- SegmentedControl,
1948
+ !isLoading && !isControlled && /* @__PURE__ */ jsx24(
1949
+ SegmentedControl2,
1868
1950
  {
1869
1951
  "aria-label": "Tree view mode",
1870
1952
  value: viewMode,
@@ -1877,13 +1959,13 @@ function GenreTreeView({
1877
1959
  ]
1878
1960
  }
1879
1961
  ),
1880
- !isLoading && showDisplayOptions && /* @__PURE__ */ jsx23(
1962
+ !isLoading && showDisplayOptions && /* @__PURE__ */ jsx24(
1881
1963
  "div",
1882
1964
  {
1883
1965
  className: "flex items-center gap-1",
1884
1966
  role: "group",
1885
1967
  "aria-label": "Tree display options",
1886
- children: !isRotationControlled && viewMode !== "stacked" && viewMode !== "outline" && /* @__PURE__ */ jsx23(
1968
+ children: !isRotationControlled && viewMode !== "stacked" && viewMode !== "outline" && /* @__PURE__ */ jsx24(
1887
1969
  Button,
1888
1970
  {
1889
1971
  variant: allowWheelRotation ? "default" : "outline",
@@ -1894,7 +1976,7 @@ function GenreTreeView({
1894
1976
  )
1895
1977
  }
1896
1978
  ),
1897
- !isLoading && !readOnly && showAddRoot && /* @__PURE__ */ jsx23(
1979
+ !isLoading && !readOnly && showAddRoot && /* @__PURE__ */ jsx24(
1898
1980
  IconTextButton,
1899
1981
  {
1900
1982
  icon: Plus2,
@@ -1909,27 +1991,27 @@ function GenreTreeView({
1909
1991
  ref: rootRef,
1910
1992
  className: ["genre-tree-view relative flex flex-col h-full", placement === "regions" && REGIONS_CLASS, className].filter(Boolean).join(" "),
1911
1993
  children: [
1912
- /* @__PURE__ */ jsx23("style", { children: PREVIOUS_BUTTON_CSS }),
1913
- /* @__PURE__ */ jsx23(
1994
+ /* @__PURE__ */ jsx24("style", { children: PREVIOUS_BUTTON_CSS }),
1995
+ /* @__PURE__ */ jsx24(
1914
1996
  "div",
1915
1997
  {
1916
1998
  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))]" : "left-5 top-5"}`,
1917
1999
  children: actions
1918
2000
  }
1919
2001
  ),
1920
- detailTab && /* @__PURE__ */ jsx23(
2002
+ detailTab && /* @__PURE__ */ jsx24(
1921
2003
  "div",
1922
2004
  {
1923
2005
  className: `absolute top-1/2 z-30 -translate-y-1/2 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))]"}`,
1924
2006
  children: detailTab
1925
2007
  }
1926
2008
  ),
1927
- /* @__PURE__ */ jsx23("div", { className: "content-container flex-1 min-h-0 flex flex-row gap-4", children: /* @__PURE__ */ jsx23("div", { className: "tree-view-container flex-1 min-w-0 flex flex-col h-full", children: isLoading ? /* @__PURE__ */ jsx23(GenreTreeViewSkeleton, { viewMode }) : viewMode === "wheel" ? (
2009
+ /* @__PURE__ */ jsx24("div", { className: "content-container flex-1 min-h-0 flex flex-row gap-4", children: /* @__PURE__ */ jsx24("div", { className: "tree-view-container flex-1 min-w-0 flex flex-col h-full", children: isLoading ? /* @__PURE__ */ jsx24(GenreTreeViewSkeleton, { viewMode }) : viewMode === "wheel" ? (
1928
2010
  // The info panel's top edge aligns with the tree's; the search bar floats `--layout-panel-inset` below it,
1929
2011
  // and the header inset (the bar's height plus that inset) extends the header's fill behind the bar,
1930
2012
  // Google Maps style; the 8px bottom padding mirrors the inset above the bar, and the hidden title's height is zeroed.
1931
2013
  // The panel's own Zoom in button is hidden: the search bar carries it.
1932
- /* @__PURE__ */ jsx23("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__ */ jsx23(GenreTreeWheelHandoff, { skeleton: /* @__PURE__ */ jsx23(GenreTreeWheelSkeleton, {}), onRevealedChange: setIsWheelRevealed, children: /* @__PURE__ */ jsx23(
2014
+ /* @__PURE__ */ jsx24("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__ */ jsx24(GenreTreeWheelHandoff, { skeleton: /* @__PURE__ */ jsx24(GenreTreeWheelSkeleton, {}), onRevealedChange: setIsWheelRevealed, children: /* @__PURE__ */ jsx24(
1933
2015
  GenrePlaylistTreeWheel,
1934
2016
  {
1935
2017
  scope,
@@ -1947,6 +2029,9 @@ function GenreTreeView({
1947
2029
  readOnly,
1948
2030
  allowWheelRotation,
1949
2031
  controlsOrientation,
2032
+ sizeMetric: filterArtist ? "songs" : deferredSizeMetric,
2033
+ busy,
2034
+ floatingControlsLeading: filterArtist ? void 0 : /* @__PURE__ */ jsx24(SizeMetricToggle, { value: sizeMetric, onChange: changeSizeMetric }),
1950
2035
  defaultViewport,
1951
2036
  onViewportChange,
1952
2037
  ...infoPanelCuration
@@ -1954,7 +2039,7 @@ function GenreTreeView({
1954
2039
  ) }) })
1955
2040
  ) : viewMode === "pop-core" ? (
1956
2041
  // Same as the wheel: the header inset extends its fill behind the floating search bar, Google Maps style.
1957
- /* @__PURE__ */ jsx23("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__ */ jsx23(GenreTreeWheelHandoff, { skeleton: /* @__PURE__ */ jsx23(GenreTreeWheelSkeleton, {}), onRevealedChange: setIsWheelRevealed, children: /* @__PURE__ */ jsx23(
2042
+ /* @__PURE__ */ jsx24("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__ */ jsx24(GenreTreeWheelHandoff, { skeleton: /* @__PURE__ */ jsx24(GenreTreeWheelSkeleton, {}), onRevealedChange: setIsWheelRevealed, children: /* @__PURE__ */ jsx24(
1958
2043
  GenrePlaylistTreeWheelRadialPopCore,
1959
2044
  {
1960
2045
  scope,
@@ -1972,13 +2057,16 @@ function GenreTreeView({
1972
2057
  readOnly,
1973
2058
  allowWheelRotation,
1974
2059
  controlsOrientation,
2060
+ sizeMetric: filterArtist ? "songs" : deferredSizeMetric,
2061
+ busy,
2062
+ floatingControlsLeading: filterArtist ? void 0 : /* @__PURE__ */ jsx24(SizeMetricToggle, { value: sizeMetric, onChange: changeSizeMetric }),
1975
2063
  defaultViewport,
1976
2064
  onViewportChange,
1977
2065
  ...infoPanelCuration,
1978
2066
  ...focus
1979
2067
  }
1980
2068
  ) }) })
1981
- ) : viewMode === "outline" ? /* @__PURE__ */ jsx23("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__ */ jsx23(
2069
+ ) : viewMode === "outline" ? /* @__PURE__ */ jsx24("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__ */ jsx24(
1982
2070
  GenrePlaylistTreeWheelRadialPopCore,
1983
2071
  {
1984
2072
  outline: true,
@@ -1997,13 +2085,13 @@ function GenreTreeView({
1997
2085
  readOnly,
1998
2086
  ...infoPanelCuration
1999
2087
  }
2000
- ) }) : /* @__PURE__ */ jsx23("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(
2088
+ ) }) : /* @__PURE__ */ jsx24("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(
2001
2089
  ([uuid, genrePlaylistTreePerRoot], rootIndex) => {
2002
- return /* @__PURE__ */ jsx23(
2090
+ return /* @__PURE__ */ jsx24(
2003
2091
  "div",
2004
2092
  {
2005
2093
  className: "tree-per-root-container relative shrink-0 h-[28rem] mt-2 mr-16 p-2 bg-[var(--color-neutral-100)] rounded-lg",
2006
- children: /* @__PURE__ */ jsx23("div", { className: "graph-container relative z-10 h-full", children: /* @__PURE__ */ jsx23(
2094
+ children: /* @__PURE__ */ jsx24("div", { className: "graph-container relative z-10 h-full", children: /* @__PURE__ */ jsx24(
2007
2095
  GenrePlaylistTreePerRoot,
2008
2096
  {
2009
2097
  scope,
@@ -2052,7 +2140,7 @@ import { useTrackList as useTrackList5 } from "@behindthemusictree/app-kit/genre
2052
2140
  import { useTrackListSidebarVisibility } from "@behindthemusictree/app-kit/genre-tree";
2053
2141
  import { TrackListOriginType as TrackListOriginType2 } from "@behindthemusictree/app-kit/genre-tree";
2054
2142
  import { useTrackNoun as useTrackNoun6 } from "@behindthemusictree/app-kit/track-noun";
2055
- import { jsx as jsx24, jsxs as jsxs18 } from "react/jsx-runtime";
2143
+ import { jsx as jsx25, jsxs as jsxs18 } from "react/jsx-runtime";
2056
2144
  function TrackListSidebar({
2057
2145
  className = "",
2058
2146
  renderDuration,
@@ -2089,14 +2177,14 @@ function TrackListSidebar({
2089
2177
  children: [
2090
2178
  /* @__PURE__ */ jsxs18("div", { className: "header flex h-16 px-4 py-2 text-[var(--color-text)]", children: [
2091
2179
  /* @__PURE__ */ jsxs18("div", { className: "origin flex text-xl ", children: [
2092
- /* @__PURE__ */ jsx24("div", { className: "from h-auto flex flex-col justify-center items-center mr-2", children: "From" }),
2093
- /* @__PURE__ */ jsx24("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__ */ jsx24("div", { className: "name max-w-full truncate", children: trackList.origin.label }) })
2180
+ /* @__PURE__ */ jsx25("div", { className: "from h-auto flex flex-col justify-center items-center mr-2", children: "From" }),
2181
+ /* @__PURE__ */ jsx25("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__ */ jsx25("div", { className: "name max-w-full truncate", children: trackList.origin.label }) })
2094
2182
  ] }),
2095
2183
  /* @__PURE__ */ jsxs18("div", { className: "info flex flex-col justify-center items-center pt-1 mr-2 text-[var(--color-text-muted)]", children: [
2096
2184
  trackList.origin.type === TrackListOriginType2.GENRE_PLAYLIST ? "\u2022 Genre playlist \u2022 " : trackList.origin.type === TrackListOriginType2.ARTIST ? "\u2022 Artist \u2022 " : `\u2022 ${trackNoun.singular} playlist \u2022 `,
2097
2185
  `${trackList.total} ${trackList.total > 1 ? trackNoun.plural : trackNoun.singular} \u2022`
2098
2186
  ] }),
2099
- /* @__PURE__ */ jsx24(
2187
+ /* @__PURE__ */ jsx25(
2100
2188
  "button",
2101
2189
  {
2102
2190
  type: "button",
@@ -2112,7 +2200,7 @@ function TrackListSidebar({
2112
2200
  {
2113
2201
  className: layout === "inline" ? "track-list flex-1 min-h-0 overflow-auto list-none p-0 m-0" : "track-list overflow-auto max-h-[calc(100vh-63.5px-79px-56px-3.5px)] list-none p-0 m-0",
2114
2202
  children: [
2115
- trackList.tracks.map((track, index) => /* @__PURE__ */ jsx24("li", { children: /* @__PURE__ */ jsx24(
2203
+ trackList.tracks.map((track, index) => /* @__PURE__ */ jsx25("li", { children: /* @__PURE__ */ jsx25(
2116
2204
  TrackItem,
2117
2205
  {
2118
2206
  track,
@@ -2122,7 +2210,7 @@ function TrackListSidebar({
2122
2210
  renderArtistLink
2123
2211
  }
2124
2212
  ) }, track.uuid)),
2125
- hasMore && /* @__PURE__ */ jsx24("li", { ref: loadMoreSentinelRef, "aria-hidden": "true", className: "h-px" })
2213
+ hasMore && /* @__PURE__ */ jsx25("li", { ref: loadMoreSentinelRef, "aria-hidden": "true", className: "h-px" })
2126
2214
  ]
2127
2215
  }
2128
2216
  )
@@ -2142,7 +2230,7 @@ import {
2142
2230
  import {
2143
2231
  useTrackListSidebarVisibility as useTrackListSidebarVisibility2
2144
2232
  } from "@behindthemusictree/app-kit/genre-tree";
2145
- import { jsx as jsx25, jsxs as jsxs19 } from "react/jsx-runtime";
2233
+ import { jsx as jsx26, jsxs as jsxs19 } from "react/jsx-runtime";
2146
2234
  var PANEL_ID = "player-panel";
2147
2235
  var iconButtonClassName = "flex h-6 w-6 shrink-0 items-center justify-center p-0 rounded-md hover:bg-white/10";
2148
2236
  var DEFAULT_EXTENDED_WIDTH = "calc(100vw - var(--layout-center-left) - var(--layout-gutter))";
@@ -2185,7 +2273,7 @@ function PlayerPanel({
2185
2273
  ),
2186
2274
  style: isExtended ? { width: extendedWidth } : void 0,
2187
2275
  children: [
2188
- /* @__PURE__ */ jsx25(
2276
+ /* @__PURE__ */ jsx26(
2189
2277
  PanelEdgeTab,
2190
2278
  {
2191
2279
  side: "right",
@@ -2205,24 +2293,24 @@ function PlayerPanel({
2205
2293
  children: [
2206
2294
  /* @__PURE__ */ jsxs19("div", { className: "flex w-[var(--layout-right-w)] shrink-0 flex-col", children: [
2207
2295
  /* @__PURE__ */ jsxs19("div", { className: "flex shrink-0 items-center justify-between gap-2 border-b border-zinc-800 p-2", children: [
2208
- /* @__PURE__ */ jsx25("span", { className: "min-w-0 truncate text-sm font-semibold", children: playerTrackObject?.track?.title }),
2209
- /* @__PURE__ */ jsx25("div", { className: "flex shrink-0 items-center gap-1", children: /* @__PURE__ */ jsx25(
2296
+ /* @__PURE__ */ jsx26("span", { className: "min-w-0 truncate text-sm font-semibold", children: playerTrackObject?.track?.title }),
2297
+ /* @__PURE__ */ jsx26("div", { className: "flex shrink-0 items-center gap-1", children: /* @__PURE__ */ jsx26(
2210
2298
  "button",
2211
2299
  {
2212
2300
  type: "button",
2213
2301
  onClick: onClose,
2214
2302
  className: iconButtonClassName,
2215
2303
  "aria-label": "Close player",
2216
- children: /* @__PURE__ */ jsx25(X2, { size: 16 })
2304
+ children: /* @__PURE__ */ jsx26(X2, { size: 16 })
2217
2305
  }
2218
2306
  ) })
2219
2307
  ] }),
2220
2308
  /* @__PURE__ */ jsxs19("div", { className: "relative w-full shrink-0", children: [
2221
- /* @__PURE__ */ jsx25(PlayerVideoSurface, { className: "aspect-video w-full bg-black" }),
2222
- playerTrackObject?.loadError && /* @__PURE__ */ jsx25("span", { className: "absolute bottom-2 left-2 right-2 text-xs text-red-400", children: playerTrackObject.loadError })
2309
+ /* @__PURE__ */ jsx26(PlayerVideoSurface, { className: "aspect-video w-full bg-black" }),
2310
+ playerTrackObject?.loadError && /* @__PURE__ */ jsx26("span", { className: "absolute bottom-2 left-2 right-2 text-xs text-red-400", children: playerTrackObject.loadError })
2223
2311
  ] })
2224
2312
  ] }),
2225
- isTrackListSidebarVisible && /* @__PURE__ */ jsx25(
2313
+ isTrackListSidebarVisible && /* @__PURE__ */ jsx26(
2226
2314
  TrackListSidebar,
2227
2315
  {
2228
2316
  layout: "inline",
@@ -2245,11 +2333,11 @@ function PlayerPanel({
2245
2333
  import { v4 as uuidv4 } from "uuid";
2246
2334
  import { AiFillStar, AiOutlineStar } from "react-icons/ai";
2247
2335
  import { FORM_RATING_NULL_VALUE } from "@behindthemusictree/app-kit/genre-tree";
2248
- import { jsx as jsx26, jsxs as jsxs20 } from "react/jsx-runtime";
2336
+ import { jsx as jsx27, jsxs as jsxs20 } from "react/jsx-runtime";
2249
2337
  function Rating({ rating, handleChange }) {
2250
2338
  const instanceId = uuidv4();
2251
- return /* @__PURE__ */ jsx26("div", { className: "flex items-center", children: [...Array(6)].map((_, i) => /* @__PURE__ */ jsxs20("div", { className: `relative ${i === 0 ? "mr-4" : "mr-1"}`, children: [
2252
- /* @__PURE__ */ jsx26(
2339
+ return /* @__PURE__ */ jsx27("div", { className: "flex items-center", children: [...Array(6)].map((_, i) => /* @__PURE__ */ jsxs20("div", { className: `relative ${i === 0 ? "mr-4" : "mr-1"}`, children: [
2340
+ /* @__PURE__ */ jsx27(
2253
2341
  "input",
2254
2342
  {
2255
2343
  id: `star-${instanceId}-${i}`,
@@ -2261,16 +2349,16 @@ function Rating({ rating, handleChange }) {
2261
2349
  onChange: handleChange
2262
2350
  }
2263
2351
  ),
2264
- /* @__PURE__ */ jsx26("label", { htmlFor: `star-${instanceId}-${i}`, className: "cursor-pointer", children: i === 0 ? /* @__PURE__ */ jsxs20("div", { className: "flex", children: [
2265
- /* @__PURE__ */ jsx26(
2352
+ /* @__PURE__ */ jsx27("label", { htmlFor: `star-${instanceId}-${i}`, className: "cursor-pointer", children: i === 0 ? /* @__PURE__ */ jsxs20("div", { className: "flex", children: [
2353
+ /* @__PURE__ */ jsx27(
2266
2354
  AiOutlineStar,
2267
2355
  {
2268
2356
  className: `w-7 h-7 absolute top-0 left-0 ${rating === FORM_RATING_NULL_VALUE ? "text-[var(--color-accent-rating)]" : "text-[var(--color-text-muted)]"}`
2269
2357
  }
2270
2358
  ),
2271
- /* @__PURE__ */ jsx26("span", { className: "flex-grow w-8" }),
2359
+ /* @__PURE__ */ jsx27("span", { className: "flex-grow w-8" }),
2272
2360
  " "
2273
- ] }) : /* @__PURE__ */ jsx26(
2361
+ ] }) : /* @__PURE__ */ jsx27(
2274
2362
  AiFillStar,
2275
2363
  {
2276
2364
  className: `w-7 h-7 absolute top-0 left-0 ${(rating ?? -1) >= i ? "text-[var(--color-accent-rating)]" : "text-[var(--color-text-muted)]"}`
@@ -2294,4 +2382,4 @@ export {
2294
2382
  GenreTreeViewSkeleton2 as GenreTreeViewSkeleton,
2295
2383
  GenreTreeOutlineSkeleton
2296
2384
  };
2297
- //# sourceMappingURL=chunk-BPCFNBFO.js.map
2385
+ //# sourceMappingURL=chunk-KPWV4LEJ.js.map