@behindthemusictree/app-kit-ui 30.0.0 → 32.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.
@@ -5,13 +5,17 @@ import {
5
5
  } from "./chunk-DKQQLLMY.js";
6
6
 
7
7
  // src/genre-tree/GenreTreeView.tsx
8
- import { useCallback as useCallback6, useDeferredValue, useEffect as useEffect6, useMemo as useMemo7, useRef as useRef4, useState as useState7 } from "react";
8
+ import { useCallback as useCallback6, useDeferredValue, useEffect as useEffect5, useMemo as useMemo7, useRef as useRef3, useState as useState8 } from "react";
9
9
  import { useIsMutating } from "@tanstack/react-query";
10
10
  import { Plus as Plus2 } from "lucide-react";
11
- import { IconTextButton, Button, SegmentedControl as SegmentedControl2 } from "@behindthemusictree/ui";
11
+ import { Maximize2, Minimize2 } from "lucide-react";
12
+ import { IconTextButton, IconButton, Button, SegmentedControl as SegmentedControl2, DetailPanel, useObscuredInset } from "@behindthemusictree/ui";
12
13
  import {
14
+ GenreNodeAncestors,
15
+ GenreNodeFamily,
13
16
  GenreTreeWheelSkeleton,
14
- GenreTreeViewSkeleton
17
+ GenreTreeViewSkeleton,
18
+ useGenreNodeRelations
15
19
  } from "@behindthemusictree/genre-tree-view";
16
20
  import {
17
21
  useArtistSearch,
@@ -21,6 +25,32 @@ import {
21
25
  import { useGenreTreeViewState, useArtist as useArtist2, useUpdateGenre as useUpdateGenre2, genreUpdateMutationKey } from "@behindthemusictree/app-kit/genre-tree";
22
26
  import { usePopup as usePopup5 } from "@behindthemusictree/app-kit/popup";
23
27
 
28
+ // src/genre-tree/playlist-tree/size-metric.ts
29
+ var hasViewsCount = (genrePlaylists) => genrePlaylists.every((genrePlaylist) => genrePlaylist.viewsCount !== void 0);
30
+ var genrePlaylistSize = (genrePlaylist, metric) => {
31
+ if (metric === "songs") return genrePlaylist.tracksCount;
32
+ if (genrePlaylist.viewsCount === void 0) {
33
+ throw new Error(`Genre playlist ${genrePlaylist.uuid} has no viewsCount to size by views`);
34
+ }
35
+ return genrePlaylist.viewsCount;
36
+ };
37
+
38
+ // src/genre-tree/playlist-tree/toTreeNodes.ts
39
+ function toTreeNodes(genrePlaylists, sizeMetric) {
40
+ return genrePlaylists.map((genrePlaylist) => ({
41
+ id: genrePlaylist.uuid,
42
+ parentId: genrePlaylist.parent?.uuid ?? null,
43
+ name: genrePlaylist.name,
44
+ itemCount: genrePlaylistSize(genrePlaylist, sizeMetric),
45
+ actionable: Boolean(genrePlaylist.criteria),
46
+ detached: !genrePlaylist.criteria,
47
+ side: genrePlaylist.criteria?.side ?? void 0,
48
+ wikidataId: genrePlaylist.criteria?.wikidataId ?? null,
49
+ umbrella: genrePlaylist.isUmbrella,
50
+ scene: genrePlaylist.scene ?? void 0
51
+ }));
52
+ }
53
+
24
54
  // src/genre-tree/playlist-tree/TreePerRoot.tsx
25
55
  import { useCallback as useCallback2, useMemo as useMemo2 } from "react";
26
56
  import {
@@ -63,7 +93,6 @@ function useGenrePlaylistPlayback({
63
93
  }
64
94
 
65
95
  // src/genre-tree/playlist-tree/TreePerRoot.tsx
66
- import { capitalize, useTrackNoun } from "@behindthemusictree/app-kit/track-noun";
67
96
  import { jsx } from "react/jsx-runtime";
68
97
  function GenrePlaylistTreePerRoot({
69
98
  scope,
@@ -75,24 +104,19 @@ function GenrePlaylistTreePerRoot({
75
104
  additionalActions,
76
105
  onNodeClick,
77
106
  onNodeHover,
78
- renderInfoPanelBody,
79
107
  selectedNodeId,
80
108
  onSelectedNodeChange,
81
- hideInfoPanelClose,
82
- hideInfoPanelTitle,
83
109
  readOnly = false,
84
- showToolbar,
85
110
  wheelZoom,
86
111
  defaultViewport,
87
112
  viewportRestore,
88
113
  onViewportChange,
89
114
  playback,
90
- ...infoPanelCuration
115
+ ...hostPanel
91
116
  }) {
92
117
  const { isPlaying, setIsPlaying } = usePlayer();
93
118
  const defaultPlayback = useGenrePlaylistPlayback({ scope, genrePlaylists: genrePlaylistTreePerRoot });
94
119
  const { playingNodeId, play } = playback ?? defaultPlayback;
95
- const trackNoun = useTrackNoun();
96
120
  const { showPopup } = usePopup();
97
121
  const nodes = useMemo2(
98
122
  () => genrePlaylistTreePerRoot.map((genrePlaylist) => ({
@@ -157,7 +181,6 @@ function GenrePlaylistTreePerRoot({
157
181
  {
158
182
  className: className2,
159
183
  nodes,
160
- itemCountLabel: `${capitalize(trackNoun.singular)} count`,
161
184
  rootColor: getGenreTreeColor(rootUuid),
162
185
  playingNodeId,
163
186
  playState: isPlaying ? "playing" : "paused",
@@ -168,18 +191,13 @@ function GenrePlaylistTreePerRoot({
168
191
  additionalActions,
169
192
  onNodeClick,
170
193
  onNodeHover,
171
- renderInfoPanelBody,
172
194
  selectedNodeId,
173
195
  onSelectedNodeChange,
174
- hideInfoPanelClose,
175
- hideInfoPanelTitle,
176
- showToolbar,
177
196
  wheelZoom,
178
197
  defaultViewport,
179
198
  viewportRestore,
180
199
  onViewportChange,
181
- ...infoPanelCuration,
182
- readOnly
200
+ ...hostPanel
183
201
  }
184
202
  );
185
203
  }
@@ -192,19 +210,6 @@ import {
192
210
  import { usePlayer as usePlayer2 } from "@behindthemusictree/app-kit/player";
193
211
  import { usePopup as usePopup2 } from "@behindthemusictree/app-kit/popup";
194
212
  import { ErrorCode as ErrorCode2 } from "@behindthemusictree/app-kit/transport";
195
-
196
- // src/genre-tree/playlist-tree/size-metric.ts
197
- var hasViewsCount = (genrePlaylists) => genrePlaylists.every((genrePlaylist) => genrePlaylist.viewsCount !== void 0);
198
- var genrePlaylistSize = (genrePlaylist, metric) => {
199
- if (metric === "songs") return genrePlaylist.tracksCount;
200
- if (genrePlaylist.viewsCount === void 0) {
201
- throw new Error(`Genre playlist ${genrePlaylist.uuid} has no viewsCount to size by views`);
202
- }
203
- return genrePlaylist.viewsCount;
204
- };
205
-
206
- // src/genre-tree/playlist-tree/TreeWheel.tsx
207
- import { capitalize as capitalize2, useTrackNoun as useTrackNoun2 } from "@behindthemusictree/app-kit/track-noun";
208
213
  import { jsx as jsx2 } from "react/jsx-runtime";
209
214
  function GenrePlaylistTreeWheel({
210
215
  scope,
@@ -215,14 +220,10 @@ function GenrePlaylistTreeWheel({
215
220
  additionalActions,
216
221
  onNodeClick,
217
222
  onNodeHover,
218
- renderInfoPanelBody,
219
223
  selectedNodeId,
220
224
  onSelectedNodeChange,
221
- hideInfoPanelClose,
222
- hideInfoPanelTitle,
223
225
  readOnly = false,
224
226
  allowWheelRotation,
225
- showToolbar,
226
227
  sizeMetric = "songs",
227
228
  floatingControlsLeading,
228
229
  busy,
@@ -230,12 +231,11 @@ function GenrePlaylistTreeWheel({
230
231
  viewportRestore,
231
232
  onViewportChange,
232
233
  playback,
233
- ...infoPanelCuration
234
+ ...hostPanel
234
235
  }) {
235
236
  const { isPlaying, setIsPlaying } = usePlayer2();
236
237
  const defaultPlayback = useGenrePlaylistPlayback({ scope, genrePlaylists });
237
238
  const { playingNodeId, play } = playback ?? defaultPlayback;
238
- const trackNoun = useTrackNoun2();
239
239
  const { showPopup } = usePopup2();
240
240
  const nodes = useMemo3(
241
241
  () => genrePlaylists.map((genrePlaylist) => ({
@@ -299,7 +299,6 @@ function GenrePlaylistTreeWheel({
299
299
  {
300
300
  className: className2,
301
301
  nodes,
302
- itemCountLabel: sizeMetric === "views" ? "Views" : `${capitalize2(trackNoun.singular)} count`,
303
302
  playingNodeId,
304
303
  playState: isPlaying ? "playing" : "paused",
305
304
  onPlayPause: handlePlayPause,
@@ -309,20 +308,15 @@ function GenrePlaylistTreeWheel({
309
308
  additionalActions,
310
309
  onNodeClick,
311
310
  onNodeHover,
312
- renderInfoPanelBody,
313
311
  selectedNodeId,
314
312
  onSelectedNodeChange,
315
- hideInfoPanelClose,
316
- hideInfoPanelTitle,
317
313
  allowWheelRotation,
318
- showToolbar,
319
314
  floatingControlsLeading,
320
315
  busy,
321
316
  defaultViewport,
322
317
  viewportRestore,
323
318
  onViewportChange,
324
- ...infoPanelCuration,
325
- readOnly
319
+ ...hostPanel
326
320
  }
327
321
  );
328
322
  }
@@ -334,7 +328,6 @@ import {
334
328
  GenreTreeWheelRadialPopCore
335
329
  } from "@behindthemusictree/genre-tree-view";
336
330
  import { usePlayer as usePlayer3 } from "@behindthemusictree/app-kit/player";
337
- import { capitalize as capitalize3, useTrackNoun as useTrackNoun3 } from "@behindthemusictree/app-kit/track-noun";
338
331
  import { jsx as jsx3 } from "react/jsx-runtime";
339
332
  function GenrePlaylistTreeWheelRadialPopCore(props) {
340
333
  const {
@@ -346,14 +339,10 @@ function GenrePlaylistTreeWheelRadialPopCore(props) {
346
339
  additionalActions,
347
340
  onNodeClick,
348
341
  onNodeHover,
349
- renderInfoPanelBody,
350
342
  selectedNodeId,
351
343
  onSelectedNodeChange,
352
- hideInfoPanelClose,
353
- hideInfoPanelTitle,
354
344
  readOnly = false,
355
345
  allowWheelRotation,
356
- showToolbar,
357
346
  sizeMetric = "songs",
358
347
  floatingControlsLeading,
359
348
  busy,
@@ -364,33 +353,14 @@ function GenrePlaylistTreeWheelRadialPopCore(props) {
364
353
  playback
365
354
  } = props;
366
355
  const focus = props;
367
- const infoPanelCuration = {
368
- renderDangerZone: props.renderDangerZone,
369
- expanded: props.expanded,
370
- onExpandedChange: props.onExpandedChange,
371
- collapsed: props.collapsed,
372
- hideExpandButton: props.hideExpandButton,
373
- overlayPanel: props.overlayPanel
356
+ const hostPanel = {
357
+ obscuredInset: props.obscuredInset,
358
+ onNodeStyleResolverChange: props.onNodeStyleResolverChange
374
359
  };
375
360
  const { isPlaying, setIsPlaying } = usePlayer3();
376
361
  const defaultPlayback = useGenrePlaylistPlayback({ scope, genrePlaylists });
377
362
  const { playingNodeId, play } = playback ?? defaultPlayback;
378
- const trackNoun = useTrackNoun3();
379
- const nodes = useMemo4(
380
- () => genrePlaylists.map((genrePlaylist) => ({
381
- id: genrePlaylist.uuid,
382
- parentId: genrePlaylist.parent?.uuid ?? null,
383
- name: genrePlaylist.name,
384
- itemCount: genrePlaylistSize(genrePlaylist, sizeMetric),
385
- actionable: Boolean(genrePlaylist.criteria),
386
- detached: !genrePlaylist.criteria,
387
- side: genrePlaylist.criteria?.side ?? void 0,
388
- wikidataId: genrePlaylist.criteria?.wikidataId ?? null,
389
- umbrella: genrePlaylist.isUmbrella,
390
- scene: genrePlaylist.scene ?? void 0
391
- })),
392
- [genrePlaylists, sizeMetric]
393
- );
363
+ const nodes = useMemo4(() => toTreeNodes(genrePlaylists, sizeMetric), [genrePlaylists, sizeMetric]);
394
364
  const handlePlayPause = useCallback4(
395
365
  (nodeId) => {
396
366
  const genrePlaylist = genrePlaylists.find((g) => g.uuid === nodeId);
@@ -431,7 +401,6 @@ function GenrePlaylistTreeWheelRadialPopCore(props) {
431
401
  const treeProps = {
432
402
  className: className2,
433
403
  nodes,
434
- itemCountLabel: sizeMetric === "views" ? "Views" : `${capitalize3(trackNoun.singular)} count`,
435
404
  playingNodeId,
436
405
  playState: isPlaying ? "playing" : "paused",
437
406
  onPlayPause: handlePlayPause,
@@ -441,14 +410,9 @@ function GenrePlaylistTreeWheelRadialPopCore(props) {
441
410
  additionalActions,
442
411
  onNodeClick,
443
412
  onNodeHover,
444
- renderInfoPanelBody,
445
413
  selectedNodeId,
446
414
  onSelectedNodeChange,
447
- hideInfoPanelClose,
448
- hideInfoPanelTitle,
449
- showToolbar,
450
- ...infoPanelCuration,
451
- readOnly
415
+ ...hostPanel
452
416
  };
453
417
  return outline ? /* @__PURE__ */ jsx3(GenreTreeOutline, { ...treeProps }) : /* @__PURE__ */ jsx3(
454
418
  GenreTreeWheelRadialPopCore,
@@ -568,7 +532,7 @@ function GenreSearch({
568
532
  type: "button",
569
533
  "aria-label": "Previous",
570
534
  title: "Previous",
571
- className: "genre-search-previous absolute left-2 top-1/2 z-10 hidden items-center justify-center p-1.5 [@media(pointer:coarse)]:min-h-[var(--layout-touch-target)] [@media(pointer:coarse)]:min-w-[var(--layout-touch-target)] text-[var(--color-text-muted)] rounded-full -translate-y-1/2 hover:bg-[var(--color-surface-muted)] hover:text-[var(--color-text)]",
535
+ className: "genre-search-previous absolute left-2 top-1/2 z-10 flex items-center justify-center p-1.5 [@media(pointer:coarse)]:min-h-[var(--layout-touch-target)] [@media(pointer:coarse)]:min-w-[var(--layout-touch-target)] text-[var(--color-text-muted)] rounded-full -translate-y-1/2 hover:bg-[var(--color-surface-muted)] hover:text-[var(--color-text)]",
572
536
  onClick: onPrevious,
573
537
  children: /* @__PURE__ */ jsx5(ArrowLeft, { "aria-hidden": "true", className: "w-4 h-4" })
574
538
  }
@@ -590,7 +554,7 @@ function GenreSearch({
590
554
  onPick: handlePick,
591
555
  status,
592
556
  loading: loading ? "Searching\u2026" : void 0,
593
- inputClassName: "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",
557
+ inputClassName: `h-[var(--layout-search-h,3rem)] ${onPrevious ? "pl-11" : "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`,
594
558
  className: "genre-search-results z-40 rounded-xl shadow-md",
595
559
  listClassName: "max-h-[min(20rem,50dvh)]",
596
560
  optionClassName: "[@media(pointer:coarse)]:min-h-[var(--layout-touch-target)]"
@@ -952,7 +916,7 @@ function GenreActionBand({
952
916
  }
953
917
 
954
918
  // src/genre-tree/GenreDetailExtras.tsx
955
- import { Skeleton as Skeleton2 } from "@behindthemusictree/ui";
919
+ import { PanelSection as PanelSection2, Skeleton as Skeleton2 } from "@behindthemusictree/ui";
956
920
  import { useFetchGenreOverview } from "@behindthemusictree/app-kit/genre-tree";
957
921
 
958
922
  // src/genre-tree/ClampedText.tsx
@@ -983,11 +947,11 @@ function ClampedText({ children, className: className2 = "" }) {
983
947
  }
984
948
 
985
949
  // src/genre-tree/GenrePlaylistTracks.tsx
986
- import { useEffect as useEffect3, useRef as useRef3 } from "react";
950
+ import { useEffect as useEffect3, useState as useState5 } from "react";
987
951
  import { useInfiniteQuery } from "@tanstack/react-query";
988
952
  import { z } from "zod";
989
953
  import { FaPlay, FaPause } from "react-icons/fa";
990
- import { RingLoader, Skeleton } from "@behindthemusictree/ui";
954
+ import { PanelSection, RingLoader, Skeleton } from "@behindthemusictree/ui";
991
955
  import { useSession } from "@behindthemusictree/app-kit/auth";
992
956
  import { useFetchWrapper } from "@behindthemusictree/app-kit/transport";
993
957
  import { PaginatedResponseSchema } from "@behindthemusictree/app-kit/transport";
@@ -1027,7 +991,7 @@ function MusicBrainzRecordingLink({
1027
991
 
1028
992
  // src/genre-tree/GenrePlaylistTracks.tsx
1029
993
  import { ArtistNames } from "@behindthemusictree/app-kit/genre-tree";
1030
- import { capitalize as capitalize4, useTrackNoun as useTrackNoun4 } from "@behindthemusictree/app-kit/track-noun";
994
+ import { capitalize, useTrackNoun } from "@behindthemusictree/app-kit/track-noun";
1031
995
  import { jsx as jsx13, jsxs as jsxs9 } from "react/jsx-runtime";
1032
996
  var CATALOGUE_PAGE_SIZE = 100;
1033
997
  var SongPageSchema = PaginatedResponseSchema(SongDetailedSchema).extend({
@@ -1061,7 +1025,7 @@ function GenrePlaylistTracks({
1061
1025
  const { session, sessionRestored } = useSession();
1062
1026
  const { playerTrackObject, playState, handlePlayPauseAction } = usePlayer4();
1063
1027
  const { showPopup } = usePopup4();
1064
- const trackNoun = useTrackNoun4();
1028
+ const trackNoun = useTrackNoun();
1065
1029
  const { uuid } = genrePlaylist;
1066
1030
  const { fetch } = useFetchWrapper(getBackendBaseUrl);
1067
1031
  const { data, isPending, isError, refetch, hasNextPage, isFetchingNextPage, fetchNextPage } = useInfiniteQuery({
@@ -1082,9 +1046,8 @@ function GenrePlaylistTracks({
1082
1046
  getNextPageParam: (lastPage) => lastPage.nextPage ?? void 0,
1083
1047
  enabled: scope !== "me" || sessionRestored && !!session?.accessToken
1084
1048
  });
1085
- const sentinelRef = useRef3(null);
1049
+ const [sentinel, setSentinel] = useState5(null);
1086
1050
  useEffect3(() => {
1087
- const sentinel = sentinelRef.current;
1088
1051
  if (!hasNextPage || isFetchingNextPage || !sentinel) return;
1089
1052
  const observer = new IntersectionObserver((entries) => {
1090
1053
  if (entries.some((entry) => entry.isIntersecting)) {
@@ -1093,7 +1056,7 @@ function GenrePlaylistTracks({
1093
1056
  });
1094
1057
  observer.observe(sentinel);
1095
1058
  return () => observer.disconnect();
1096
- }, [hasNextPage, isFetchingNextPage, fetchNextPage]);
1059
+ }, [hasNextPage, isFetchingNextPage, fetchNextPage, sentinel]);
1097
1060
  const pages = data?.pages ?? [];
1098
1061
  const tracks = pages.flatMap((page) => page.tracks);
1099
1062
  const total = pages[0]?.total ?? void 0;
@@ -1104,11 +1067,7 @@ function GenrePlaylistTracks({
1104
1067
  showPopup(/* @__PURE__ */ jsx13(InternalErrorPopup, { errorCode: ErrorCode3.CLIENT_UNKNOWN }));
1105
1068
  });
1106
1069
  };
1107
- return /* @__PURE__ */ jsxs9("div", { className: "gtv-info-panel-children", children: [
1108
- /* @__PURE__ */ jsxs9("span", { className: "gtv-info-panel-children-title", children: [
1109
- capitalize4(trackNoun.plural),
1110
- total !== void 0 && ` (${total})`
1111
- ] }),
1070
+ return /* @__PURE__ */ jsx13(PanelSection, { title: `${capitalize(trackNoun.plural)}${total !== void 0 ? ` (${total})` : ""}`, children: /* @__PURE__ */ jsxs9("div", { children: [
1112
1071
  renderTracksToolbar?.(),
1113
1072
  isPending ? /* @__PURE__ */ jsx13(Skeleton, { className: "h-4 w-3/4", "aria-busy": "true" }) : isError && tracks.length === 0 ? /* @__PURE__ */ jsxs9("p", { role: "alert", children: [
1114
1073
  "Couldn't load ",
@@ -1171,13 +1130,13 @@ function GenrePlaylistTracks({
1171
1130
  track.uuid
1172
1131
  );
1173
1132
  }),
1174
- hasNextPage && /* @__PURE__ */ jsx13("li", { ref: sentinelRef, "aria-hidden": "true", className: "h-4" })
1133
+ hasNextPage && /* @__PURE__ */ jsx13("li", { ref: setSentinel, "aria-hidden": "true", className: "h-4" })
1175
1134
  ] })
1176
- ] });
1135
+ ] }) });
1177
1136
  }
1178
1137
 
1179
1138
  // src/genre-tree/GenreDetailExtras.tsx
1180
- import { useTrackNoun as useTrackNoun5 } from "@behindthemusictree/app-kit/track-noun";
1139
+ import { useTrackNoun as useTrackNoun2 } from "@behindthemusictree/app-kit/track-noun";
1181
1140
  import { SiWikidata, SiWikipedia } from "react-icons/si";
1182
1141
  import { Fragment as Fragment6, jsx as jsx14, jsxs as jsxs10 } from "react/jsx-runtime";
1183
1142
  function GenreDetailExtras({
@@ -1195,7 +1154,7 @@ function GenreDetailExtras({
1195
1154
  family,
1196
1155
  ...trackSlots
1197
1156
  }) {
1198
- const trackNoun = useTrackNoun5();
1157
+ const trackNoun = useTrackNoun2();
1199
1158
  const { data: overview, isPending } = useFetchGenreOverview(
1200
1159
  genreUuid,
1201
1160
  scope,
@@ -1204,10 +1163,7 @@ function GenreDetailExtras({
1204
1163
  );
1205
1164
  if (isPending) {
1206
1165
  return /* @__PURE__ */ jsxs10(Fragment6, { children: [
1207
- /* @__PURE__ */ jsxs10("div", { className: "gtv-info-panel-children", "aria-busy": "true", "data-testid": "genre-detail-extras-skeleton", children: [
1208
- /* @__PURE__ */ jsx14("span", { className: "gtv-info-panel-children-title", children: "Summary" }),
1209
- /* @__PURE__ */ jsx14(Skeleton2, { className: "h-4 w-3/4" })
1210
- ] }),
1166
+ /* @__PURE__ */ jsx14(PanelSection2, { title: "Summary", children: /* @__PURE__ */ jsx14("div", { "aria-busy": "true", "data-testid": "genre-detail-extras-skeleton", children: /* @__PURE__ */ jsx14(Skeleton2, { className: "h-4 w-3/4" }) }) }),
1211
1167
  family
1212
1168
  ] });
1213
1169
  }
@@ -1215,50 +1171,38 @@ function GenreDetailExtras({
1215
1171
  const { summary, summarySourceUrl, essentialSongs } = overview;
1216
1172
  const wikidataUrl = wikidataId ? `https://www.wikidata.org/wiki/${encodeURIComponent(wikidataId)}` : null;
1217
1173
  return /* @__PURE__ */ jsxs10(Fragment6, { children: [
1218
- (summarySourceUrl || wikidataUrl) && /* @__PURE__ */ jsxs10("div", { className: "gtv-info-panel-children text-[0.8125rem] leading-[1.45] text-[var(--color-text)]", children: [
1219
- /* @__PURE__ */ jsx14("span", { className: "gtv-info-panel-children-title", children: "Links" }),
1220
- /* @__PURE__ */ jsxs10("div", { className: "mt-1 flex flex-wrap gap-x-3 gap-y-1 text-xs", children: [
1221
- summarySourceUrl && /* @__PURE__ */ jsxs10(
1222
- "a",
1223
- {
1224
- href: summarySourceUrl,
1225
- target: "_blank",
1226
- rel: "noopener noreferrer",
1227
- className: "inline-flex items-center gap-1.5 text-[var(--color-text-muted)] hover:text-[var(--color-text)]",
1228
- children: [
1229
- /* @__PURE__ */ jsx14(SiWikipedia, { "aria-hidden": "true", className: "h-3.5 w-3.5" }),
1230
- "Wikipedia"
1231
- ]
1232
- }
1233
- ),
1234
- wikidataUrl && /* @__PURE__ */ jsxs10(
1235
- "a",
1236
- {
1237
- href: wikidataUrl,
1238
- target: "_blank",
1239
- rel: "noopener noreferrer",
1240
- className: "inline-flex items-center gap-1.5 text-[var(--color-text-muted)] hover:text-[var(--color-text)]",
1241
- children: [
1242
- /* @__PURE__ */ jsx14(SiWikidata, { "aria-hidden": "true", className: "h-3.5 w-3.5" }),
1243
- "Wikidata"
1244
- ]
1245
- }
1246
- )
1247
- ] })
1248
- ] }),
1249
- renderSummary ? renderSummary(overview) : /* @__PURE__ */ jsxs10("div", { className: "gtv-info-panel-children text-[0.8125rem] leading-[1.45] text-[var(--color-text)]", children: [
1250
- /* @__PURE__ */ jsx14("span", { className: "gtv-info-panel-children-title", children: "Summary" }),
1251
- /* @__PURE__ */ jsx14(ClampedText, { className: "mt-1", children: summary ?? "\u2014" }, genreUuid)
1252
- ] }),
1174
+ (summarySourceUrl || wikidataUrl) && /* @__PURE__ */ jsx14(PanelSection2, { title: "Links", children: /* @__PURE__ */ jsxs10("div", { className: "flex flex-wrap gap-x-3 gap-y-1 text-xs text-[var(--color-text)]", children: [
1175
+ summarySourceUrl && /* @__PURE__ */ jsxs10(
1176
+ "a",
1177
+ {
1178
+ href: summarySourceUrl,
1179
+ target: "_blank",
1180
+ rel: "noopener noreferrer",
1181
+ className: "inline-flex items-center gap-1.5 text-[var(--color-text-muted)] hover:text-[var(--color-text)]",
1182
+ children: [
1183
+ /* @__PURE__ */ jsx14(SiWikipedia, { "aria-hidden": "true", className: "h-3.5 w-3.5" }),
1184
+ "Wikipedia"
1185
+ ]
1186
+ }
1187
+ ),
1188
+ wikidataUrl && /* @__PURE__ */ jsxs10(
1189
+ "a",
1190
+ {
1191
+ href: wikidataUrl,
1192
+ target: "_blank",
1193
+ rel: "noopener noreferrer",
1194
+ className: "inline-flex items-center gap-1.5 text-[var(--color-text-muted)] hover:text-[var(--color-text)]",
1195
+ children: [
1196
+ /* @__PURE__ */ jsx14(SiWikidata, { "aria-hidden": "true", className: "h-3.5 w-3.5" }),
1197
+ "Wikidata"
1198
+ ]
1199
+ }
1200
+ )
1201
+ ] }) }),
1202
+ /* @__PURE__ */ jsx14(PanelSection2, { title: "Summary", children: renderSummary ? renderSummary(overview) : /* @__PURE__ */ jsx14(ClampedText, { className: "text-[0.8125rem] leading-[1.45] text-[var(--color-text)]", children: summary ?? "\u2014" }, genreUuid) }),
1253
1203
  family,
1254
1204
  renderGenreDetailExtras?.(overview),
1255
- renderEssentialTracks ? renderEssentialTracks(overview) : essentialSongs && /* @__PURE__ */ jsxs10("div", { className: "gtv-info-panel-children text-[0.8125rem] leading-[1.45] text-[var(--color-text)]", children: [
1256
- /* @__PURE__ */ jsxs10("span", { className: "gtv-info-panel-children-title", children: [
1257
- "Essential ",
1258
- trackNoun.plural
1259
- ] }),
1260
- essentialSongs.length > 0 ? /* @__PURE__ */ jsx14("ul", { className: "mt-1 divide-y divide-[var(--color-border)]", children: essentialSongs.map((track) => /* @__PURE__ */ jsx14("li", { className: "py-1", children: track.title }, track.uuid)) }) : /* @__PURE__ */ jsx14("p", { className: "mt-1", children: "\u2014" })
1261
- ] }),
1205
+ (renderEssentialTracks || essentialSongs) && /* @__PURE__ */ jsx14(PanelSection2, { title: `Essential ${trackNoun.plural}`, children: renderEssentialTracks ? renderEssentialTracks(overview) : essentialSongs && essentialSongs.length > 0 ? /* @__PURE__ */ jsx14("ul", { className: "text-[0.8125rem] leading-[1.45] text-[var(--color-text)]", children: essentialSongs.map((track) => /* @__PURE__ */ jsx14("li", { className: "py-1", children: track.title }, track.uuid)) }) : /* @__PURE__ */ jsx14("p", { className: "text-[0.8125rem] leading-[1.45] text-[var(--color-text)]", children: "\u2014" }) }),
1262
1206
  /* @__PURE__ */ jsx14(
1263
1207
  GenrePlaylistTracks,
1264
1208
  {
@@ -1329,69 +1273,13 @@ function GenreInfoPanelBody({
1329
1273
  ] });
1330
1274
  }
1331
1275
 
1332
- // src/genre-tree/PanelEdgeTab.tsx
1333
- import { ChevronLeft, ChevronRight } from "lucide-react";
1334
- import { twMerge } from "tailwind-merge";
1335
- import { jsx as jsx16, jsxs as jsxs12 } from "react/jsx-runtime";
1336
- var buttonClassName = "flex h-12 w-full items-center justify-center p-0 text-[var(--color-text-muted)] hover:bg-[var(--color-surface-muted)] hover:text-[var(--color-text)] focus-visible:outline focus-visible:outline-2 focus-visible:-outline-offset-2 focus-visible:outline-[var(--color-primary)]";
1337
- function PanelEdgeTab({ side, edge, center, controls, className: className2 }) {
1338
- if (!edge && !center) return null;
1339
- const isLeft = side === "left";
1340
- const EdgeIcon = isLeft ? ChevronLeft : ChevronRight;
1341
- const CenterIcon = isLeft ? ChevronRight : ChevronLeft;
1342
- const renderButton = (action, icon) => /* @__PURE__ */ jsx16(
1343
- "button",
1344
- {
1345
- type: "button",
1346
- "aria-label": action.label,
1347
- title: action.label,
1348
- "aria-controls": controls,
1349
- "aria-expanded": action.expanded,
1350
- className: buttonClassName,
1351
- onClick: action.onClick,
1352
- children: icon
1353
- }
1354
- );
1355
- const edgeButton = edge && renderButton(edge, /* @__PURE__ */ jsx16(EdgeIcon, { "aria-hidden": "true", className: "h-5 w-5" }));
1356
- const centerButton = center && renderButton(center, /* @__PURE__ */ jsx16(CenterIcon, { "aria-hidden": "true", className: "h-5 w-5" }));
1357
- const buttons = isLeft ? [edgeButton, centerButton] : [centerButton, edgeButton];
1358
- return /* @__PURE__ */ jsxs12(
1359
- "div",
1360
- {
1361
- className: twMerge(
1362
- "panel-edge-tab flex w-[var(--layout-edge-tab-w)] flex-col overflow-hidden bg-[var(--color-surface)] shadow-[0_1px_2px_rgba(60,64,67,0.3),0_2px_6px_2px_rgba(60,64,67,0.15)] [clip-path:inset(-0.5rem_-0.5rem_-0.5rem_0)]",
1363
- isLeft ? "rounded-r-lg" : "rounded-l-lg [clip-path:inset(-0.5rem_0_-0.5rem_-0.5rem)]",
1364
- className2
1365
- ),
1366
- children: [
1367
- buttons[0],
1368
- buttons[0] && buttons[1] && /* @__PURE__ */ jsx16("div", { role: "presentation", className: "h-px shrink-0 bg-[var(--color-border)]" }),
1369
- buttons[1]
1370
- ]
1371
- }
1372
- );
1373
- }
1374
-
1375
- // src/genre-tree/useLeftDetailPanelLayout.ts
1376
- import { useEffect as useEffect4 } from "react";
1377
- function useLeftDetailPanelLayout(isPanelShown, enabled = true) {
1378
- useEffect4(() => {
1379
- if (!enabled || isPanelShown) return void 0;
1380
- const root = document.documentElement;
1381
- root.style.setProperty("--layout-left-w", "0px");
1382
- return () => {
1383
- root.style.removeProperty("--layout-left-w");
1384
- };
1385
- }, [enabled, isPanelShown]);
1386
- }
1387
-
1388
1276
  // src/genre-tree/ChangeParentPopup.tsx
1389
- import { useEffect as useEffect5, useMemo as useMemo6 } from "react";
1277
+ import { useEffect as useEffect4, useMemo as useMemo6 } from "react";
1390
1278
  import {
1391
1279
  getReparentCandidates,
1392
1280
  useUpdateGenre
1393
1281
  } from "@behindthemusictree/app-kit/genre-tree";
1394
- import { jsx as jsx17, jsxs as jsxs13 } from "react/jsx-runtime";
1282
+ import { jsx as jsx16, jsxs as jsxs12 } from "react/jsx-runtime";
1395
1283
  function ChangeParentPopup({
1396
1284
  genrePlaylist,
1397
1285
  genrePlaylists,
@@ -1419,19 +1307,19 @@ function ChangeParentPopup({
1419
1307
  }
1420
1308
  );
1421
1309
  };
1422
- useEffect5(() => {
1310
+ useEffect4(() => {
1423
1311
  const handleKeyDown = (event) => {
1424
1312
  if (event.key === "Escape") onClose();
1425
1313
  };
1426
1314
  window.addEventListener("keydown", handleKeyDown);
1427
1315
  return () => window.removeEventListener("keydown", handleKeyDown);
1428
1316
  }, [onClose]);
1429
- return /* @__PURE__ */ jsxs13(BasePopup_default, { title: `Change parent of ${genrePlaylist.name}`, onClose, showCancelButton: true, fitContent: true, children: [
1430
- /* @__PURE__ */ jsxs13("p", { className: "mb-3 text-sm text-[var(--color-text-muted)]", children: [
1317
+ return /* @__PURE__ */ jsxs12(BasePopup_default, { title: `Change parent of ${genrePlaylist.name}`, onClose, showCancelButton: true, fitContent: true, children: [
1318
+ /* @__PURE__ */ jsxs12("p", { className: "mb-3 text-sm text-[var(--color-text-muted)]", children: [
1431
1319
  "Current parent: ",
1432
1320
  genrePlaylist.parent?.name ?? "none"
1433
1321
  ] }),
1434
- /* @__PURE__ */ jsx17("div", { "aria-busy": isPending, className: isPending ? "pointer-events-none opacity-60" : void 0, children: /* @__PURE__ */ jsx17(
1322
+ /* @__PURE__ */ jsx16("div", { "aria-busy": isPending, className: isPending ? "pointer-events-none opacity-60" : void 0, children: /* @__PURE__ */ jsx16(
1435
1323
  GenreSearch,
1436
1324
  {
1437
1325
  genrePlaylists: candidates,
@@ -1439,7 +1327,7 @@ function ChangeParentPopup({
1439
1327
  placeholder: "Search a new parent\u2026"
1440
1328
  }
1441
1329
  ) }),
1442
- formErrors.length > 0 && /* @__PURE__ */ jsx17("p", { role: "alert", className: "mt-3 text-sm text-[var(--color-red-500)]", children: formErrors.map(({ message }) => message).join(" ") })
1330
+ formErrors.length > 0 && /* @__PURE__ */ jsx16("p", { role: "alert", className: "mt-3 text-sm text-[var(--color-red-500)]", children: formErrors.map(({ message }) => message).join(" ") })
1443
1331
  ] });
1444
1332
  }
1445
1333
 
@@ -1463,7 +1351,7 @@ import {
1463
1351
  SiYoutube
1464
1352
  } from "react-icons/si";
1465
1353
  import { Chip } from "@behindthemusictree/ui";
1466
- import { jsx as jsx18 } from "react/jsx-runtime";
1354
+ import { jsx as jsx17 } from "react/jsx-runtime";
1467
1355
  var LINK_KINDS = {
1468
1356
  official: { label: "Official website", Icon: Globe },
1469
1357
  wikipedia: { label: "Wikipedia", Icon: SiWikipedia2 },
@@ -1500,7 +1388,7 @@ function artistLinks(artist) {
1500
1388
  return links;
1501
1389
  }
1502
1390
  function ArtistLinks({ links }) {
1503
- return /* @__PURE__ */ jsx18("ul", { className: "flex flex-wrap gap-1.5", children: links.map(({ key, url, Icon, label }) => /* @__PURE__ */ jsx18("li", { children: /* @__PURE__ */ jsx18(Chip, { href: url, icon: /* @__PURE__ */ jsx18(Icon, { className: "h-3.5 w-3.5 shrink-0", "aria-hidden": "true" }), children: label }) }, key)) });
1391
+ return /* @__PURE__ */ jsx17("ul", { className: "flex flex-wrap gap-1.5", children: links.map(({ key, url, Icon, label }) => /* @__PURE__ */ jsx17("li", { children: /* @__PURE__ */ jsx17(Chip, { href: url, icon: /* @__PURE__ */ jsx17(Icon, { className: "h-3.5 w-3.5 shrink-0", "aria-hidden": "true" }), children: label }) }, key)) });
1504
1392
  }
1505
1393
 
1506
1394
  // src/genre-tree/artist/formatYears.ts
@@ -1511,7 +1399,7 @@ function formatYears(beginYear, endYear, ended) {
1511
1399
  }
1512
1400
 
1513
1401
  // src/genre-tree/artist/ArtistHeader.tsx
1514
- import { jsx as jsx19 } from "react/jsx-runtime";
1402
+ import { jsx as jsx18 } from "react/jsx-runtime";
1515
1403
  var countryNames = new Intl.DisplayNames(["en"], { type: "region" });
1516
1404
  function ArtistHeader({ artist }) {
1517
1405
  const fields = [];
@@ -1521,35 +1409,35 @@ function ArtistHeader({ artist }) {
1521
1409
  const years = formatYears(artist.beginYear, artist.endYear, artist.ended);
1522
1410
  if (years) fields.push({ label: "Active", value: years });
1523
1411
  const links = artistLinks(artist);
1524
- if (links.length > 0) fields.push({ label: "Links", value: /* @__PURE__ */ jsx19(ArtistLinks, { links }) });
1412
+ if (links.length > 0) fields.push({ label: "Links", value: /* @__PURE__ */ jsx18(ArtistLinks, { links }) });
1525
1413
  if (fields.length === 0) return null;
1526
- return /* @__PURE__ */ jsx19(FieldList, { items: fields });
1414
+ return /* @__PURE__ */ jsx18(FieldList, { items: fields });
1527
1415
  }
1528
1416
 
1529
1417
  // src/genre-tree/artist/ArtistPhoto.tsx
1530
- import { jsx as jsx20, jsxs as jsxs14 } from "react/jsx-runtime";
1418
+ import { jsx as jsx19, jsxs as jsxs13 } from "react/jsx-runtime";
1531
1419
  function ExternalLink({ href, children }) {
1532
- return /* @__PURE__ */ jsx20("a", { href, target: "_blank", rel: "noopener noreferrer", className: "underline-offset-2 hover:underline", children });
1420
+ return /* @__PURE__ */ jsx19("a", { href, target: "_blank", rel: "noopener noreferrer", className: "underline-offset-2 hover:underline", children });
1533
1421
  }
1534
1422
  function ArtistPhoto({ image, name }) {
1535
- return /* @__PURE__ */ jsxs14("figure", { className: "flex flex-col gap-1.5 px-6 py-2.5", children: [
1536
- /* @__PURE__ */ jsx20("img", { src: image.url, alt: name, className: "h-64 w-full object-contain bg-[var(--color-surface-muted)] rounded" }),
1537
- /* @__PURE__ */ jsxs14("figcaption", { className: "text-xs text-[var(--color-text-muted)]", children: [
1423
+ return /* @__PURE__ */ jsxs13("figure", { className: "flex flex-col gap-1.5 px-6 py-2.5", children: [
1424
+ /* @__PURE__ */ jsx19("img", { src: image.url, alt: name, className: "h-64 w-full object-contain bg-[var(--color-surface-muted)] rounded" }),
1425
+ /* @__PURE__ */ jsxs13("figcaption", { className: "text-xs text-[var(--color-text-muted)]", children: [
1538
1426
  "Photo: ",
1539
- /* @__PURE__ */ jsx20(ExternalLink, { href: image.pageUrl, children: image.author ?? "Wikimedia Commons" }),
1427
+ /* @__PURE__ */ jsx19(ExternalLink, { href: image.pageUrl, children: image.author ?? "Wikimedia Commons" }),
1540
1428
  ",",
1541
1429
  " ",
1542
- image.licenseUrl ? /* @__PURE__ */ jsx20(ExternalLink, { href: image.licenseUrl, children: image.license }) : image.license
1430
+ image.licenseUrl ? /* @__PURE__ */ jsx19(ExternalLink, { href: image.licenseUrl, children: image.license }) : image.license
1543
1431
  ] })
1544
1432
  ] });
1545
1433
  }
1546
1434
 
1547
1435
  // src/genre-tree/artist/ArtistMembers.tsx
1548
- import { PanelGroup, PanelSection } from "@behindthemusictree/ui";
1549
- import { jsx as jsx21, jsxs as jsxs15 } from "react/jsx-runtime";
1436
+ import { PanelGroup, PanelSection as PanelSection3 } from "@behindthemusictree/ui";
1437
+ import { jsx as jsx20, jsxs as jsxs14 } from "react/jsx-runtime";
1550
1438
  function MembershipList({ title, memberships, renderArtistLink }) {
1551
1439
  if (memberships.length === 0) return null;
1552
- return /* @__PURE__ */ jsx21(PanelGroup, { title, children: /* @__PURE__ */ jsx21("ul", { children: memberships.map((membership, index) => {
1440
+ return /* @__PURE__ */ jsx20(PanelGroup, { title, children: /* @__PURE__ */ jsx20("ul", { children: memberships.map((membership, index) => {
1553
1441
  const details = [
1554
1442
  formatYears(membership.beginYear, membership.endYear, membership.ended),
1555
1443
  ...membership.roles
@@ -1557,9 +1445,9 @@ function MembershipList({ title, memberships, renderArtistLink }) {
1557
1445
  const detailsText = details.filter(Boolean).join(" \xB7 ");
1558
1446
  return (
1559
1447
  // A member can have several periods in the same group.
1560
- /* @__PURE__ */ jsxs15("li", { className: "truncate py-0.5", title: detailsText || void 0, children: [
1561
- /* @__PURE__ */ jsx21("span", { className: "font-medium text-[var(--color-text)]", children: renderArtistLink(membership.artist) }),
1562
- detailsText && /* @__PURE__ */ jsxs15("span", { className: "text-[var(--color-text-muted)]", children: [
1448
+ /* @__PURE__ */ jsxs14("li", { className: "truncate py-0.5", title: detailsText || void 0, children: [
1449
+ /* @__PURE__ */ jsx20("span", { className: "font-medium text-[var(--color-text)]", children: renderArtistLink(membership.artist) }),
1450
+ detailsText && /* @__PURE__ */ jsxs14("span", { className: "text-[var(--color-text-muted)]", children: [
1563
1451
  " \u2014 ",
1564
1452
  detailsText
1565
1453
  ] })
@@ -1570,9 +1458,9 @@ function MembershipList({ title, memberships, renderArtistLink }) {
1570
1458
  function ArtistMembers({ title, memberships, renderArtistLink }) {
1571
1459
  if (memberships.length === 0) return null;
1572
1460
  const isCurrent = (membership) => !membership.ended && membership.endYear === null;
1573
- return /* @__PURE__ */ jsxs15(PanelSection, { title, children: [
1574
- /* @__PURE__ */ jsx21(MembershipList, { title: "Current", memberships: memberships.filter(isCurrent), renderArtistLink }),
1575
- /* @__PURE__ */ jsx21(
1461
+ return /* @__PURE__ */ jsxs14(PanelSection3, { title, children: [
1462
+ /* @__PURE__ */ jsx20(MembershipList, { title: "Current", memberships: memberships.filter(isCurrent), renderArtistLink }),
1463
+ /* @__PURE__ */ jsx20(
1576
1464
  MembershipList,
1577
1465
  {
1578
1466
  title: "Past",
@@ -1584,8 +1472,8 @@ function ArtistMembers({ title, memberships, renderArtistLink }) {
1584
1472
  }
1585
1473
 
1586
1474
  // src/genre-tree/artist/ArtistDiscography.tsx
1587
- import { PanelGroup as PanelGroup2, PanelSection as PanelSection2 } from "@behindthemusictree/ui";
1588
- import { jsx as jsx22, jsxs as jsxs16 } from "react/jsx-runtime";
1475
+ import { PanelGroup as PanelGroup2, PanelSection as PanelSection4 } from "@behindthemusictree/ui";
1476
+ import { jsx as jsx21, jsxs as jsxs15 } from "react/jsx-runtime";
1589
1477
  var GROUPS = ["Albums", "EPs", "Singles", "Live", "Compilations", "Other"];
1590
1478
  var PRIMARY_GROUPS = { album: "Albums", ep: "EPs", single: "Singles" };
1591
1479
  function discographyGroup({ primaryType, secondaryTypes }) {
@@ -1605,21 +1493,21 @@ function byYear(a, b) {
1605
1493
  function ArtistDiscography({ albums }) {
1606
1494
  if (albums.length === 0) return null;
1607
1495
  const sorted = [...albums].sort(byYear);
1608
- return /* @__PURE__ */ jsx22(PanelSection2, { title: "Discography", children: GROUPS.map((group) => {
1496
+ return /* @__PURE__ */ jsx21(PanelSection4, { title: "Discography", children: GROUPS.map((group) => {
1609
1497
  const groupAlbums = sorted.filter((album) => discographyGroup(album) === group);
1610
1498
  if (groupAlbums.length === 0) return null;
1611
- return /* @__PURE__ */ jsx22(PanelGroup2, { title: group, children: /* @__PURE__ */ jsx22("ul", { children: groupAlbums.map((album) => /* @__PURE__ */ jsxs16("li", { className: "grid grid-cols-[2.5rem_minmax(0,1fr)] gap-2 py-0.5", children: [
1612
- /* @__PURE__ */ jsx22("span", { className: "tabular-nums text-[var(--color-text-muted)]", children: album.year ?? "\u2014" }),
1613
- /* @__PURE__ */ jsx22("span", { className: "truncate text-[var(--color-text)]", title: album.name, children: album.name })
1499
+ return /* @__PURE__ */ jsx21(PanelGroup2, { title: group, children: /* @__PURE__ */ jsx21("ul", { children: groupAlbums.map((album) => /* @__PURE__ */ jsxs15("li", { className: "grid grid-cols-[2.5rem_minmax(0,1fr)] gap-2 py-0.5", children: [
1500
+ /* @__PURE__ */ jsx21("span", { className: "tabular-nums text-[var(--color-text-muted)]", children: album.year ?? "\u2014" }),
1501
+ /* @__PURE__ */ jsx21("span", { className: "truncate text-[var(--color-text)]", title: album.name, children: album.name })
1614
1502
  ] }, album.uuid)) }) }, group);
1615
1503
  }) });
1616
1504
  }
1617
1505
 
1618
1506
  // src/genre-tree/artist/ArtistSongs.tsx
1619
- import { useState as useState5 } from "react";
1507
+ import { useState as useState6 } from "react";
1620
1508
  import { keepPreviousData } from "@tanstack/react-query";
1621
1509
  import { Play as Play2 } from "lucide-react";
1622
- import { Pagination, PanelSection as PanelSection3, Skeleton as Skeleton3 } from "@behindthemusictree/ui";
1510
+ import { Pagination, PanelSection as PanelSection5, Skeleton as Skeleton3 } from "@behindthemusictree/ui";
1623
1511
  import { usePlayer as usePlayer8 } from "@behindthemusictree/app-kit/player";
1624
1512
  import {
1625
1513
  SongDetailedSchema as SongDetailedSchema2,
@@ -1637,7 +1525,7 @@ import { useTrackList as useTrackList3 } from "@behindthemusictree/app-kit/genre
1637
1525
  import { FaPlay as FaPlay2, FaPause as FaPause2 } from "react-icons/fa";
1638
1526
  import { usePlayer as usePlayer6 } from "@behindthemusictree/app-kit/player";
1639
1527
  import { PlayStates as PlayStates2 } from "@behindthemusictree/app-kit/player";
1640
- import { jsx as jsx23, jsxs as jsxs17 } from "react/jsx-runtime";
1528
+ import { jsx as jsx22, jsxs as jsxs16 } from "react/jsx-runtime";
1641
1529
  function TrackPositionPlayPause({
1642
1530
  position,
1643
1531
  uuid,
@@ -1651,14 +1539,14 @@ function TrackPositionPlayPause({
1651
1539
  const isPlaying = isThisTrack && isConfirmedPlayable && playState === PlayStates2.PLAYING;
1652
1540
  const isPaused = isThisTrack && isConfirmedPlayable && playState === PlayStates2.PAUSED;
1653
1541
  const isCurrentTrackActive = isPlaying || isPaused;
1654
- return /* @__PURE__ */ jsxs17(
1542
+ return /* @__PURE__ */ jsxs16(
1655
1543
  "div",
1656
1544
  {
1657
1545
  className: "track-position-play-pause relative flex items-center justify-center gap-2 text-lg w-16 cursor-pointer",
1658
1546
  style: { minWidth: "4rem", maxWidth: "4rem" },
1659
1547
  onClick: handlePlayPauseClick,
1660
1548
  children: [
1661
- /* @__PURE__ */ jsx23("div", { className: "track-position-play-pause__status group-hover:hidden flex items-center justify-center", children: isAwaitingPlayback ? /* @__PURE__ */ jsx23("span", { className: "track-position-play-pause__spinner", role: "status", "aria-label": "Loading track" }) : isCurrentTrackActive ? /* @__PURE__ */ jsxs17(
1549
+ /* @__PURE__ */ jsx22("div", { className: "track-position-play-pause__status group-hover:hidden flex items-center justify-center", children: isAwaitingPlayback ? /* @__PURE__ */ jsx22("span", { className: "track-position-play-pause__spinner", role: "status", "aria-label": "Loading track" }) : isCurrentTrackActive ? /* @__PURE__ */ jsxs16(
1662
1550
  "span",
1663
1551
  {
1664
1552
  className: "track-position-play-pause__bars",
@@ -1666,18 +1554,18 @@ function TrackPositionPlayPause({
1666
1554
  role: "img",
1667
1555
  "aria-label": isPlaying ? "Playing" : "Paused",
1668
1556
  children: [
1669
- /* @__PURE__ */ jsx23("span", { className: "track-position-play-pause__bar" }),
1670
- /* @__PURE__ */ jsx23("span", { className: "track-position-play-pause__bar" }),
1671
- /* @__PURE__ */ jsx23("span", { className: "track-position-play-pause__bar" })
1557
+ /* @__PURE__ */ jsx22("span", { className: "track-position-play-pause__bar" }),
1558
+ /* @__PURE__ */ jsx22("span", { className: "track-position-play-pause__bar" }),
1559
+ /* @__PURE__ */ jsx22("span", { className: "track-position-play-pause__bar" })
1672
1560
  ]
1673
1561
  }
1674
1562
  ) : position }),
1675
- /* @__PURE__ */ jsx23(
1563
+ /* @__PURE__ */ jsx22(
1676
1564
  "div",
1677
1565
  {
1678
1566
  className: `hidden group-hover:flex items-center justify-center ${isCurrentTrackActive ? "" : "absolute inset-0"}`,
1679
1567
  "aria-hidden": "true",
1680
- children: isAwaitingPlayback ? /* @__PURE__ */ jsx23("span", { className: "track-position-play-pause__spinner" }) : isPlaying ? /* @__PURE__ */ jsx23(FaPause2, {}) : /* @__PURE__ */ jsx23(FaPlay2, {})
1568
+ children: isAwaitingPlayback ? /* @__PURE__ */ jsx22("span", { className: "track-position-play-pause__spinner" }) : isPlaying ? /* @__PURE__ */ jsx22(FaPause2, {}) : /* @__PURE__ */ jsx22(FaPlay2, {})
1681
1569
  }
1682
1570
  )
1683
1571
  ]
@@ -1688,7 +1576,7 @@ function TrackPositionPlayPause({
1688
1576
  // src/genre-tree/track-list-sidebar/TrackItem.tsx
1689
1577
  import { isSong as isSong2, songUnplayableReason as songUnplayableReason2 } from "@behindthemusictree/app-kit/genre-tree";
1690
1578
  import { ArtistNames as ArtistNames2 } from "@behindthemusictree/app-kit/genre-tree";
1691
- import { jsx as jsx24, jsxs as jsxs18 } from "react/jsx-runtime";
1579
+ import { jsx as jsx23, jsxs as jsxs17 } from "react/jsx-runtime";
1692
1580
  function TrackItem({
1693
1581
  track,
1694
1582
  position,
@@ -1712,12 +1600,12 @@ function TrackItem({
1712
1600
  loadTrackForPlayer(track.uuid);
1713
1601
  }
1714
1602
  };
1715
- return /* @__PURE__ */ jsxs18(
1603
+ return /* @__PURE__ */ jsxs17(
1716
1604
  "div",
1717
1605
  {
1718
1606
  className: `track-item flex h-14 text-[var(--color-text)] ${isUnplayable ? "opacity-50" : "hover:bg-[var(--color-surface-muted)] group"}`,
1719
1607
  children: [
1720
- isUnplayable ? /* @__PURE__ */ jsx24(
1608
+ isUnplayable ? /* @__PURE__ */ jsx23(
1721
1609
  "div",
1722
1610
  {
1723
1611
  className: "flex items-center justify-center text-lg w-16",
@@ -1725,7 +1613,7 @@ function TrackItem({
1725
1613
  "aria-disabled": "true",
1726
1614
  children: position
1727
1615
  }
1728
- ) : /* @__PURE__ */ jsx24(
1616
+ ) : /* @__PURE__ */ jsx23(
1729
1617
  TrackPositionPlayPause,
1730
1618
  {
1731
1619
  position,
@@ -1734,12 +1622,12 @@ function TrackItem({
1734
1622
  isSelected: selectedTrack?.uuid === track.uuid
1735
1623
  }
1736
1624
  ),
1737
- /* @__PURE__ */ jsxs18("div", { className: "title-artist-container flex flex-col items-start justify-center min-w-0 flex-1", children: [
1738
- /* @__PURE__ */ jsx24("div", { className: "flex max-w-full items-center gap-2", children: /* @__PURE__ */ jsx24("div", { className: "title text-lg font-bold text-[var(--color-text)] max-w-full truncate", children: renderSongLink ? renderSongLink(track) : isSong2(track) ? /* @__PURE__ */ jsx24(MusicBrainzRecordingLink, { song: track, className: "hover:underline focus-visible:underline", children: track.title }) : track.title }) }),
1739
- track.artists && track.artists.length > 0 ? /* @__PURE__ */ jsx24("div", { className: "artist text-base max-w-full truncate text-[var(--color-text-muted)]", children: /* @__PURE__ */ jsx24(ArtistNames2, { artists: track.artists, renderArtistLink }) }) : ""
1625
+ /* @__PURE__ */ jsxs17("div", { className: "title-artist-container flex flex-col items-start justify-center min-w-0 flex-1", children: [
1626
+ /* @__PURE__ */ jsx23("div", { className: "flex max-w-full items-center gap-2", children: /* @__PURE__ */ jsx23("div", { className: "title text-lg font-bold text-[var(--color-text)] max-w-full truncate", children: renderSongLink ? renderSongLink(track) : isSong2(track) ? /* @__PURE__ */ jsx23(MusicBrainzRecordingLink, { song: track, className: "hover:underline focus-visible:underline", children: track.title }) : track.title }) }),
1627
+ track.artists && track.artists.length > 0 ? /* @__PURE__ */ jsx23("div", { className: "artist text-base max-w-full truncate text-[var(--color-text-muted)]", children: /* @__PURE__ */ jsx23(ArtistNames2, { artists: track.artists, renderArtistLink }) }) : ""
1740
1628
  ] }),
1741
- /* @__PURE__ */ jsx24("div", { className: "genre-name-container flex items-center justify-start w-[21.6%] min-w-0 shrink-0", children: track.genre ? /* @__PURE__ */ jsx24("div", { className: "genre-name max-w-full whitespace-normal break-words p-1 text-xs font-bold", children: track.genre.name }) : "" }),
1742
- renderDuration && /* @__PURE__ */ jsx24(
1629
+ /* @__PURE__ */ jsx23("div", { className: "genre-name-container flex items-center justify-start w-[21.6%] min-w-0 shrink-0", children: track.genre ? /* @__PURE__ */ jsx23("div", { className: "genre-name max-w-full whitespace-normal break-words p-1 text-xs font-bold", children: track.genre.name }) : "" }),
1630
+ renderDuration && /* @__PURE__ */ jsx23(
1743
1631
  "div",
1744
1632
  {
1745
1633
  className: "duration flex text-base w-16 items-center justify-center",
@@ -1747,22 +1635,22 @@ function TrackItem({
1747
1635
  children: renderDuration(track)
1748
1636
  }
1749
1637
  ),
1750
- renderActions && /* @__PURE__ */ jsx24("div", { className: "edit flex text-base w-6 items-center justify-center mr-2 cursor-pointer", children: renderActions(track) })
1638
+ renderActions && /* @__PURE__ */ jsx23("div", { className: "edit flex text-base w-6 items-center justify-center mr-2 cursor-pointer", children: renderActions(track) })
1751
1639
  ]
1752
1640
  }
1753
1641
  );
1754
1642
  }
1755
1643
 
1756
1644
  // src/genre-tree/artist/ArtistSongs.tsx
1757
- import { Fragment as Fragment8, jsx as jsx25, jsxs as jsxs19 } from "react/jsx-runtime";
1645
+ import { Fragment as Fragment8, jsx as jsx24, jsxs as jsxs18 } from "react/jsx-runtime";
1758
1646
  var SongPageSchema2 = PaginatedResponseSchema2(SongDetailedSchema2);
1759
1647
  var SORTS = [
1760
1648
  { ordering: "title", label: "Title" },
1761
1649
  { ordering: "-albumYear", label: "Year" }
1762
1650
  ];
1763
1651
  function ArtistSongs({ artist, getBackendBaseUrl, renderArtistLink, renderSongLink }) {
1764
- const [page, setPage] = useState5(1);
1765
- const [ordering, setOrdering] = useState5("title");
1652
+ const [page, setPage] = useState6(1);
1653
+ const [ordering, setOrdering] = useState6("title");
1766
1654
  const { fetch } = useFetchWrapper2(getBackendBaseUrl);
1767
1655
  const { playNewTrackListFromArtist } = useTrackList4();
1768
1656
  const { playerTrackObject } = usePlayer8();
@@ -1787,21 +1675,21 @@ function ArtistSongs({ artist, getBackendBaseUrl, renderArtistLink, renderSongLi
1787
1675
  event.stopPropagation();
1788
1676
  void play(song.uuid);
1789
1677
  };
1790
- return /* @__PURE__ */ jsx25(PanelSection3, { title: "Songs", children: isPending ? /* @__PURE__ */ jsx25(Skeleton3, { className: "h-14", "aria-busy": "true" }) : isError ? /* @__PURE__ */ jsx25("p", { className: "text-red-600", children: "Could not load songs." }) : data.results.length === 0 ? /* @__PURE__ */ jsx25("p", { className: "text-[var(--color-text-muted)]", children: "No songs yet." }) : /* @__PURE__ */ jsxs19(Fragment8, { children: [
1791
- /* @__PURE__ */ jsxs19("div", { className: "flex flex-wrap items-center justify-between gap-2 mb-2", children: [
1792
- /* @__PURE__ */ jsxs19(
1678
+ return /* @__PURE__ */ jsx24(PanelSection5, { title: "Songs", children: isPending ? /* @__PURE__ */ jsx24(Skeleton3, { className: "h-14", "aria-busy": "true" }) : isError ? /* @__PURE__ */ jsx24("p", { className: "text-red-600", children: "Could not load songs." }) : data.results.length === 0 ? /* @__PURE__ */ jsx24("p", { className: "text-[var(--color-text-muted)]", children: "No songs yet." }) : /* @__PURE__ */ jsxs18(Fragment8, { children: [
1679
+ /* @__PURE__ */ jsxs18("div", { className: "flex flex-wrap items-center justify-between gap-2 mb-2", children: [
1680
+ /* @__PURE__ */ jsxs18(
1793
1681
  "button",
1794
1682
  {
1795
1683
  type: "button",
1796
1684
  onClick: () => void play(),
1797
1685
  className: "flex w-fit items-center gap-1.5 px-3 py-1 text-xs font-medium bg-gray-900 text-white rounded-full hover:bg-gray-700",
1798
1686
  children: [
1799
- /* @__PURE__ */ jsx25(Play2, { className: "h-3.5 w-3.5", "aria-hidden": "true" }),
1687
+ /* @__PURE__ */ jsx24(Play2, { className: "h-3.5 w-3.5", "aria-hidden": "true" }),
1800
1688
  "Play all"
1801
1689
  ]
1802
1690
  }
1803
1691
  ),
1804
- /* @__PURE__ */ jsx25("div", { role: "group", "aria-label": "Sort songs by", className: "flex gap-1 text-xs", children: SORTS.map((sort) => /* @__PURE__ */ jsx25(
1692
+ /* @__PURE__ */ jsx24("div", { role: "group", "aria-label": "Sort songs by", className: "flex gap-1 text-xs", children: SORTS.map((sort) => /* @__PURE__ */ jsx24(
1805
1693
  "button",
1806
1694
  {
1807
1695
  type: "button",
@@ -1816,7 +1704,7 @@ function ArtistSongs({ artist, getBackendBaseUrl, renderArtistLink, renderSongLi
1816
1704
  sort.ordering
1817
1705
  )) })
1818
1706
  ] }),
1819
- /* @__PURE__ */ jsx25("ul", { className: "overflow-hidden mb-2 bg-zinc-950 rounded-lg", children: data.results.map((song, index) => /* @__PURE__ */ jsx25("li", { onClickCapture: (event) => handleRowClickCapture(event, song), children: /* @__PURE__ */ jsx25(
1707
+ /* @__PURE__ */ jsx24("ul", { className: "overflow-hidden mb-2 bg-zinc-950 rounded-lg", children: data.results.map((song, index) => /* @__PURE__ */ jsx24("li", { onClickCapture: (event) => handleRowClickCapture(event, song), children: /* @__PURE__ */ jsx24(
1820
1708
  TrackItem,
1821
1709
  {
1822
1710
  track: song,
@@ -1825,12 +1713,12 @@ function ArtistSongs({ artist, getBackendBaseUrl, renderArtistLink, renderSongLi
1825
1713
  renderSongLink
1826
1714
  }
1827
1715
  ) }, song.uuid)) }),
1828
- /* @__PURE__ */ jsx25(Pagination, { currentPage: page, totalPages: data.totalPages, onPageChange: setPage })
1716
+ /* @__PURE__ */ jsx24(Pagination, { currentPage: page, totalPages: data.totalPages, onPageChange: setPage })
1829
1717
  ] }) });
1830
1718
  }
1831
1719
 
1832
1720
  // src/genre-tree/artist/useArtistPanel.tsx
1833
- import { Fragment as Fragment9, jsx as jsx26, jsxs as jsxs20 } from "react/jsx-runtime";
1721
+ import { Fragment as Fragment9, jsx as jsx25, jsxs as jsxs19 } from "react/jsx-runtime";
1834
1722
  function useArtistPanel({
1835
1723
  uuid,
1836
1724
  scope,
@@ -1851,26 +1739,26 @@ function useArtistPanel({
1851
1739
  return {
1852
1740
  ...panel,
1853
1741
  title: "Artist",
1854
- content: /* @__PURE__ */ jsxs20("div", { "aria-busy": "true", className: "flex flex-col gap-2.5 px-6 py-2.5", children: [
1855
- /* @__PURE__ */ jsx26(Skeleton4, { className: "h-3 w-16" }),
1856
- /* @__PURE__ */ jsx26(Skeleton4, { className: "h-4 w-32" }),
1857
- /* @__PURE__ */ jsx26(Skeleton4, { className: "h-3 w-16" }),
1858
- /* @__PURE__ */ jsx26(Skeleton4, { className: "h-4 w-24" }),
1859
- /* @__PURE__ */ jsx26(Skeleton4, { className: "h-16 mt-2.5" }),
1860
- /* @__PURE__ */ jsx26(Skeleton4, { className: "h-16" })
1742
+ content: /* @__PURE__ */ jsxs19("div", { "aria-busy": "true", className: "flex flex-col gap-2.5 px-6 py-2.5", children: [
1743
+ /* @__PURE__ */ jsx25(Skeleton4, { className: "h-3 w-16" }),
1744
+ /* @__PURE__ */ jsx25(Skeleton4, { className: "h-4 w-32" }),
1745
+ /* @__PURE__ */ jsx25(Skeleton4, { className: "h-3 w-16" }),
1746
+ /* @__PURE__ */ jsx25(Skeleton4, { className: "h-4 w-24" }),
1747
+ /* @__PURE__ */ jsx25(Skeleton4, { className: "h-16 mt-2.5" }),
1748
+ /* @__PURE__ */ jsx25(Skeleton4, { className: "h-16" })
1861
1749
  ] })
1862
1750
  };
1863
1751
  case "not-found":
1864
1752
  return {
1865
1753
  ...panel,
1866
1754
  title: "Artist not found",
1867
- content: /* @__PURE__ */ jsx26("p", { className: "px-6 py-2.5 text-[var(--color-text-muted)]", children: "This artist does not exist." })
1755
+ content: /* @__PURE__ */ jsx25("p", { className: "px-6 py-2.5 text-[var(--color-text-muted)]", children: "This artist does not exist." })
1868
1756
  };
1869
1757
  case "error":
1870
1758
  return {
1871
1759
  ...panel,
1872
1760
  title: "Artist",
1873
- content: /* @__PURE__ */ jsx26("p", { className: "px-6 py-2.5 text-red-600", children: "Could not load this artist." })
1761
+ content: /* @__PURE__ */ jsx25("p", { className: "px-6 py-2.5 text-red-600", children: "Could not load this artist." })
1874
1762
  };
1875
1763
  case "success": {
1876
1764
  const { artist } = result;
@@ -1879,13 +1767,13 @@ function useArtistPanel({
1879
1767
  return {
1880
1768
  ...panel,
1881
1769
  title: artist.name,
1882
- content: /* @__PURE__ */ jsxs20(Fragment9, { children: [
1883
- shareUrl && /* @__PURE__ */ jsx26("div", { className: "genre-action-band border-b border-[var(--color-border)]", children: /* @__PURE__ */ jsx26("div", { className: "flex flex-wrap gap-1 px-3.5 py-3", children: shareTarget && /* @__PURE__ */ jsx26(ShareActionButton, { target: shareTarget, url: shareUrl }) }) }),
1884
- artist.image && /* @__PURE__ */ jsx26(ArtistPhoto, { image: artist.image, name: artist.name }),
1885
- /* @__PURE__ */ jsx26(ArtistHeader, { artist }),
1886
- /* @__PURE__ */ jsx26(ArtistMembers, { title: "Members", memberships: artist.members, renderArtistLink }),
1887
- /* @__PURE__ */ jsx26(ArtistMembers, { title: "Member of", memberships: artist.memberOf, renderArtistLink }),
1888
- artist.tracksCount > 0 && /* @__PURE__ */ jsx26(
1770
+ content: /* @__PURE__ */ jsxs19(Fragment9, { children: [
1771
+ shareUrl && /* @__PURE__ */ jsx25("div", { className: "genre-action-band border-b border-[var(--color-border)]", children: /* @__PURE__ */ jsx25("div", { className: "flex flex-wrap gap-1 px-3.5 py-3", children: shareTarget && /* @__PURE__ */ jsx25(ShareActionButton, { target: shareTarget, url: shareUrl }) }) }),
1772
+ artist.image && /* @__PURE__ */ jsx25(ArtistPhoto, { image: artist.image, name: artist.name }),
1773
+ /* @__PURE__ */ jsx25(ArtistHeader, { artist }),
1774
+ /* @__PURE__ */ jsx25(ArtistMembers, { title: "Members", memberships: artist.members, renderArtistLink }),
1775
+ /* @__PURE__ */ jsx25(ArtistMembers, { title: "Member of", memberships: artist.memberOf, renderArtistLink }),
1776
+ artist.tracksCount > 0 && /* @__PURE__ */ jsx25(
1889
1777
  ArtistSongs,
1890
1778
  {
1891
1779
  artist: { uuid: artist.uuid, name: artist.name },
@@ -1895,7 +1783,7 @@ function useArtistPanel({
1895
1783
  }
1896
1784
  )
1897
1785
  ] }),
1898
- extraContent: artist.albums.length > 0 ? /* @__PURE__ */ jsx26(ArtistDiscography, { albums: artist.albums }) : void 0
1786
+ extraContent: artist.albums.length > 0 ? /* @__PURE__ */ jsx25(ArtistDiscography, { albums: artist.albums }) : void 0
1899
1787
  };
1900
1788
  }
1901
1789
  }
@@ -1908,9 +1796,9 @@ import {
1908
1796
  } from "@behindthemusictree/app-kit/genre-tree";
1909
1797
 
1910
1798
  // src/genre-tree/song/SongPanelContent.tsx
1911
- import { useState as useState6 } from "react";
1799
+ import { useState as useState7 } from "react";
1912
1800
  import { Pause as Pause2, Play as Play3 } from "lucide-react";
1913
- import { FieldList as FieldList2, PanelSection as PanelSection4, Skeleton as Skeleton5 } from "@behindthemusictree/ui";
1801
+ import { FieldList as FieldList2, PanelSection as PanelSection6, Skeleton as Skeleton5 } from "@behindthemusictree/ui";
1914
1802
  import { PlayStates as PlayStates3, usePlayer as usePlayer9 } from "@behindthemusictree/app-kit/player";
1915
1803
  import {
1916
1804
  ArtistNames as ArtistNames3,
@@ -1919,40 +1807,40 @@ import {
1919
1807
  useSongHistory,
1920
1808
  useTrackList as useTrackList5
1921
1809
  } from "@behindthemusictree/app-kit/genre-tree";
1922
- import { Fragment as Fragment10, jsx as jsx27, jsxs as jsxs21 } from "react/jsx-runtime";
1810
+ import { Fragment as Fragment10, jsx as jsx26, jsxs as jsxs20 } from "react/jsx-runtime";
1923
1811
  var numberFormat = new Intl.NumberFormat("en");
1924
1812
  var dateFormat = new Intl.DateTimeFormat("en", { dateStyle: "medium" });
1925
1813
  var externalLink = "text-[var(--color-primary)] hover:underline focus-visible:underline";
1926
1814
  function SongHistory({ uuid, getBackendBaseUrl }) {
1927
- const [open, setOpen] = useState6(false);
1815
+ const [open, setOpen] = useState7(false);
1928
1816
  const history = useSongHistory(uuid, getBackendBaseUrl, open);
1929
1817
  let body = null;
1930
- if (history.status === "loading") body = /* @__PURE__ */ jsx27(Skeleton5, { className: "h-10", "aria-busy": "true" });
1931
- else if (history.status === "error") body = /* @__PURE__ */ jsx27("p", { className: "text-red-600", children: "Could not load the history." });
1818
+ if (history.status === "loading") body = /* @__PURE__ */ jsx26(Skeleton5, { className: "h-10", "aria-busy": "true" });
1819
+ else if (history.status === "error") body = /* @__PURE__ */ jsx26("p", { className: "text-red-600", children: "Could not load the history." });
1932
1820
  else if (history.status === "success")
1933
- body = history.entries.length === 0 ? /* @__PURE__ */ jsx27("p", { className: "text-[var(--color-text-muted)]", children: "No changes yet." }) : /* @__PURE__ */ jsx27("ul", { className: "divide-y divide-[var(--color-border)]", children: history.entries.map((entry) => /* @__PURE__ */ jsxs21("li", { className: "py-1.5 text-sm", children: [
1934
- /* @__PURE__ */ jsx27("span", { className: "font-medium", children: entry.action }),
1935
- entry.oldValue !== null || entry.newValue !== null ? /* @__PURE__ */ jsxs21("span", { className: "text-[var(--color-text-muted)]", children: [
1821
+ body = history.entries.length === 0 ? /* @__PURE__ */ jsx26("p", { className: "text-[var(--color-text-muted)]", children: "No changes yet." }) : /* @__PURE__ */ jsx26("ul", { className: "divide-y divide-[var(--color-border)]", children: history.entries.map((entry) => /* @__PURE__ */ jsxs20("li", { className: "py-1.5 text-sm", children: [
1822
+ /* @__PURE__ */ jsx26("span", { className: "font-medium", children: entry.action }),
1823
+ entry.oldValue !== null || entry.newValue !== null ? /* @__PURE__ */ jsxs20("span", { className: "text-[var(--color-text-muted)]", children: [
1936
1824
  " ",
1937
1825
  "\u2014 ",
1938
1826
  entry.oldValue ?? "none",
1939
1827
  " \u2192 ",
1940
1828
  entry.newValue ?? "none"
1941
1829
  ] }) : null,
1942
- /* @__PURE__ */ jsxs21("span", { className: "block text-xs text-[var(--color-text-muted)]", children: [
1830
+ /* @__PURE__ */ jsxs20("span", { className: "block text-xs text-[var(--color-text-muted)]", children: [
1943
1831
  entry.actorPseudo ?? "Anonymous",
1944
1832
  " \xB7 ",
1945
1833
  dateFormat.format(new Date(entry.createdOn))
1946
1834
  ] })
1947
1835
  ] }, entry.uuid)) });
1948
- return /* @__PURE__ */ jsxs21(
1836
+ return /* @__PURE__ */ jsxs20(
1949
1837
  "details",
1950
1838
  {
1951
1839
  onToggle: (event) => setOpen(event.currentTarget.open),
1952
1840
  className: "px-6 pb-2.5 mt-2.5 border-t border-[var(--color-border)]",
1953
1841
  children: [
1954
- /* @__PURE__ */ jsx27("summary", { className: "mt-2.5 cursor-pointer text-[0.6875rem] font-semibold uppercase text-[var(--color-text-muted)]", children: "History" }),
1955
- /* @__PURE__ */ jsx27("div", { className: "mt-1.5", children: body })
1842
+ /* @__PURE__ */ jsx26("summary", { className: "mt-2.5 cursor-pointer text-[0.6875rem] font-semibold uppercase text-[var(--color-text-muted)]", children: "History" }),
1843
+ /* @__PURE__ */ jsx26("div", { className: "mt-1.5", children: body })
1956
1844
  ]
1957
1845
  }
1958
1846
  );
@@ -1978,13 +1866,13 @@ function SongPanelContent({
1978
1866
  const path = genrePath.length > 0 ? genrePath : null;
1979
1867
  fields.push({
1980
1868
  label: "Genre",
1981
- value: path ? /* @__PURE__ */ jsx27("span", { className: "flex flex-wrap items-center gap-x-1", children: path.map((genrePlaylist, index) => /* @__PURE__ */ jsxs21("span", { children: [
1982
- index > 0 && /* @__PURE__ */ jsx27("span", { "aria-hidden": "true", children: "\u203A " }),
1983
- /* @__PURE__ */ jsx27("button", { type: "button", onClick: () => onSelectGenre(genrePlaylist), className: "hover:underline", children: genrePlaylist.name })
1869
+ value: path ? /* @__PURE__ */ jsx26("span", { className: "flex flex-wrap items-center gap-x-1", children: path.map((genrePlaylist, index) => /* @__PURE__ */ jsxs20("span", { children: [
1870
+ index > 0 && /* @__PURE__ */ jsx26("span", { "aria-hidden": "true", children: "\u203A " }),
1871
+ /* @__PURE__ */ jsx26("button", { type: "button", onClick: () => onSelectGenre(genrePlaylist), className: "hover:underline", children: genrePlaylist.name })
1984
1872
  ] }, genrePlaylist.uuid)) }) : song.genre.name
1985
1873
  });
1986
1874
  } else {
1987
- fields.push({ label: "Genre", value: /* @__PURE__ */ jsx27("span", { className: "text-[var(--color-text-muted)]", children: "No genre" }) });
1875
+ fields.push({ label: "Genre", value: /* @__PURE__ */ jsx26("span", { className: "text-[var(--color-text-muted)]", children: "No genre" }) });
1988
1876
  }
1989
1877
  if (song.firstReleaseYear) fields.push({ label: "First released", value: song.firstReleaseYear });
1990
1878
  const viewCount = song.youtubeVideo?.viewCount;
@@ -1994,11 +1882,11 @@ function SongPanelContent({
1994
1882
  const links = [];
1995
1883
  if (musicBrainzUrl)
1996
1884
  links.push(
1997
- /* @__PURE__ */ jsx27("a", { href: musicBrainzUrl, target: "_blank", rel: "noopener noreferrer", className: externalLink, children: "MusicBrainz" }, "mb")
1885
+ /* @__PURE__ */ jsx26("a", { href: musicBrainzUrl, target: "_blank", rel: "noopener noreferrer", className: externalLink, children: "MusicBrainz" }, "mb")
1998
1886
  );
1999
1887
  if (song.youtubeVideo)
2000
1888
  links.push(
2001
- /* @__PURE__ */ jsx27(
1889
+ /* @__PURE__ */ jsx26(
2002
1890
  "a",
2003
1891
  {
2004
1892
  href: `https://www.youtube.com/watch?v=${encodeURIComponent(song.youtubeVideo.id)}`,
@@ -2013,33 +1901,33 @@ function SongPanelContent({
2013
1901
  if (links.length > 0)
2014
1902
  fields.push({
2015
1903
  label: "Links",
2016
- value: /* @__PURE__ */ jsx27("span", { className: "flex gap-3", children: links })
1904
+ value: /* @__PURE__ */ jsx26("span", { className: "flex gap-3", children: links })
2017
1905
  });
2018
- return /* @__PURE__ */ jsxs21(Fragment10, { children: [
2019
- /* @__PURE__ */ jsx27("div", { className: "genre-action-band border-b border-[var(--color-border)]", children: /* @__PURE__ */ jsxs21("div", { className: "flex flex-wrap gap-1 px-3.5 py-3", children: [
2020
- unplayableReason ? /* @__PURE__ */ jsx27("p", { className: "flex items-center px-2.5 text-sm text-[var(--color-text-muted)]", children: unplayableReason }) : /* @__PURE__ */ jsxs21(
1906
+ return /* @__PURE__ */ jsxs20(Fragment10, { children: [
1907
+ /* @__PURE__ */ jsx26("div", { className: "genre-action-band border-b border-[var(--color-border)]", children: /* @__PURE__ */ jsxs20("div", { className: "flex flex-wrap gap-1 px-3.5 py-3", children: [
1908
+ unplayableReason ? /* @__PURE__ */ jsx26("p", { className: "flex items-center px-2.5 text-sm text-[var(--color-text-muted)]", children: unplayableReason }) : /* @__PURE__ */ jsxs20(
2021
1909
  "button",
2022
1910
  {
2023
1911
  type: "button",
2024
1912
  onClick: () => isCurrent ? handlePlayPauseAction() : playNewTrackListFromTrackUuid(song, scope),
2025
1913
  className: "group flex flex-col items-center gap-1 w-16 text-xs text-center text-[var(--color-text)] focus-visible:outline-none",
2026
1914
  children: [
2027
- /* @__PURE__ */ jsx27("span", { className: "flex items-center justify-center w-10 h-10 rounded-full group-focus-visible:ring-2 group-focus-visible:ring-[var(--color-focus-ring)] bg-[var(--color-surface-muted)] transition-colors group-hover:bg-[var(--color-border)]", children: isPlaying ? /* @__PURE__ */ jsx27(Pause2, { "aria-hidden": "true", className: "w-5 h-5" }) : /* @__PURE__ */ jsx27(Play3, { "aria-hidden": "true", className: "w-5 h-5" }) }),
1915
+ /* @__PURE__ */ jsx26("span", { className: "flex items-center justify-center w-10 h-10 rounded-full group-focus-visible:ring-2 group-focus-visible:ring-[var(--color-focus-ring)] bg-[var(--color-surface-muted)] transition-colors group-hover:bg-[var(--color-border)]", children: isPlaying ? /* @__PURE__ */ jsx26(Pause2, { "aria-hidden": "true", className: "w-5 h-5" }) : /* @__PURE__ */ jsx26(Play3, { "aria-hidden": "true", className: "w-5 h-5" }) }),
2028
1916
  isPlaying ? "Pause" : "Play"
2029
1917
  ]
2030
1918
  }
2031
1919
  ),
2032
- shareUrl && /* @__PURE__ */ jsx27(ShareActionButton, { target: shareTarget, url: shareUrl })
1920
+ shareUrl && /* @__PURE__ */ jsx26(ShareActionButton, { target: shareTarget, url: shareUrl })
2033
1921
  ] }) }),
2034
- song.artists && song.artists.length > 0 && /* @__PURE__ */ jsx27("p", { className: "px-6 pt-2.5 text-[var(--color-text-muted)]", children: /* @__PURE__ */ jsx27(ArtistNames3, { artists: song.artists, renderArtistLink }) }),
2035
- /* @__PURE__ */ jsx27(FieldList2, { items: fields }),
2036
- song.essentialGenres && song.essentialGenres.length > 0 && /* @__PURE__ */ jsx27(PanelSection4, { title: "Essential in", children: /* @__PURE__ */ jsx27("ul", { children: song.essentialGenres.map((genre) => /* @__PURE__ */ jsx27("li", { className: "py-0.5", children: genre.name }, genre.uuid)) }) }),
2037
- /* @__PURE__ */ jsx27(SongHistory, { uuid: song.uuid, getBackendBaseUrl })
1922
+ song.artists && song.artists.length > 0 && /* @__PURE__ */ jsx26("p", { className: "px-6 pt-2.5 text-[var(--color-text-muted)]", children: /* @__PURE__ */ jsx26(ArtistNames3, { artists: song.artists, renderArtistLink }) }),
1923
+ /* @__PURE__ */ jsx26(FieldList2, { items: fields }),
1924
+ song.essentialGenres && song.essentialGenres.length > 0 && /* @__PURE__ */ jsx26(PanelSection6, { title: "Essential in", children: /* @__PURE__ */ jsx26("ul", { children: song.essentialGenres.map((genre) => /* @__PURE__ */ jsx26("li", { className: "py-0.5", children: genre.name }, genre.uuid)) }) }),
1925
+ /* @__PURE__ */ jsx26(SongHistory, { uuid: song.uuid, getBackendBaseUrl })
2038
1926
  ] });
2039
1927
  }
2040
1928
 
2041
1929
  // src/genre-tree/song/useSongPanel.tsx
2042
- import { jsx as jsx28, jsxs as jsxs22 } from "react/jsx-runtime";
1930
+ import { jsx as jsx27, jsxs as jsxs21 } from "react/jsx-runtime";
2043
1931
  function useSongPanel({
2044
1932
  uuid,
2045
1933
  scope,
@@ -2061,24 +1949,24 @@ function useSongPanel({
2061
1949
  return {
2062
1950
  ...panel,
2063
1951
  title: "Song",
2064
- content: /* @__PURE__ */ jsxs22("div", { "aria-busy": "true", className: "flex flex-col gap-2.5 px-6 py-2.5", children: [
2065
- /* @__PURE__ */ jsx28(Skeleton6, { className: "h-3 w-16" }),
2066
- /* @__PURE__ */ jsx28(Skeleton6, { className: "h-4 w-32" }),
2067
- /* @__PURE__ */ jsx28(Skeleton6, { className: "h-3 w-16" }),
2068
- /* @__PURE__ */ jsx28(Skeleton6, { className: "h-4 w-24" })
1952
+ content: /* @__PURE__ */ jsxs21("div", { "aria-busy": "true", className: "flex flex-col gap-2.5 px-6 py-2.5", children: [
1953
+ /* @__PURE__ */ jsx27(Skeleton6, { className: "h-3 w-16" }),
1954
+ /* @__PURE__ */ jsx27(Skeleton6, { className: "h-4 w-32" }),
1955
+ /* @__PURE__ */ jsx27(Skeleton6, { className: "h-3 w-16" }),
1956
+ /* @__PURE__ */ jsx27(Skeleton6, { className: "h-4 w-24" })
2069
1957
  ] })
2070
1958
  };
2071
1959
  case "not-found":
2072
1960
  return {
2073
1961
  ...panel,
2074
1962
  title: "Song not found",
2075
- content: /* @__PURE__ */ jsx28("p", { className: "px-6 py-2.5 text-[var(--color-text-muted)]", children: "This song does not exist." })
1963
+ content: /* @__PURE__ */ jsx27("p", { className: "px-6 py-2.5 text-[var(--color-text-muted)]", children: "This song does not exist." })
2076
1964
  };
2077
1965
  case "error":
2078
1966
  return {
2079
1967
  ...panel,
2080
1968
  title: "Song",
2081
- content: /* @__PURE__ */ jsx28("p", { className: "px-6 py-2.5 text-red-600", children: "Could not load this song." })
1969
+ content: /* @__PURE__ */ jsx27("p", { className: "px-6 py-2.5 text-red-600", children: "Could not load this song." })
2082
1970
  };
2083
1971
  case "success": {
2084
1972
  const { song } = result;
@@ -2086,7 +1974,7 @@ function useSongPanel({
2086
1974
  return {
2087
1975
  ...panel,
2088
1976
  title: song.title,
2089
- content: /* @__PURE__ */ jsx28(
1977
+ content: /* @__PURE__ */ jsx27(
2090
1978
  SongPanelContent,
2091
1979
  {
2092
1980
  song,
@@ -2106,12 +1994,12 @@ function useSongPanel({
2106
1994
  // src/genre-tree/SizeMetricToggle.tsx
2107
1995
  import { MdMusicNote, MdVisibility } from "react-icons/md";
2108
1996
  import { SegmentedControl } from "@behindthemusictree/ui";
2109
- import { jsx as jsx29 } from "react/jsx-runtime";
1997
+ import { jsx as jsx28 } from "react/jsx-runtime";
2110
1998
  function SizeMetricToggle({
2111
1999
  value,
2112
2000
  onChange
2113
2001
  }) {
2114
- return /* @__PURE__ */ jsx29(
2002
+ return /* @__PURE__ */ jsx28(
2115
2003
  SegmentedControl,
2116
2004
  {
2117
2005
  variant: "floating",
@@ -2119,26 +2007,19 @@ function SizeMetricToggle({
2119
2007
  value,
2120
2008
  onChange,
2121
2009
  options: [
2122
- { value: "songs", label: /* @__PURE__ */ jsx29(MdMusicNote, { size: 18, "aria-hidden": true }), "aria-label": "Size by songs", title: "Node size by song count" },
2123
- { value: "views", label: /* @__PURE__ */ jsx29(MdVisibility, { size: 18, "aria-hidden": true }), "aria-label": "Size by views", title: "Node size by song views" }
2010
+ { value: "songs", label: /* @__PURE__ */ jsx28(MdMusicNote, { size: 18, "aria-hidden": true }), "aria-label": "Size by songs", title: "Node size by song count" },
2011
+ { value: "views", label: /* @__PURE__ */ jsx28(MdVisibility, { size: 18, "aria-hidden": true }), "aria-label": "Size by views", title: "Node size by song views" }
2124
2012
  ]
2125
2013
  }
2126
2014
  );
2127
2015
  }
2128
2016
 
2129
2017
  // src/genre-tree/GenreTreeView.tsx
2130
- import { Fragment as Fragment11, jsx as jsx30, jsxs as jsxs23 } from "react/jsx-runtime";
2131
- var PREVIOUS_BUTTON_CSS = `
2132
- .genre-tree-view .search-overlay .gtv-info-panel-previous { display: none; }
2133
- .genre-tree-view:has(.search-overlay .gtv-info-panel-previous) .genre-search input { padding-left: 2.75rem; }
2134
- .genre-tree-view:has(.search-overlay .gtv-info-panel-previous) .genre-search-previous { display: flex; }
2135
- @keyframes genre-tree-view-previous-in { from { opacity: 0; transform: translate(-0.5rem, -50%); } to { opacity: 1; transform: translate(0, -50%); } }
2136
- @media (prefers-reduced-motion: no-preference) {
2137
- .genre-search-previous { animation: genre-tree-view-previous-in 200ms ease-out; }
2138
- }
2139
- `;
2018
+ import { Fragment as Fragment11, jsx as jsx29, jsxs as jsxs22 } from "react/jsx-runtime";
2140
2019
  var NO_POP_ROOT_TITLE = "This genre tree has no 'Mainstream Pop' root yet";
2141
- var REGIONS_CLASS = "[--gtv-center-left:var(--layout-center-left)] [--gtv-center-right:var(--layout-center-right)] [--gtv-center-top:var(--layout-band-top)] [&_.gtv-info-panel--overlay]:[--gtv-info-panel-header-inset:0px]";
2020
+ var REGIONS_CLASS = "[--gtv-center-left:var(--layout-center-left)] [--gtv-center-right:var(--layout-center-right)] [--gtv-center-top:var(--layout-band-top)]";
2021
+ var noop = () => {
2022
+ };
2142
2023
  function GenreTreeView({
2143
2024
  scope,
2144
2025
  handleGenreCreationAction,
@@ -2192,19 +2073,19 @@ function GenreTreeView({
2192
2073
  placement = "default",
2193
2074
  ...focus
2194
2075
  }) {
2195
- const getShareUrlRef = useRef4(getShareUrl);
2076
+ const getShareUrlRef = useRef3(getShareUrl);
2196
2077
  getShareUrlRef.current = getShareUrl;
2197
- const [internalArtistUuid, setInternalArtistUuid] = useState7(null);
2078
+ const [internalArtistUuid, setInternalArtistUuid] = useState8(null);
2198
2079
  const isArtistControlled = controlledArtistUuid !== void 0;
2199
2080
  const artistUuid = isArtistControlled ? controlledArtistUuid : internalArtistUuid;
2200
- const [internalSongUuid, setInternalSongUuid] = useState7(null);
2081
+ const [internalSongUuid, setInternalSongUuid] = useState8(null);
2201
2082
  const isSongControlled = controlledSongUuid !== void 0;
2202
2083
  const songUuid = isSongControlled ? controlledSongUuid : internalSongUuid;
2203
2084
  const changeSong = (uuid) => {
2204
2085
  if (!isSongControlled) setInternalSongUuid(uuid);
2205
2086
  onSongUuidChange?.(uuid);
2206
2087
  };
2207
- const [internalSizeMetric, setInternalSizeMetric] = useState7("songs");
2088
+ const [internalSizeMetric, setInternalSizeMetric] = useState8("songs");
2208
2089
  const sizeMetric = controlledSizeMetric ?? internalSizeMetric;
2209
2090
  const deferredSizeMetric = useDeferredValue(sizeMetric);
2210
2091
  const changeSizeMetric = (metric) => {
@@ -2243,7 +2124,7 @@ function GenreTreeView({
2243
2124
  artistUuid
2244
2125
  });
2245
2126
  const restoreRevision = viewportRestore?.revision;
2246
- const [stackedRestoreOwner, setStackedRestoreOwner] = useState7({ revision: restoreRevision, rootUuid: selectedGenrePlaylist?.root.uuid });
2127
+ const [stackedRestoreOwner, setStackedRestoreOwner] = useState8({ revision: restoreRevision, rootUuid: selectedGenrePlaylist?.root.uuid });
2247
2128
  let restoreRootUuid = stackedRestoreOwner.rootUuid;
2248
2129
  if (stackedRestoreOwner.revision !== restoreRevision) {
2249
2130
  restoreRootUuid = selectedGenrePlaylist?.root.uuid;
@@ -2274,14 +2155,14 @@ function GenreTreeView({
2274
2155
  if (selectedNodeId !== null) clearSelection();
2275
2156
  changeSong(uuid);
2276
2157
  };
2277
- const openSongRef = useRef4(openSong);
2158
+ const openSongRef = useRef3(openSong);
2278
2159
  openSongRef.current = openSong;
2279
2160
  const resolvedRenderSongLink = useCallback6(
2280
- (song) => renderSongLink ? renderSongLink(song) : /* @__PURE__ */ jsx30("button", { type: "button", onClick: () => openSongRef.current(song.uuid), className: "hover:underline", children: song.title }),
2161
+ (song) => renderSongLink ? renderSongLink(song) : /* @__PURE__ */ jsx29("button", { type: "button", onClick: () => openSongRef.current(song.uuid), className: "hover:underline", children: song.title }),
2281
2162
  [renderSongLink]
2282
2163
  );
2283
- const [expandedSongUuid, setExpandedSongUuid] = useState7(null);
2284
- const [expandedArtistUuid, setExpandedArtistUuid] = useState7(null);
2164
+ const [expandedSongUuid, setExpandedSongUuid] = useState8(null);
2165
+ const [expandedArtistUuid, setExpandedArtistUuid] = useState8(null);
2285
2166
  const artistBaseUrl = artistSearchBaseUrl ?? getBackendBaseUrl;
2286
2167
  const songBaseUrl = getBackendBaseUrl;
2287
2168
  const artistPanel = useArtistPanel({
@@ -2289,7 +2170,7 @@ function GenreTreeView({
2289
2170
  scope,
2290
2171
  getBackendBaseUrl: artistBaseUrl,
2291
2172
  getShareUrl: getShareUrlRef.current,
2292
- renderArtistLink: renderArtistLink ?? ((artist) => artist.isSpecialPurpose ? artist.name : /* @__PURE__ */ jsx30("button", { type: "button", onClick: () => openArtist(artist.uuid), className: "hover:underline", children: artist.name })),
2173
+ renderArtistLink: renderArtistLink ?? ((artist) => artist.isSpecialPurpose ? artist.name : /* @__PURE__ */ jsx29("button", { type: "button", onClick: () => openArtist(artist.uuid), className: "hover:underline", children: artist.name })),
2293
2174
  expanded: controlledArtistExpanded ?? (artistUuid !== null && expandedArtistUuid === artistUuid),
2294
2175
  onExpandedChange: (on) => {
2295
2176
  if (controlledArtistExpanded === void 0) setExpandedArtistUuid(on ? artistUuid : null);
@@ -2298,7 +2179,7 @@ function GenreTreeView({
2298
2179
  renderSongLink: resolvedRenderSongLink,
2299
2180
  onClose: () => changeArtist(null)
2300
2181
  });
2301
- const resolvedRenderArtistLink = renderArtistLink ?? ((artist) => artist.isSpecialPurpose ? artist.name : /* @__PURE__ */ jsx30("button", { type: "button", onClick: () => openArtist(artist.uuid), className: "hover:underline", children: artist.name }));
2182
+ const resolvedRenderArtistLink = renderArtistLink ?? ((artist) => artist.isSpecialPurpose ? artist.name : /* @__PURE__ */ jsx29("button", { type: "button", onClick: () => openArtist(artist.uuid), className: "hover:underline", children: artist.name }));
2302
2183
  const getGenrePath = (genre) => {
2303
2184
  const path = [];
2304
2185
  let current = visibleGenrePlaylists.find((playlist) => playlist.criteria?.uuid === genre.uuid);
@@ -2330,9 +2211,8 @@ function GenreTreeView({
2330
2211
  const builtInSongPanel = overlayPanel || hasSelectedGenreNode ? void 0 : songPanel;
2331
2212
  const builtInArtistPanel = overlayPanel || hasSelectedGenreNode || builtInSongPanel ? void 0 : artistPanel;
2332
2213
  const builtInPanel = builtInSongPanel ?? builtInArtistPanel;
2333
- const activeOverlayPanel = overlayPanel ?? (builtInPanel && { ...builtInPanel, hideTitle: true, hideClose: true });
2334
- const [uncontrolledCollapsed, setUncontrolledCollapsed] = useState7(false);
2335
- const [uncontrolledExpanded, setUncontrolledExpanded] = useState7(false);
2214
+ const [uncontrolledCollapsed, setUncontrolledCollapsed] = useState8(false);
2215
+ const [uncontrolledExpanded, setUncontrolledExpanded] = useState8(false);
2336
2216
  const isCollapsed = collapsed ?? uncontrolledCollapsed;
2337
2217
  const isExpanded = expanded ?? uncontrolledExpanded;
2338
2218
  const changeCollapsed = (next) => {
@@ -2343,55 +2223,56 @@ function GenreTreeView({
2343
2223
  setUncontrolledExpanded(next);
2344
2224
  onExpandedChange?.(next);
2345
2225
  };
2346
- useEffect6(() => {
2226
+ useEffect5(() => {
2347
2227
  setUncontrolledCollapsed(false);
2348
2228
  }, [selectedNodeId, artistUuid, songUuid]);
2349
- const isDetailHidden = isCollapsed || !hasSelectedGenreNode && !activeOverlayPanel;
2350
- useLeftDetailPanelLayout(!isDetailHidden, placement === "regions");
2351
- const detailOverlay = placement === "regions" && activeOverlayPanel?.onExpandedChange ? activeOverlayPanel : void 0;
2352
- const detailExpanded = detailOverlay ? !!detailOverlay.expanded : isExpanded;
2353
- const onDetailExpandedChange = detailOverlay ? detailOverlay.onExpandedChange : changeExpanded;
2354
- const infoPanelCuration = {
2355
- renderDangerZone,
2356
- expanded: placement === "regions" ? isExpanded : expanded,
2357
- onExpandedChange: placement === "regions" ? changeExpanded : onExpandedChange,
2358
- collapsed: placement === "regions" ? isCollapsed : void 0,
2359
- hideExpandButton: placement === "regions" ? true : void 0,
2360
- overlayPanel: detailOverlay ? { ...detailOverlay, collapsed: isCollapsed, hideExpandButton: true } : activeOverlayPanel
2361
- };
2362
- const [isWheelRevealed, setIsWheelRevealed] = useState7(false);
2229
+ const [isWheelRevealed, setIsWheelRevealed] = useState8(false);
2363
2230
  const usesHandoff = viewMode === "wheel" || viewMode === "pop-core";
2364
2231
  const isTreeReady = !isLoading && (!usesHandoff || isWheelRevealed);
2365
- const rootRef = useRef4(null);
2366
- const handlePrevious = () => {
2367
- rootRef.current?.querySelector(".search-overlay .gtv-info-panel-previous")?.click();
2368
- };
2369
- useEffect6(() => {
2370
- if (!isTreeReady || viewMode !== "pop-core" || !hasSelectedGenreNode || !selectedGenrePlaylist) return;
2371
- const root = rootRef.current;
2372
- const centerChip = root?.querySelector(".gtv-wheel-center-node .gtv-wheel-chip");
2373
- if (!centerChip || centerChip.getAttribute("aria-label") !== selectedGenrePlaylist.name) return;
2374
- const panelTitle = root?.querySelector(".gtv-info-panel-title");
2375
- if (panelTitle?.textContent?.trim().startsWith(selectedGenrePlaylist.name)) return;
2376
- centerChip.click();
2377
- }, [hasSelectedGenreNode, isTreeReady, selectedGenrePlaylist, viewMode]);
2232
+ const [resolveStyle, setResolveStyle] = useState8(null);
2233
+ const handleNodeStyleResolverChange = useCallback6(
2234
+ (resolver) => setResolveStyle(() => resolver),
2235
+ []
2236
+ );
2237
+ const panelRef = useRef3(null);
2238
+ const obscuredInset = useObscuredInset(panelRef);
2239
+ const hostPanel = { obscuredInset, onNodeStyleResolverChange: handleNodeStyleResolverChange };
2240
+ const nodeMetric = isArtistFiltered || !canSizeByViews ? "songs" : deferredSizeMetric;
2241
+ const treeNodes = useMemo7(() => toTreeNodes(visibleGenrePlaylists, nodeMetric), [visibleGenrePlaylists, nodeMetric]);
2242
+ const relations = useGenreNodeRelations(
2243
+ treeNodes,
2244
+ !overlayPanel && hasSelectedGenreNode ? selectedNodeId : null,
2245
+ resolveStyle
2246
+ );
2247
+ const [selectionHistory, setSelectionHistory] = useState8({
2248
+ current: selectedNodeId,
2249
+ previous: null
2250
+ });
2251
+ if (selectionHistory.current !== selectedNodeId) {
2252
+ setSelectionHistory({
2253
+ current: selectedNodeId,
2254
+ previous: selectionHistory.current ?? selectionHistory.previous
2255
+ });
2256
+ }
2257
+ const previousPlaylist = selectionHistory.previous !== null && selectionHistory.previous !== selectedNodeId ? genrePlaylistByNodeId.get(selectionHistory.previous) : void 0;
2258
+ const handlePrevious = previousPlaylist && visibleGenrePlaylists.includes(previousPlaylist) ? () => handleGenreSearchSelect(previousPlaylist) : void 0;
2378
2259
  const { showPopup, hidePopup } = usePopup5();
2379
- const [searchText, setSearchText] = useState7("");
2260
+ const [searchText, setSearchText] = useState8("");
2380
2261
  const artistSearchResult = useArtistSearch(artistBaseUrl, artistSearch ? searchText : "");
2381
2262
  const artistGroup = artistSearch ? {
2382
2263
  key: "artists",
2383
2264
  label: "Artists",
2384
2265
  items: artistSearchResult.artists.map((artist) => ({
2385
2266
  key: artist.uuid,
2386
- content: /* @__PURE__ */ jsxs23(Fragment11, { children: [
2387
- /* @__PURE__ */ jsxs23("span", { className: "block truncate font-medium", children: [
2267
+ content: /* @__PURE__ */ jsxs22(Fragment11, { children: [
2268
+ /* @__PURE__ */ jsxs22("span", { className: "block truncate font-medium", children: [
2388
2269
  artist.name,
2389
- artist.disambiguation && /* @__PURE__ */ jsxs23("span", { className: "font-normal text-[var(--color-neutral-500)]", children: [
2270
+ artist.disambiguation && /* @__PURE__ */ jsxs22("span", { className: "font-normal text-[var(--color-neutral-500)]", children: [
2390
2271
  " \xB7 ",
2391
2272
  artist.disambiguation
2392
2273
  ] })
2393
2274
  ] }),
2394
- artist.matchedAlias && /* @__PURE__ */ jsxs23("span", { className: "block truncate text-xs text-[var(--color-neutral-500)]", children: [
2275
+ artist.matchedAlias && /* @__PURE__ */ jsxs22("span", { className: "block truncate text-xs text-[var(--color-neutral-500)]", children: [
2395
2276
  "also known as ",
2396
2277
  artist.matchedAlias
2397
2278
  ] })
@@ -2410,9 +2291,9 @@ function GenreTreeView({
2410
2291
  label: "Songs",
2411
2292
  items: songSearchResult.songs.map((song) => ({
2412
2293
  key: song.uuid,
2413
- content: /* @__PURE__ */ jsxs23("span", { className: isSongSearchResultPlayable(song) ? void 0 : "opacity-60", children: [
2414
- /* @__PURE__ */ jsx30("span", { className: "block truncate font-medium", children: song.title }),
2415
- /* @__PURE__ */ jsx30("span", { className: "block truncate text-xs text-[var(--color-neutral-500)]", children: [song.artists.map((artist) => artist.name).join(", "), song.genre?.name ?? "No genre"].filter(Boolean).join(" \xB7 ") })
2294
+ content: /* @__PURE__ */ jsxs22("span", { className: isSongSearchResultPlayable(song) ? void 0 : "opacity-60", children: [
2295
+ /* @__PURE__ */ jsx29("span", { className: "block truncate font-medium", children: song.title }),
2296
+ /* @__PURE__ */ jsx29("span", { className: "block truncate text-xs text-[var(--color-neutral-500)]", children: [song.artists.map((artist) => artist.name).join(", "), song.genre?.name ?? "No genre"].filter(Boolean).join(" \xB7 ") })
2416
2297
  ] }),
2417
2298
  onSelect: () => {
2418
2299
  openSong(song.uuid);
@@ -2438,36 +2319,49 @@ function GenreTreeView({
2438
2319
  const handleGenreSearchZoomIn = () => {
2439
2320
  if (selectedUuid) focus.onFocusGenreChange?.(selectedUuid, "zoom-in");
2440
2321
  };
2441
- const zoomInRef = useRef4(void 0);
2322
+ const zoomInRef = useRef3(void 0);
2442
2323
  zoomInRef.current = canZoomIn ? handleGenreSearchZoomIn : void 0;
2443
- const renderInfoPanelBody = useCallback6(
2444
- (node, { toolbar, ancestors, family }) => {
2445
- const genrePlaylist = genrePlaylistByNodeId.get(node.id);
2446
- const criteria = genrePlaylist?.criteria;
2447
- if (!genrePlaylist || !criteria) return { primary: /* @__PURE__ */ jsxs23(Fragment11, { children: [
2448
- toolbar,
2449
- family
2450
- ] }), aside: ancestors };
2451
- const genreUuid = criteria.uuid;
2452
- const parentPlaylist = visibleGenrePlaylists.find(({ uuid }) => uuid === genrePlaylist.parent?.uuid);
2453
- const canToggleSide = !readOnly && parentPlaylist !== void 0 && parentPlaylist.parent === null;
2454
- const side = criteria.side ?? "core";
2455
- const shareTarget = {
2456
- kind: "genre",
2457
- genrePlaylistUuid: genrePlaylist.uuid,
2458
- name: criteria.name,
2459
- scope,
2460
- artistUuid
2461
- };
2462
- const shareUrl = getShareUrlRef.current?.(shareTarget) ?? void 0;
2463
- const popSibling = canToggleSide && side === "core" ? visibleGenrePlaylists.find(
2464
- (sibling) => sibling.parent?.uuid === parentPlaylist.uuid && sibling.uuid !== genrePlaylist.uuid && sibling.criteria?.side === "pop"
2465
- ) : void 0;
2466
- return {
2467
- primary: /* @__PURE__ */ jsx30(
2324
+ const selectGenreFromPanel = (nodeId) => {
2325
+ const genrePlaylist = genrePlaylistByNodeId.get(nodeId);
2326
+ if (genrePlaylist) handleGenreSearchSelect(genrePlaylist);
2327
+ };
2328
+ const renderGenreBody = (relation) => {
2329
+ const { node } = relation;
2330
+ const family = /* @__PURE__ */ jsx29(
2331
+ GenreNodeFamily,
2332
+ {
2333
+ node,
2334
+ fill: relation.fill,
2335
+ textColor: relation.textColor,
2336
+ parentNode: relation.parentNode,
2337
+ childNodes: relation.childNodes,
2338
+ onSelectNode: selectGenreFromPanel
2339
+ }
2340
+ );
2341
+ const ancestors = /* @__PURE__ */ jsx29(GenreNodeAncestors, { ancestorNodes: relation.ancestorNodes, onSelectNode: selectGenreFromPanel });
2342
+ const genrePlaylist = genrePlaylistByNodeId.get(node.id);
2343
+ const criteria = genrePlaylist?.criteria;
2344
+ if (!genrePlaylist || !criteria) return { primary: family, aside: ancestors };
2345
+ const genreUuid = criteria.uuid;
2346
+ const parentPlaylist = visibleGenrePlaylists.find(({ uuid }) => uuid === genrePlaylist.parent?.uuid);
2347
+ const canToggleSide = !readOnly && parentPlaylist !== void 0 && parentPlaylist.parent === null;
2348
+ const side = criteria.side ?? "core";
2349
+ const shareTarget = {
2350
+ kind: "genre",
2351
+ genrePlaylistUuid: genrePlaylist.uuid,
2352
+ name: criteria.name,
2353
+ scope,
2354
+ artistUuid
2355
+ };
2356
+ const shareUrl = getShareUrlRef.current?.(shareTarget) ?? void 0;
2357
+ const popSibling = canToggleSide && side === "core" ? visibleGenrePlaylists.find(
2358
+ (sibling) => sibling.parent?.uuid === parentPlaylist.uuid && sibling.uuid !== genrePlaylist.uuid && sibling.criteria?.side === "pop"
2359
+ ) : void 0;
2360
+ return {
2361
+ primary: /* @__PURE__ */ jsxs22(Fragment11, { children: [
2362
+ /* @__PURE__ */ jsx29(
2468
2363
  GenreInfoPanelBody,
2469
2364
  {
2470
- toolbar,
2471
2365
  family,
2472
2366
  genreUuid,
2473
2367
  genrePlaylist,
@@ -2483,7 +2377,7 @@ function GenreTreeView({
2483
2377
  onAddChild: readOnly ? void 0 : () => handleGenreCreationAction(criteria),
2484
2378
  onRename: readOnly ? void 0 : () => handleGenreRenameAction(criteria),
2485
2379
  onChangeParent: readOnly ? void 0 : () => showPopup(
2486
- /* @__PURE__ */ jsx30(
2380
+ /* @__PURE__ */ jsx29(
2487
2381
  ChangeParentPopup,
2488
2382
  {
2489
2383
  genrePlaylist: { ...genrePlaylist, criteria },
@@ -2514,49 +2408,65 @@ function GenreTreeView({
2514
2408
  }
2515
2409
  }
2516
2410
  ),
2517
- aside: ancestors
2518
- };
2519
- },
2520
- [
2521
- genrePlaylistByNodeId,
2522
- additionalActions,
2523
- readOnly,
2524
- handleGenreCreationAction,
2525
- handleGenreRenameAction,
2526
- showPopup,
2527
- hidePopup,
2528
- onParentChanged,
2529
- visibleGenrePlaylists,
2530
- updateGenre,
2531
- playback,
2532
- filterArtist,
2533
- scope,
2534
- getBackendBaseUrl,
2535
- criteriaOverviewSchema,
2536
- renderGenreDetailExtras,
2537
- renderArtistLink,
2538
- resolvedRenderSongLink,
2539
- renderSummary,
2540
- renderEssentialTracks,
2541
- renderSongLeading,
2542
- renderSongActions,
2543
- renderTracksToolbar
2544
- ]
2545
- );
2411
+ renderDangerZone?.(node)
2412
+ ] }),
2413
+ aside: ancestors
2414
+ };
2415
+ };
2546
2416
  const isRotationControlled = controlledAllowWheelRotation !== void 0;
2547
2417
  const allowWheelRotation = controlledAllowWheelRotation ?? internalAllowWheelRotation;
2548
- const detailTab = placement === "regions" && (hasSelectedGenreNode || detailOverlay) ? /* @__PURE__ */ jsx30(
2549
- PanelEdgeTab,
2418
+ let activePanel = null;
2419
+ if (overlayPanel) {
2420
+ activePanel = {
2421
+ title: overlayPanel.title,
2422
+ content: overlayPanel.content,
2423
+ aside: overlayPanel.extraContent,
2424
+ expanded: overlayPanel.expanded ?? false,
2425
+ onExpandedChange: overlayPanel.onExpandedChange,
2426
+ onClose: overlayPanel.onClose,
2427
+ compact: false
2428
+ };
2429
+ } else if (relations) {
2430
+ const { primary, aside } = renderGenreBody(relations);
2431
+ activePanel = {
2432
+ title: relations.node.name,
2433
+ content: primary,
2434
+ aside,
2435
+ expanded: isExpanded,
2436
+ onExpandedChange: changeExpanded,
2437
+ onClose: clearSelection,
2438
+ compact: true,
2439
+ headerAccent: { background: relations.fill, color: relations.textColor }
2440
+ };
2441
+ } else if (builtInPanel) {
2442
+ activePanel = {
2443
+ title: builtInPanel.title,
2444
+ content: builtInPanel.content,
2445
+ aside: builtInPanel.extraContent,
2446
+ expanded: builtInPanel.expanded,
2447
+ onExpandedChange: builtInPanel.onExpandedChange,
2448
+ onClose: builtInPanel.onClose,
2449
+ compact: true
2450
+ };
2451
+ }
2452
+ const isPanelShown = isTreeReady && activePanel !== null;
2453
+ const detailTab = placement === "regions" && activePanel?.onExpandedChange ? {
2454
+ edge: activePanel.expanded ? { label: "Shrink panel", onClick: () => activePanel.onExpandedChange?.(false) } : isCollapsed ? null : { label: "Hide panel", onClick: () => changeCollapsed(true) },
2455
+ center: activePanel.expanded ? null : isCollapsed ? { label: "Show panel", onClick: () => changeCollapsed(false) } : activePanel.onExpandedChange ? { label: "Extend panel", onClick: () => activePanel.onExpandedChange?.(true) } : null
2456
+ } : void 0;
2457
+ const expandButton = placement === "default" && activePanel?.onExpandedChange ? /* @__PURE__ */ jsx29(
2458
+ IconButton,
2550
2459
  {
2551
- side: "left",
2552
- edge: detailExpanded ? { label: "Shrink panel", onClick: () => onDetailExpandedChange?.(false) } : isCollapsed ? null : { label: "Hide panel", onClick: () => changeCollapsed(true) },
2553
- center: detailExpanded ? null : isCollapsed ? { label: "Show panel", onClick: () => changeCollapsed(false) } : onDetailExpandedChange ? { label: "Extend panel", onClick: () => onDetailExpandedChange(true) } : null
2460
+ "aria-label": activePanel.expanded ? "Shrink panel" : "Extend panel",
2461
+ style: activePanel.headerAccent && { color: "inherit" },
2462
+ onClick: () => activePanel.onExpandedChange?.(!activePanel.expanded),
2463
+ children: activePanel.expanded ? /* @__PURE__ */ jsx29(Minimize2, { "aria-hidden": "true", className: "h-4 w-4" }) : /* @__PURE__ */ jsx29(Maximize2, { "aria-hidden": "true", className: "h-4 w-4" })
2554
2464
  }
2555
2465
  ) : null;
2556
2466
  const renderArtistFilter = () => {
2557
2467
  if (isLoading || !filterArtist) return null;
2558
- return /* @__PURE__ */ jsxs23("span", { className: "inline-flex items-center overflow-hidden rounded-full bg-[var(--color-neutral-100)] text-sm", children: [
2559
- /* @__PURE__ */ jsx30(
2468
+ return /* @__PURE__ */ jsxs22("span", { className: "inline-flex items-center overflow-hidden rounded-full bg-[var(--color-neutral-100)] text-sm", children: [
2469
+ /* @__PURE__ */ jsx29(
2560
2470
  "button",
2561
2471
  {
2562
2472
  type: "button",
@@ -2568,7 +2478,7 @@ function GenreTreeView({
2568
2478
  children: filterArtist.name
2569
2479
  }
2570
2480
  ),
2571
- /* @__PURE__ */ jsx30(
2481
+ /* @__PURE__ */ jsx29(
2572
2482
  "button",
2573
2483
  {
2574
2484
  type: "button",
@@ -2580,22 +2490,22 @@ function GenreTreeView({
2580
2490
  )
2581
2491
  ] });
2582
2492
  };
2583
- const actions = /* @__PURE__ */ jsxs23(Fragment11, { children: [
2584
- !isLoading && /* @__PURE__ */ jsx30(
2493
+ const actions = /* @__PURE__ */ jsxs22(Fragment11, { children: [
2494
+ !isLoading && /* @__PURE__ */ jsx29(
2585
2495
  GenreSearch,
2586
2496
  {
2587
2497
  genrePlaylists: visibleGenrePlaylists,
2588
2498
  onSelect: handleGenreSearchSelect,
2589
- selectedName: selectedGenrePlaylist?.name ?? builtInPanel?.title ?? null,
2499
+ selectedName: selectedGenrePlaylist?.name ?? (typeof builtInPanel?.title === "string" ? builtInPanel.title : null),
2590
2500
  onClear: builtInSongPanel ? () => changeSong(null) : builtInArtistPanel ? () => changeArtist(null) : handleGenreSearchClear,
2591
2501
  onPrevious: handlePrevious,
2592
2502
  onQueryChange: artistSearch || songSearch ? setSearchText : void 0,
2593
2503
  extraGroups
2594
2504
  }
2595
2505
  ),
2596
- !isLoading && filterArtist && /* @__PURE__ */ jsx30("div", { className: "hidden w-full justify-center max-md:flex", children: renderArtistFilter() }),
2597
- /* @__PURE__ */ jsxs23("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: [
2598
- !isLoading && !isControlled && /* @__PURE__ */ jsx30(
2506
+ !isLoading && filterArtist && /* @__PURE__ */ jsx29("div", { className: "hidden w-full justify-center max-md:flex", children: renderArtistFilter() }),
2507
+ /* @__PURE__ */ jsxs22("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: [
2508
+ !isLoading && !isControlled && /* @__PURE__ */ jsx29(
2599
2509
  SegmentedControl2,
2600
2510
  {
2601
2511
  "aria-label": "Tree view mode",
@@ -2609,13 +2519,13 @@ function GenreTreeView({
2609
2519
  ]
2610
2520
  }
2611
2521
  ),
2612
- !isLoading && showDisplayOptions && /* @__PURE__ */ jsx30(
2522
+ !isLoading && showDisplayOptions && /* @__PURE__ */ jsx29(
2613
2523
  "div",
2614
2524
  {
2615
2525
  className: "flex items-center gap-1",
2616
2526
  role: "group",
2617
2527
  "aria-label": "Tree display options",
2618
- children: !isRotationControlled && viewMode !== "stacked" && viewMode !== "outline" && /* @__PURE__ */ jsx30(
2528
+ children: !isRotationControlled && viewMode !== "stacked" && viewMode !== "outline" && /* @__PURE__ */ jsx29(
2619
2529
  Button,
2620
2530
  {
2621
2531
  variant: allowWheelRotation ? "default" : "outline",
@@ -2626,7 +2536,7 @@ function GenreTreeView({
2626
2536
  )
2627
2537
  }
2628
2538
  ),
2629
- !isLoading && !readOnly && showAddRoot && /* @__PURE__ */ jsx30(
2539
+ !isLoading && !readOnly && showAddRoot && /* @__PURE__ */ jsx29(
2630
2540
  IconTextButton,
2631
2541
  {
2632
2542
  icon: Plus2,
@@ -2636,42 +2546,34 @@ function GenreTreeView({
2636
2546
  )
2637
2547
  ] })
2638
2548
  ] });
2639
- return /* @__PURE__ */ jsxs23(
2549
+ return /* @__PURE__ */ jsxs22(
2640
2550
  "div",
2641
2551
  {
2642
- ref: rootRef,
2552
+ "data-left-panel": isPanelShown && !isCollapsed ? void 0 : "hidden",
2643
2553
  className: ["genre-tree-view relative flex flex-col h-full", placement === "regions" && REGIONS_CLASS, className2].filter(Boolean).join(" "),
2644
2554
  children: [
2645
- /* @__PURE__ */ jsx30("style", { children: PREVIOUS_BUTTON_CSS }),
2646
- /* @__PURE__ */ jsx30(
2555
+ /* @__PURE__ */ jsx29(
2647
2556
  "div",
2648
2557
  {
2649
2558
  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))] max-md:right-[calc(var(--layout-gutter)+var(--layout-panel-inset))] max-md:top-[var(--layout-band-top)] max-md:flex-col max-md:items-stretch" : "left-5 top-5"}`,
2650
2559
  children: actions
2651
2560
  }
2652
2561
  ),
2653
- !isLoading && filterArtist && /* @__PURE__ */ jsx30(
2562
+ !isLoading && filterArtist && /* @__PURE__ */ jsx29(
2654
2563
  "div",
2655
2564
  {
2656
2565
  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"}`,
2657
2566
  children: renderArtistFilter()
2658
2567
  }
2659
2568
  ),
2660
- detailTab && /* @__PURE__ */ jsx30(
2661
- "div",
2662
- {
2663
- className: `absolute top-1/2 z-30 -translate-y-1/2 max-md:hidden 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))]"}`,
2664
- children: detailTab
2665
- }
2666
- ),
2667
- /* @__PURE__ */ jsx30("div", { className: "content-container flex-1 min-h-0 flex flex-row gap-4", children: /* @__PURE__ */ jsxs23("div", { className: "tree-view-container relative flex-1 min-w-0 flex flex-col h-full", children: [
2668
- !isLoading && viewMode === "outline" && floatingControlsLeading && /* @__PURE__ */ jsx30("div", { className: "absolute bottom-3 right-3 z-10", children: floatingControlsLeading }),
2669
- isLoading ? /* @__PURE__ */ jsx30(GenreTreeViewSkeleton, { viewMode }) : viewMode === "wheel" ? (
2569
+ /* @__PURE__ */ jsx29("div", { className: "content-container flex-1 min-h-0 flex flex-row gap-4", children: /* @__PURE__ */ jsxs22("div", { className: "tree-view-container relative flex-1 min-w-0 flex flex-col h-full", children: [
2570
+ !isLoading && viewMode === "outline" && floatingControlsLeading && /* @__PURE__ */ jsx29("div", { className: "absolute bottom-3 right-3 z-10", children: floatingControlsLeading }),
2571
+ isLoading ? /* @__PURE__ */ jsx29(GenreTreeViewSkeleton, { viewMode }) : viewMode === "wheel" ? (
2670
2572
  // The info panel's top edge aligns with the tree's; the search bar floats `--layout-panel-inset` below it,
2671
2573
  // and the header inset (the bar's height plus that inset) extends the header's fill behind the bar,
2672
2574
  // Google Maps style; the 8px bottom padding mirrors the inset above the bar, and the hidden title's height is zeroed.
2673
2575
  // The panel's own Zoom in button is hidden: the search bar carries it.
2674
- /* @__PURE__ */ jsx30("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__ */ jsx30(GenreTreeWheelHandoff, { skeleton: /* @__PURE__ */ jsx30(GenreTreeWheelSkeleton, {}), onRevealedChange: setIsWheelRevealed, children: /* @__PURE__ */ jsx30(
2576
+ /* @__PURE__ */ jsx29("div", { className: "tree-container search-overlay relative flex-1 w-full min-h-0 ", children: /* @__PURE__ */ jsx29(GenreTreeWheelHandoff, { skeleton: /* @__PURE__ */ jsx29(GenreTreeWheelSkeleton, {}), onRevealedChange: setIsWheelRevealed, children: /* @__PURE__ */ jsx29(
2675
2577
  GenrePlaylistTreeWheel,
2676
2578
  {
2677
2579
  scope,
@@ -2680,29 +2582,26 @@ function GenreTreeView({
2680
2582
  handleGenreRenameAction,
2681
2583
  additionalActions,
2682
2584
  onSelectedNodeChange: handleSelectedNodeChange,
2683
- hideInfoPanelClose: true,
2684
- hideInfoPanelTitle: true,
2685
2585
  onNodeHover: handleNodeHover,
2686
- renderInfoPanelBody,
2586
+ ...hostPanel,
2687
2587
  playback,
2688
2588
  selectedNodeId,
2689
2589
  readOnly,
2690
2590
  allowWheelRotation,
2691
2591
  sizeMetric: isArtistFiltered || !canSizeByViews ? "songs" : deferredSizeMetric,
2692
2592
  busy,
2693
- floatingControlsLeading: floatingControlsLeading || canSizeByViews ? /* @__PURE__ */ jsxs23("div", { className: "flex items-center gap-2", children: [
2593
+ floatingControlsLeading: floatingControlsLeading || canSizeByViews ? /* @__PURE__ */ jsxs22("div", { className: "flex items-center gap-2", children: [
2694
2594
  floatingControlsLeading,
2695
- canSizeByViews && /* @__PURE__ */ jsx30(SizeMetricToggle, { value: sizeMetric, onChange: changeSizeMetric })
2595
+ canSizeByViews && /* @__PURE__ */ jsx29(SizeMetricToggle, { value: sizeMetric, onChange: changeSizeMetric })
2696
2596
  ] }) : void 0,
2697
2597
  defaultViewport,
2698
2598
  viewportRestore,
2699
- onViewportChange,
2700
- ...infoPanelCuration
2599
+ onViewportChange
2701
2600
  }
2702
2601
  ) }) })
2703
2602
  ) : viewMode === "pop-core" ? (
2704
2603
  // Same as the wheel: the header inset extends its fill behind the floating search bar, Google Maps style.
2705
- /* @__PURE__ */ jsx30("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__ */ jsx30(GenreTreeWheelHandoff, { skeleton: /* @__PURE__ */ jsx30(GenreTreeWheelSkeleton, {}), onRevealedChange: setIsWheelRevealed, children: /* @__PURE__ */ jsx30(
2604
+ /* @__PURE__ */ jsx29("div", { className: "tree-container search-overlay relative flex-1 w-full min-h-0 ", children: /* @__PURE__ */ jsx29(GenreTreeWheelHandoff, { skeleton: /* @__PURE__ */ jsx29(GenreTreeWheelSkeleton, {}), onRevealedChange: setIsWheelRevealed, children: /* @__PURE__ */ jsx29(
2706
2605
  GenrePlaylistTreeWheelRadialPopCore,
2707
2606
  {
2708
2607
  scope,
@@ -2711,28 +2610,25 @@ function GenreTreeView({
2711
2610
  handleGenreRenameAction,
2712
2611
  additionalActions,
2713
2612
  onSelectedNodeChange: handleSelectedNodeChange,
2714
- hideInfoPanelClose: true,
2715
- hideInfoPanelTitle: true,
2716
2613
  onNodeHover: handleNodeHover,
2717
- renderInfoPanelBody,
2614
+ ...hostPanel,
2718
2615
  playback,
2719
2616
  selectedNodeId,
2720
2617
  readOnly,
2721
2618
  allowWheelRotation,
2722
2619
  sizeMetric: isArtistFiltered || !canSizeByViews ? "songs" : deferredSizeMetric,
2723
2620
  busy,
2724
- floatingControlsLeading: floatingControlsLeading || canSizeByViews ? /* @__PURE__ */ jsxs23("div", { className: "flex items-center gap-2", children: [
2621
+ floatingControlsLeading: floatingControlsLeading || canSizeByViews ? /* @__PURE__ */ jsxs22("div", { className: "flex items-center gap-2", children: [
2725
2622
  floatingControlsLeading,
2726
- canSizeByViews && /* @__PURE__ */ jsx30(SizeMetricToggle, { value: sizeMetric, onChange: changeSizeMetric })
2623
+ canSizeByViews && /* @__PURE__ */ jsx29(SizeMetricToggle, { value: sizeMetric, onChange: changeSizeMetric })
2727
2624
  ] }) : void 0,
2728
2625
  defaultViewport,
2729
2626
  viewportRestore,
2730
2627
  onViewportChange,
2731
- ...infoPanelCuration,
2732
2628
  ...focus
2733
2629
  }
2734
2630
  ) }) })
2735
- ) : viewMode === "outline" ? /* @__PURE__ */ jsx30("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__ */ jsx30(
2631
+ ) : viewMode === "outline" ? /* @__PURE__ */ jsx29("div", { className: "tree-container flex-1 min-h-0 w-full overflow-y-auto relative", children: /* @__PURE__ */ jsx29(
2736
2632
  GenrePlaylistTreeWheelRadialPopCore,
2737
2633
  {
2738
2634
  outline: true,
@@ -2742,22 +2638,19 @@ function GenreTreeView({
2742
2638
  handleGenreRenameAction,
2743
2639
  additionalActions,
2744
2640
  onSelectedNodeChange: handleSelectedNodeChange,
2745
- hideInfoPanelClose: true,
2746
- hideInfoPanelTitle: true,
2747
2641
  onNodeHover: handleNodeHover,
2748
- renderInfoPanelBody,
2642
+ ...hostPanel,
2749
2643
  playback,
2750
2644
  selectedNodeId,
2751
- readOnly,
2752
- ...infoPanelCuration
2645
+ readOnly
2753
2646
  }
2754
- ) }) : /* @__PURE__ */ jsx30("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(
2755
- ([uuid, genrePlaylistTreePerRoot], rootIndex) => {
2756
- return /* @__PURE__ */ jsx30(
2647
+ ) }) : /* @__PURE__ */ jsx29("div", { className: "tree-container relative flex flex-col gap-4 w-full pt-14 overflow-x-auto overflow-y-auto text-[var(--color-text)]", children: Object.entries(groupedGenrePlaylistsByRoot).map(
2648
+ ([uuid, genrePlaylistTreePerRoot]) => {
2649
+ return /* @__PURE__ */ jsx29(
2757
2650
  "div",
2758
2651
  {
2759
2652
  className: "tree-per-root-container relative shrink-0 h-[28rem] mt-2 mr-16 p-2 bg-[var(--color-neutral-100)] rounded-lg",
2760
- children: /* @__PURE__ */ jsx30("div", { className: "graph-container relative z-10 h-full", children: /* @__PURE__ */ jsx30(
2653
+ children: /* @__PURE__ */ jsx29("div", { className: "graph-container relative z-10 h-full", children: /* @__PURE__ */ jsx29(
2761
2654
  GenrePlaylistTreePerRoot,
2762
2655
  {
2763
2656
  scope,
@@ -2767,26 +2660,45 @@ function GenreTreeView({
2767
2660
  handleGenreRenameAction,
2768
2661
  additionalActions,
2769
2662
  onSelectedNodeChange: handleSelectedNodeChange,
2770
- hideInfoPanelClose: true,
2771
- hideInfoPanelTitle: true,
2772
2663
  onNodeHover: handleNodeHover,
2773
- renderInfoPanelBody,
2664
+ ...hostPanel,
2774
2665
  playback,
2775
2666
  selectedNodeId: uuid === selectedGenrePlaylist?.root.uuid ? selectedNodeId : null,
2776
2667
  readOnly,
2777
2668
  wheelZoom: "modifier",
2778
2669
  defaultViewport: uuid === selectedGenrePlaylist?.root.uuid ? defaultViewport : void 0,
2779
2670
  viewportRestore: uuid === selectedGenrePlaylist?.root.uuid && uuid === restoreRootUuid ? viewportRestore : void 0,
2780
- onViewportChange: uuid === selectedGenrePlaylist?.root.uuid ? onViewportChange : void 0,
2781
- ...infoPanelCuration,
2782
- overlayPanel: rootIndex === 0 ? activeOverlayPanel : void 0
2671
+ onViewportChange: uuid === selectedGenrePlaylist?.root.uuid ? onViewportChange : void 0
2783
2672
  }
2784
2673
  ) })
2785
2674
  },
2786
2675
  uuid
2787
2676
  );
2788
2677
  }
2789
- ) })
2678
+ ) }),
2679
+ /* @__PURE__ */ jsx29(
2680
+ DetailPanel,
2681
+ {
2682
+ ref: panelRef,
2683
+ side: "left",
2684
+ title: activePanel?.title ?? "",
2685
+ hideTitle: activePanel?.compact,
2686
+ hideClose: activePanel?.compact,
2687
+ headerAccent: activePanel?.headerAccent,
2688
+ headerActions: expandButton,
2689
+ onClose: activePanel?.onClose ?? noop,
2690
+ collapsed: isCollapsed || !isTreeReady || !activePanel,
2691
+ expanded: activePanel?.expanded,
2692
+ onExpandedChange: activePanel?.onExpandedChange,
2693
+ aside: activePanel?.aside,
2694
+ edgeTab: detailTab,
2695
+ headerClassName: activePanel?.compact ? "pt-[calc(var(--layout-search-h)+var(--layout-panel-inset)+0.5rem)]" : void 0,
2696
+ children: activePanel?.aside && !activePanel.expanded ? /* @__PURE__ */ jsxs22(Fragment11, { children: [
2697
+ activePanel.content,
2698
+ activePanel.aside
2699
+ ] }) : activePanel?.content
2700
+ }
2701
+ )
2790
2702
  ] }) })
2791
2703
  ]
2792
2704
  }
@@ -2797,17 +2709,18 @@ function GenreTreeView({
2797
2709
  import { useMemo as useMemo8 } from "react";
2798
2710
  import {
2799
2711
  ACCENT_TEXT_COLOR,
2712
+ GenreNodeAncestors as GenreNodeAncestors2,
2713
+ GenreNodeFamily as GenreNodeFamily2,
2800
2714
  getGenreTreeColor as getGenreTreeColor2,
2801
- InfoPanel,
2802
2715
  TEXT_COLOR,
2803
2716
  tintSurface
2804
2717
  } from "@behindthemusictree/genre-tree-view";
2805
- import { Skeleton as Skeleton7 } from "@behindthemusictree/ui";
2718
+ import { DetailPanel as DetailPanel2, IconButton as IconButton2, Skeleton as Skeleton7 } from "@behindthemusictree/ui";
2719
+ import { Maximize2 as Maximize22, Minimize2 as Minimize22 } from "lucide-react";
2806
2720
  import {
2807
2721
  useFetchGenreDetail
2808
2722
  } from "@behindthemusictree/app-kit/genre-tree";
2809
- import { jsx as jsx31, jsxs as jsxs24 } from "react/jsx-runtime";
2810
- var ITEM_COUNT_LABEL = "Songs";
2723
+ import { jsx as jsx30, jsxs as jsxs23 } from "react/jsx-runtime";
2811
2724
  var RELATIVE_ITEM_COUNT = 1;
2812
2725
  function toNode(genre, parentId) {
2813
2726
  return {
@@ -2831,6 +2744,7 @@ function GenreDetailPanel({
2831
2744
  collapsed,
2832
2745
  expanded,
2833
2746
  onExpandedChange,
2747
+ edgeTab,
2834
2748
  extras
2835
2749
  }) {
2836
2750
  const { data: detail, isPending, isError } = useFetchGenreDetail(genreUuid, scope, getBackendBaseUrl);
@@ -2843,7 +2757,7 @@ function GenreDetailPanel({
2843
2757
  fill: tintSurface(rootColor),
2844
2758
  textColor: TEXT_COLOR
2845
2759
  });
2846
- const ancestors = [...detail.ascendants].sort((a, b) => b.degree - a.degree);
2760
+ const ancestors2 = [...detail.ascendants].sort((a, b) => b.degree - a.degree);
2847
2761
  return {
2848
2762
  node: {
2849
2763
  id: detail.uuid,
@@ -2855,57 +2769,72 @@ function GenreDetailPanel({
2855
2769
  fill: rootColor,
2856
2770
  parentNode: detail.parent ? relative(detail.parent, null) : null,
2857
2771
  childNodes: detail.children.map((child) => relative(child, detail.uuid)),
2858
- ancestorNodes: ancestors.map(
2859
- ({ ascendant }, index) => relative(ascendant, index === 0 ? null : ancestors[index - 1].ascendant.uuid)
2772
+ ancestorNodes: ancestors2.map(
2773
+ ({ ascendant }, index) => relative(ascendant, index === 0 ? null : ancestors2[index - 1].ascendant.uuid)
2860
2774
  )
2861
2775
  };
2862
2776
  }, [detail]);
2863
2777
  if (isError) {
2864
- return /* @__PURE__ */ jsx31("div", { role: "alert", className: "p-4 text-sm text-[var(--color-text)]", children: "This genre could not be loaded." });
2778
+ return /* @__PURE__ */ jsx30("div", { role: "alert", className: "p-4 text-sm text-[var(--color-text)]", children: "This genre could not be loaded." });
2865
2779
  }
2866
2780
  if (isPending || !detail || !view) {
2867
- return /* @__PURE__ */ jsxs24("div", { className: "flex flex-col gap-3 p-4", "aria-busy": "true", "data-testid": "genre-detail-panel-skeleton", children: [
2868
- /* @__PURE__ */ jsx31(Skeleton7, { className: "h-6 w-2/3" }),
2869
- /* @__PURE__ */ jsx31(Skeleton7, { className: "h-4 w-full" }),
2870
- /* @__PURE__ */ jsx31(Skeleton7, { className: "h-4 w-3/4" })
2781
+ return /* @__PURE__ */ jsxs23("div", { className: "flex flex-col gap-3 p-4", "aria-busy": "true", "data-testid": "genre-detail-panel-skeleton", children: [
2782
+ /* @__PURE__ */ jsx30(Skeleton7, { className: "h-6 w-2/3" }),
2783
+ /* @__PURE__ */ jsx30(Skeleton7, { className: "h-4 w-full" }),
2784
+ /* @__PURE__ */ jsx30(Skeleton7, { className: "h-4 w-3/4" })
2871
2785
  ] });
2872
2786
  }
2873
- const renderBody = (node, sections) => ({
2874
- primary: /* @__PURE__ */ jsx31(
2875
- GenreInfoPanelBody,
2876
- {
2877
- toolbar: sections.toolbar,
2878
- family: sections.family,
2879
- genreUuid: detail.uuid,
2880
- genrePlaylist: { ...detail.criteriaPlaylist, tracksCount: detail.tracksCount },
2881
- wikidataId: detail.wikidataId,
2882
- scope,
2883
- getBackendBaseUrl,
2884
- playback,
2885
- band: { node, onZoomIn, shareUrl, shareTarget: shareUrl ? shareTarget : void 0 },
2886
- extras
2887
- }
2888
- ),
2889
- aside: null
2890
- });
2891
- return /* @__PURE__ */ jsx31(
2892
- InfoPanel,
2787
+ const selectGenre = (nodeId) => onSelectGenre(nodeId);
2788
+ const family = /* @__PURE__ */ jsx30(
2789
+ GenreNodeFamily2,
2893
2790
  {
2894
2791
  node: view.node,
2895
2792
  fill: view.fill,
2896
2793
  textColor: ACCENT_TEXT_COLOR,
2897
2794
  parentNode: view.parentNode,
2898
2795
  childNodes: view.childNodes,
2899
- ancestorNodes: view.ancestorNodes,
2796
+ onSelectNode: selectGenre
2797
+ }
2798
+ );
2799
+ const ancestors = /* @__PURE__ */ jsx30(GenreNodeAncestors2, { ancestorNodes: view.ancestorNodes, onSelectNode: selectGenre });
2800
+ return /* @__PURE__ */ jsxs23(
2801
+ DetailPanel2,
2802
+ {
2900
2803
  side: "left",
2804
+ title: view.node.name,
2805
+ headerAccent: { background: view.fill, color: ACCENT_TEXT_COLOR },
2806
+ headerActions: onExpandedChange ? /* @__PURE__ */ jsx30(
2807
+ IconButton2,
2808
+ {
2809
+ "aria-label": expanded ? "Shrink panel" : "Extend panel",
2810
+ style: { color: "inherit" },
2811
+ onClick: () => onExpandedChange(!expanded),
2812
+ children: expanded ? /* @__PURE__ */ jsx30(Minimize22, { "aria-hidden": "true", className: "h-4 w-4" }) : /* @__PURE__ */ jsx30(Maximize22, { "aria-hidden": "true", className: "h-4 w-4" })
2813
+ }
2814
+ ) : void 0,
2901
2815
  onClose,
2902
- onSelectNode: (nodeId) => onSelectGenre(nodeId),
2903
- itemCountLabel: ITEM_COUNT_LABEL,
2904
- renderInfoPanelBody: renderBody,
2816
+ collapsed,
2905
2817
  expanded,
2906
2818
  onExpandedChange,
2907
- collapsed,
2908
- hideExpandButton: onExpandedChange === void 0
2819
+ edgeTab,
2820
+ aside: ancestors,
2821
+ children: [
2822
+ /* @__PURE__ */ jsx30(
2823
+ GenreInfoPanelBody,
2824
+ {
2825
+ family,
2826
+ genreUuid: detail.uuid,
2827
+ genrePlaylist: { ...detail.criteriaPlaylist, tracksCount: detail.tracksCount },
2828
+ wikidataId: detail.wikidataId,
2829
+ scope,
2830
+ getBackendBaseUrl,
2831
+ playback,
2832
+ band: { node: view.node, onZoomIn, shareUrl, shareTarget: shareUrl ? shareTarget : void 0 },
2833
+ extras
2834
+ }
2835
+ ),
2836
+ !expanded && ancestors
2837
+ ]
2909
2838
  }
2910
2839
  );
2911
2840
  }
@@ -2919,13 +2848,13 @@ import {
2919
2848
  } from "@behindthemusictree/genre-tree-view";
2920
2849
 
2921
2850
  // src/genre-tree/track-list-sidebar/TrackListSidebar.tsx
2922
- import { useEffect as useEffect7, useLayoutEffect as useLayoutEffect2, useRef as useRef5 } from "react";
2851
+ import { useEffect as useEffect6, useLayoutEffect as useLayoutEffect2, useRef as useRef4 } from "react";
2923
2852
  import { ListMusic } from "lucide-react";
2924
2853
  import { useTrackList as useTrackList6 } from "@behindthemusictree/app-kit/genre-tree";
2925
2854
  import { TrackListOriginType as TrackListOriginType2 } from "@behindthemusictree/app-kit/genre-tree";
2926
- import { useTrackNoun as useTrackNoun6 } from "@behindthemusictree/app-kit/track-noun";
2927
- import { jsx as jsx32, jsxs as jsxs25 } from "react/jsx-runtime";
2928
- var useBrowserLayoutEffect = typeof window === "undefined" ? useEffect7 : useLayoutEffect2;
2855
+ import { useTrackNoun as useTrackNoun3 } from "@behindthemusictree/app-kit/track-noun";
2856
+ import { jsx as jsx31, jsxs as jsxs24 } from "react/jsx-runtime";
2857
+ var useBrowserLayoutEffect = typeof window === "undefined" ? useEffect6 : useLayoutEffect2;
2929
2858
  function TrackListSidebar({
2930
2859
  className: className2 = "",
2931
2860
  renderDuration,
@@ -2938,17 +2867,17 @@ function TrackListSidebar({
2938
2867
  renderSongLink
2939
2868
  }) {
2940
2869
  const { trackList, loadMore, autoAdvanceScrollRequest } = useTrackList6();
2941
- const trackNoun = useTrackNoun6();
2942
- const trackListRef = useRef5(null);
2943
- const previousOriginRef = useRef5(trackList?.origin ?? null);
2944
- const handledAutoAdvanceRequestIdRef = useRef5(null);
2945
- const shouldFollowPlaybackRef = useRef5(true);
2946
- const programmaticScrollTopRef = useRef5(null);
2947
- const userScrollIntentRef = useRef5(false);
2948
- const userScrollIntentTimerRef = useRef5(null);
2949
- const loadMoreSentinelRef = useRef5(null);
2870
+ const trackNoun = useTrackNoun3();
2871
+ const trackListRef = useRef4(null);
2872
+ const previousOriginRef = useRef4(trackList?.origin ?? null);
2873
+ const handledAutoAdvanceRequestIdRef = useRef4(null);
2874
+ const shouldFollowPlaybackRef = useRef4(true);
2875
+ const programmaticScrollTopRef = useRef4(null);
2876
+ const userScrollIntentRef = useRef4(false);
2877
+ const userScrollIntentTimerRef = useRef4(null);
2878
+ const loadMoreSentinelRef = useRef4(null);
2950
2879
  const hasMore = !!trackList && trackList.nextPage !== null;
2951
- useEffect7(() => {
2880
+ useEffect6(() => {
2952
2881
  const sentinel = loadMoreSentinelRef.current;
2953
2882
  if (!hasMore || !sentinel) return;
2954
2883
  const observer = new IntersectionObserver((entries) => {
@@ -2968,11 +2897,11 @@ function TrackListSidebar({
2968
2897
  programmaticScrollTopRef.current = null;
2969
2898
  if (trackListRef.current) trackListRef.current.scrollTop = 0;
2970
2899
  }, [trackList?.origin]);
2971
- useEffect7(() => {
2900
+ useEffect6(() => {
2972
2901
  const list = trackListRef.current;
2973
2902
  if (!isCollapsed && list?.scrollTop === 0) shouldFollowPlaybackRef.current = true;
2974
2903
  }, [isCollapsed]);
2975
- useEffect7(
2904
+ useEffect6(
2976
2905
  () => () => {
2977
2906
  if (userScrollIntentTimerRef.current !== null) clearTimeout(userScrollIntentTimerRef.current);
2978
2907
  },
@@ -3026,25 +2955,25 @@ function TrackListSidebar({
3026
2955
  }
3027
2956
  };
3028
2957
  const positionClasses = layout === "inline" ? "relative w-full flex h-full flex-col" : "fixed right-0 w-[50rem] flex flex-col";
3029
- return trackList ? /* @__PURE__ */ jsxs25(
2958
+ return trackList ? /* @__PURE__ */ jsxs24(
3030
2959
  "div",
3031
2960
  {
3032
2961
  "data-theme": "dark",
3033
2962
  className: `track-list-sidebar ${positionClasses} rounded-2xl bg-[var(--color-surface)] pb-1 ${className2}`,
3034
2963
  style: layout === "fixed" ? { top: "63.5px", maxHeight: "calc(100vh - 63.5px - 79px)" } : void 0,
3035
2964
  children: [
3036
- /* @__PURE__ */ jsxs25("div", { className: "header flex h-16 items-center px-4 py-2 text-[var(--color-text)]", children: [
3037
- /* @__PURE__ */ jsxs25("div", { className: "min-w-0 flex flex-1 flex-col justify-center", children: [
3038
- /* @__PURE__ */ jsxs25("div", { className: "origin flex text-xl", children: [
3039
- /* @__PURE__ */ jsx32("div", { className: "from h-auto flex flex-col justify-center items-center mr-2", children: "From" }),
3040
- /* @__PURE__ */ jsx32("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__ */ jsx32("div", { className: "name max-w-full truncate", children: trackList.origin.label }) })
2965
+ /* @__PURE__ */ jsxs24("div", { className: "header flex h-16 items-center px-4 py-2 text-[var(--color-text)]", children: [
2966
+ /* @__PURE__ */ jsxs24("div", { className: "min-w-0 flex flex-1 flex-col justify-center", children: [
2967
+ /* @__PURE__ */ jsxs24("div", { className: "origin flex text-xl", children: [
2968
+ /* @__PURE__ */ jsx31("div", { className: "from h-auto flex flex-col justify-center items-center mr-2", children: "From" }),
2969
+ /* @__PURE__ */ jsx31("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__ */ jsx31("div", { className: "name max-w-full truncate", children: trackList.origin.label }) })
3041
2970
  ] }),
3042
- /* @__PURE__ */ jsxs25("div", { className: "info flex items-center text-sm text-[var(--color-text-muted)]", children: [
2971
+ /* @__PURE__ */ jsxs24("div", { className: "info flex items-center text-sm text-[var(--color-text-muted)]", children: [
3043
2972
  trackList.origin.type === TrackListOriginType2.GENRE_PLAYLIST ? "Genre \u2022 " : trackList.origin.type === TrackListOriginType2.ARTIST ? "Artist \u2022 " : `${trackNoun.singular} playlist \u2022 `,
3044
2973
  `${trackList.total} ${trackList.total > 1 ? trackNoun.plural : trackNoun.singular}`
3045
2974
  ] })
3046
2975
  ] }),
3047
- (isCollapsed && onExpand || !isCollapsed && onMinimize) && /* @__PURE__ */ jsx32(
2976
+ (isCollapsed && onExpand || !isCollapsed && onMinimize) && /* @__PURE__ */ jsx31(
3048
2977
  "button",
3049
2978
  {
3050
2979
  type: "button",
@@ -3053,11 +2982,11 @@ function TrackListSidebar({
3053
2982
  "aria-expanded": !isCollapsed,
3054
2983
  className: `ml-2 flex h-8 w-8 shrink-0 items-center justify-center rounded-md hover:bg-[var(--color-surface-muted)] ${isCollapsed ? "text-[var(--color-text-muted)]" : "text-white"}`,
3055
2984
  onClick: isCollapsed ? onExpand : onMinimize,
3056
- children: /* @__PURE__ */ jsx32(ListMusic, { "aria-hidden": "true", className: "h-5 w-5" })
2985
+ children: /* @__PURE__ */ jsx31(ListMusic, { "aria-hidden": "true", className: "h-5 w-5" })
3057
2986
  }
3058
2987
  )
3059
2988
  ] }),
3060
- !isCollapsed && /* @__PURE__ */ jsxs25(
2989
+ !isCollapsed && /* @__PURE__ */ jsxs24(
3061
2990
  "ul",
3062
2991
  {
3063
2992
  ref: trackListRef,
@@ -3067,7 +2996,7 @@ function TrackListSidebar({
3067
2996
  onPointerDown: markUserScrollIntent,
3068
2997
  className: layout === "inline" ? "track-list flex-1 min-h-0 overflow-auto list-none p-0 m-0" : "track-list flex-1 min-h-0 overflow-auto list-none p-0 m-0",
3069
2998
  children: [
3070
- trackList.tracks.map((track, index) => /* @__PURE__ */ jsx32("li", { "data-track-uuid": track.uuid, children: /* @__PURE__ */ jsx32(
2999
+ trackList.tracks.map((track, index) => /* @__PURE__ */ jsx31("li", { "data-track-uuid": track.uuid, children: /* @__PURE__ */ jsx31(
3071
3000
  TrackItem,
3072
3001
  {
3073
3002
  track,
@@ -3078,7 +3007,7 @@ function TrackListSidebar({
3078
3007
  renderSongLink
3079
3008
  }
3080
3009
  ) }, track.uuid)),
3081
- hasMore && /* @__PURE__ */ jsx32("li", { ref: loadMoreSentinelRef, "aria-hidden": "true", className: "h-px" })
3010
+ hasMore && /* @__PURE__ */ jsx31("li", { ref: loadMoreSentinelRef, "aria-hidden": "true", className: "h-px" })
3082
3011
  ]
3083
3012
  }
3084
3013
  )
@@ -3088,8 +3017,8 @@ function TrackListSidebar({
3088
3017
  }
3089
3018
 
3090
3019
  // src/genre-tree/PlayerPanel.tsx
3091
- import { useEffect as useEffect8, useState as useState8 } from "react";
3092
- import { twMerge as twMerge2 } from "tailwind-merge";
3020
+ import { useEffect as useEffect7, useState as useState9 } from "react";
3021
+ import { twMerge } from "tailwind-merge";
3093
3022
  import {
3094
3023
  usePlayer as usePlayer11,
3095
3024
  PlayerVideoSurface
@@ -3097,12 +3026,13 @@ import {
3097
3026
  import {
3098
3027
  useTrackListSidebarVisibility
3099
3028
  } from "@behindthemusictree/app-kit/genre-tree";
3029
+ import { PanelEdgeTab } from "@behindthemusictree/ui";
3100
3030
 
3101
3031
  // src/genre-tree/PlayerControls.tsx
3102
3032
  import { Pause as Pause3, Play as Play4, SkipBack, SkipForward, Volume2, VolumeX } from "lucide-react";
3103
3033
  import { formatTime, useTrackList as useTrackList7 } from "@behindthemusictree/app-kit/genre-tree";
3104
3034
  import { PlayStates as PlayStates4, useCurrentTime, usePlayer as usePlayer10 } from "@behindthemusictree/app-kit/player";
3105
- import { jsx as jsx33, jsxs as jsxs26 } from "react/jsx-runtime";
3035
+ import { jsx as jsx32, jsxs as jsxs25 } from "react/jsx-runtime";
3106
3036
  function PlayerControls() {
3107
3037
  const {
3108
3038
  duration,
@@ -3121,15 +3051,15 @@ function PlayerControls() {
3121
3051
  const safeDuration = Number.isFinite(duration) && duration > 0 ? duration : 0;
3122
3052
  const safeCurrentTime = Number.isFinite(currentTime) ? Math.max(0, Math.min(currentTime, safeDuration)) : 0;
3123
3053
  const isPlaying = playState === PlayStates4.PLAYING;
3124
- return /* @__PURE__ */ jsxs26(
3054
+ return /* @__PURE__ */ jsxs25(
3125
3055
  "section",
3126
3056
  {
3127
3057
  "aria-label": "Player controls",
3128
3058
  className: "player-controls flex shrink-0 flex-col gap-2 border-b border-zinc-800 px-3 py-2 text-zinc-200",
3129
3059
  children: [
3130
- /* @__PURE__ */ jsxs26("div", { className: "flex min-w-0 items-center gap-2 text-xs tabular-nums text-zinc-400", children: [
3131
- /* @__PURE__ */ jsx33("span", { "aria-label": "Elapsed time", children: formatTime(safeCurrentTime) }),
3132
- /* @__PURE__ */ jsx33(
3060
+ /* @__PURE__ */ jsxs25("div", { className: "flex min-w-0 items-center gap-2 text-xs tabular-nums text-zinc-400", children: [
3061
+ /* @__PURE__ */ jsx32("span", { "aria-label": "Elapsed time", children: formatTime(safeCurrentTime) }),
3062
+ /* @__PURE__ */ jsx32(
3133
3063
  "input",
3134
3064
  {
3135
3065
  "aria-label": "Seek",
@@ -3143,11 +3073,11 @@ function PlayerControls() {
3143
3073
  onChange: (event) => seekTo(Number(event.currentTarget.value))
3144
3074
  }
3145
3075
  ),
3146
- /* @__PURE__ */ jsx33("span", { "aria-label": "Duration", children: formatTime(safeDuration) })
3076
+ /* @__PURE__ */ jsx32("span", { "aria-label": "Duration", children: formatTime(safeDuration) })
3147
3077
  ] }),
3148
- /* @__PURE__ */ jsxs26("div", { className: "grid min-h-9 grid-cols-[1fr_auto_1fr] items-center gap-3", children: [
3149
- /* @__PURE__ */ jsxs26("div", { className: "col-start-2 flex items-center justify-center gap-2", children: [
3150
- /* @__PURE__ */ jsx33(
3078
+ /* @__PURE__ */ jsxs25("div", { className: "grid min-h-9 grid-cols-[1fr_auto_1fr] items-center gap-3", children: [
3079
+ /* @__PURE__ */ jsxs25("div", { className: "col-start-2 flex items-center justify-center gap-2", children: [
3080
+ /* @__PURE__ */ jsx32(
3151
3081
  "button",
3152
3082
  {
3153
3083
  type: "button",
@@ -3156,10 +3086,10 @@ function PlayerControls() {
3156
3086
  disabled: !isReady || !hasPreviousTrack,
3157
3087
  onClick: playPreviousTrack,
3158
3088
  className: "flex h-8 w-8 shrink-0 items-center justify-center rounded-full text-zinc-300 hover:bg-zinc-800 hover:text-white disabled:cursor-not-allowed disabled:opacity-40",
3159
- children: /* @__PURE__ */ jsx33(SkipBack, { "aria-hidden": "true", className: "h-4 w-4" })
3089
+ children: /* @__PURE__ */ jsx32(SkipBack, { "aria-hidden": "true", className: "h-4 w-4" })
3160
3090
  }
3161
3091
  ),
3162
- /* @__PURE__ */ jsx33(
3092
+ /* @__PURE__ */ jsx32(
3163
3093
  "button",
3164
3094
  {
3165
3095
  type: "button",
@@ -3168,10 +3098,10 @@ function PlayerControls() {
3168
3098
  disabled: !isReady,
3169
3099
  onClick: handlePlayPauseAction,
3170
3100
  className: "flex h-9 w-9 shrink-0 items-center justify-center rounded-full bg-white text-zinc-950 hover:bg-zinc-200 disabled:cursor-not-allowed disabled:opacity-40",
3171
- children: isPlaying ? /* @__PURE__ */ jsx33(Pause3, { "aria-hidden": "true", className: "h-4 w-4", fill: "currentColor" }) : /* @__PURE__ */ jsx33(Play4, { "aria-hidden": "true", className: "h-4 w-4", fill: "currentColor" })
3101
+ children: isPlaying ? /* @__PURE__ */ jsx32(Pause3, { "aria-hidden": "true", className: "h-4 w-4", fill: "currentColor" }) : /* @__PURE__ */ jsx32(Play4, { "aria-hidden": "true", className: "h-4 w-4", fill: "currentColor" })
3172
3102
  }
3173
3103
  ),
3174
- /* @__PURE__ */ jsx33(
3104
+ /* @__PURE__ */ jsx32(
3175
3105
  "button",
3176
3106
  {
3177
3107
  type: "button",
@@ -3180,14 +3110,14 @@ function PlayerControls() {
3180
3110
  disabled: !isReady || !hasNextTrack,
3181
3111
  onClick: playNextTrack,
3182
3112
  className: "flex h-8 w-8 shrink-0 items-center justify-center rounded-full text-zinc-300 hover:bg-zinc-800 hover:text-white disabled:cursor-not-allowed disabled:opacity-40",
3183
- children: /* @__PURE__ */ jsx33(SkipForward, { "aria-hidden": "true", className: "h-4 w-4" })
3113
+ children: /* @__PURE__ */ jsx32(SkipForward, { "aria-hidden": "true", className: "h-4 w-4" })
3184
3114
  }
3185
3115
  )
3186
3116
  ] }),
3187
- /* @__PURE__ */ jsxs26("label", { className: "col-start-3 flex w-[4.2rem] shrink-0 items-center gap-[0.35rem] justify-self-end text-zinc-400 sm:w-[4.9rem]", children: [
3188
- volume === 0 ? /* @__PURE__ */ jsx33(VolumeX, { "aria-hidden": "true", className: "h-[0.7rem] w-[0.7rem] shrink-0" }) : /* @__PURE__ */ jsx33(Volume2, { "aria-hidden": "true", className: "h-[0.7rem] w-[0.7rem] shrink-0" }),
3189
- /* @__PURE__ */ jsx33("span", { className: "sr-only", children: "Volume" }),
3190
- /* @__PURE__ */ jsx33(
3117
+ /* @__PURE__ */ jsxs25("label", { className: "col-start-3 flex w-[4.2rem] shrink-0 items-center gap-[0.35rem] justify-self-end text-zinc-400 sm:w-[4.9rem]", children: [
3118
+ volume === 0 ? /* @__PURE__ */ jsx32(VolumeX, { "aria-hidden": "true", className: "h-[0.7rem] w-[0.7rem] shrink-0" }) : /* @__PURE__ */ jsx32(Volume2, { "aria-hidden": "true", className: "h-[0.7rem] w-[0.7rem] shrink-0" }),
3119
+ /* @__PURE__ */ jsx32("span", { className: "sr-only", children: "Volume" }),
3120
+ /* @__PURE__ */ jsx32(
3191
3121
  "input",
3192
3122
  {
3193
3123
  "aria-label": "Volume",
@@ -3209,7 +3139,7 @@ function PlayerControls() {
3209
3139
  }
3210
3140
 
3211
3141
  // src/genre-tree/PlayerPanel.tsx
3212
- import { jsx as jsx34, jsxs as jsxs27 } from "react/jsx-runtime";
3142
+ import { jsx as jsx33, jsxs as jsxs26 } from "react/jsx-runtime";
3213
3143
  var PANEL_ID = "player-panel";
3214
3144
  var DEFAULT_EXTENDED_WIDTH = "calc(100vw - var(--layout-center-left) - var(--layout-gutter))";
3215
3145
  function PlayerPanel({
@@ -3223,12 +3153,12 @@ function PlayerPanel({
3223
3153
  }) {
3224
3154
  const { playerTrackObject } = usePlayer11();
3225
3155
  const { toggleTrackListSidebar, isTrackListSidebarVisible } = useTrackListSidebarVisibility();
3226
- const [lastLoadedTrackTitle, setLastLoadedTrackTitle] = useState8("");
3156
+ const [lastLoadedTrackTitle, setLastLoadedTrackTitle] = useState9("");
3227
3157
  const loadedTrackTitle = playerTrackObject?.track.title;
3228
- useEffect8(() => {
3158
+ useEffect7(() => {
3229
3159
  if (loadedTrackTitle) setLastLoadedTrackTitle(loadedTrackTitle);
3230
3160
  }, [loadedTrackTitle]);
3231
- const [uncontrolledExtended, setUncontrolledExtended] = useState8(false);
3161
+ const [uncontrolledExtended, setUncontrolledExtended] = useState9(false);
3232
3162
  const isExtendRequested = expanded ?? uncontrolledExtended;
3233
3163
  const setIsExtendRequested = (next) => {
3234
3164
  setUncontrolledExtended(next);
@@ -3249,11 +3179,11 @@ function PlayerPanel({
3249
3179
  onClick: () => setIsExtendRequested(true),
3250
3180
  expanded: false
3251
3181
  } : null;
3252
- return /* @__PURE__ */ jsxs27(
3182
+ return /* @__PURE__ */ jsxs26(
3253
3183
  "div",
3254
3184
  {
3255
3185
  id: PANEL_ID,
3256
- className: twMerge2(
3186
+ className: twMerge(
3257
3187
  "fixed right-[var(--layout-gutter)] top-[var(--layout-band-top)] z-40 max-h-[calc(100vh-var(--layout-band-top)-3.5rem)]",
3258
3188
  !isExtended && "w-[var(--layout-right-w)]",
3259
3189
  isTrackListSidebarVisible && "h-[calc(100vh-var(--layout-band-top)-3.5rem)]",
@@ -3262,7 +3192,7 @@ function PlayerPanel({
3262
3192
  ),
3263
3193
  style: isExtended ? { width: extendedWidth } : void 0,
3264
3194
  children: [
3265
- /* @__PURE__ */ jsx34(
3195
+ /* @__PURE__ */ jsx33(
3266
3196
  PanelEdgeTab,
3267
3197
  {
3268
3198
  side: "right",
@@ -3272,30 +3202,30 @@ function PlayerPanel({
3272
3202
  className: "absolute right-full top-1/2 -translate-y-1/2"
3273
3203
  }
3274
3204
  ),
3275
- /* @__PURE__ */ jsxs27(
3205
+ /* @__PURE__ */ jsxs26(
3276
3206
  "div",
3277
3207
  {
3278
- className: twMerge2(
3208
+ className: twMerge(
3279
3209
  "flex h-full max-h-[inherit] overflow-hidden rounded-[var(--radius-panel,1rem)] bg-zinc-950 text-zinc-100 shadow-[0_1px_2px_rgba(0,0,0,0.5),0_2px_6px_2px_rgba(0,0,0,0.3)]",
3280
3210
  isExtended ? "flex-row" : "flex-col"
3281
3211
  ),
3282
3212
  children: [
3283
- /* @__PURE__ */ jsxs27("div", { className: "flex w-[var(--layout-right-w)] shrink-0 flex-col", children: [
3284
- /* @__PURE__ */ jsx34("div", { className: "flex shrink-0 items-center justify-between gap-2 border-b border-zinc-800 p-2", children: /* @__PURE__ */ jsx34("span", { className: "min-w-0 truncate text-sm font-semibold", children: loadedTrackTitle || lastLoadedTrackTitle }) }),
3285
- /* @__PURE__ */ jsxs27("div", { className: "relative w-full shrink-0", children: [
3286
- /* @__PURE__ */ jsx34(PlayerVideoSurface, { className: "aspect-video w-full bg-black" }),
3287
- playerTrackObject?.loadError && /* @__PURE__ */ jsx34("span", { className: "absolute bottom-2 left-2 right-2 text-xs text-red-400", children: playerTrackObject.loadError })
3213
+ /* @__PURE__ */ jsxs26("div", { className: "flex w-[var(--layout-right-w)] shrink-0 flex-col", children: [
3214
+ /* @__PURE__ */ jsx33("div", { className: "flex shrink-0 items-center justify-between gap-2 border-b border-zinc-800 p-2", children: /* @__PURE__ */ jsx33("span", { className: "min-w-0 truncate text-sm font-semibold", children: loadedTrackTitle || lastLoadedTrackTitle }) }),
3215
+ /* @__PURE__ */ jsxs26("div", { className: "relative w-full shrink-0", children: [
3216
+ /* @__PURE__ */ jsx33(PlayerVideoSurface, { className: "aspect-video w-full bg-black" }),
3217
+ playerTrackObject?.loadError && /* @__PURE__ */ jsx33("span", { className: "absolute bottom-2 left-2 right-2 text-xs text-red-400", children: playerTrackObject.loadError })
3288
3218
  ] }),
3289
- /* @__PURE__ */ jsx34(PlayerControls, {})
3219
+ /* @__PURE__ */ jsx33(PlayerControls, {})
3290
3220
  ] }),
3291
- /* @__PURE__ */ jsx34(
3221
+ /* @__PURE__ */ jsx33(
3292
3222
  TrackListSidebar,
3293
3223
  {
3294
3224
  layout: "inline",
3295
3225
  isCollapsed: !isTrackListSidebarVisible,
3296
3226
  onExpand: toggleTrackListSidebar,
3297
3227
  onMinimize: hideSongList,
3298
- className: twMerge2(
3228
+ className: twMerge(
3299
3229
  "min-h-0 min-w-0 flex-1",
3300
3230
  isExtended && "border-l border-zinc-800"
3301
3231
  ),
@@ -3315,11 +3245,11 @@ function PlayerPanel({
3315
3245
  import { v4 as uuidv4 } from "uuid";
3316
3246
  import { AiFillStar, AiOutlineStar } from "react-icons/ai";
3317
3247
  import { FORM_RATING_NULL_VALUE } from "@behindthemusictree/app-kit/genre-tree";
3318
- import { jsx as jsx35, jsxs as jsxs28 } from "react/jsx-runtime";
3248
+ import { jsx as jsx34, jsxs as jsxs27 } from "react/jsx-runtime";
3319
3249
  function Rating({ rating, handleChange }) {
3320
3250
  const instanceId = uuidv4();
3321
- return /* @__PURE__ */ jsx35("div", { className: "flex items-center", children: [...Array(6)].map((_, i) => /* @__PURE__ */ jsxs28("div", { className: `relative ${i === 0 ? "mr-4" : "mr-1"}`, children: [
3322
- /* @__PURE__ */ jsx35(
3251
+ return /* @__PURE__ */ jsx34("div", { className: "flex items-center", children: [...Array(6)].map((_, i) => /* @__PURE__ */ jsxs27("div", { className: `relative ${i === 0 ? "mr-4" : "mr-1"}`, children: [
3252
+ /* @__PURE__ */ jsx34(
3323
3253
  "input",
3324
3254
  {
3325
3255
  id: `star-${instanceId}-${i}`,
@@ -3331,16 +3261,16 @@ function Rating({ rating, handleChange }) {
3331
3261
  onChange: handleChange
3332
3262
  }
3333
3263
  ),
3334
- /* @__PURE__ */ jsx35("label", { htmlFor: `star-${instanceId}-${i}`, className: "cursor-pointer", children: i === 0 ? /* @__PURE__ */ jsxs28("div", { className: "flex", children: [
3335
- /* @__PURE__ */ jsx35(
3264
+ /* @__PURE__ */ jsx34("label", { htmlFor: `star-${instanceId}-${i}`, className: "cursor-pointer", children: i === 0 ? /* @__PURE__ */ jsxs27("div", { className: "flex", children: [
3265
+ /* @__PURE__ */ jsx34(
3336
3266
  AiOutlineStar,
3337
3267
  {
3338
3268
  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)]"}`
3339
3269
  }
3340
3270
  ),
3341
- /* @__PURE__ */ jsx35("span", { className: "flex-grow w-8" }),
3271
+ /* @__PURE__ */ jsx34("span", { className: "flex-grow w-8" }),
3342
3272
  " "
3343
- ] }) : /* @__PURE__ */ jsx35(
3273
+ ] }) : /* @__PURE__ */ jsx34(
3344
3274
  AiFillStar,
3345
3275
  {
3346
3276
  className: `w-7 h-7 absolute top-0 left-0 ${(rating ?? -1) >= i ? "text-[var(--color-accent-rating)]" : "text-[var(--color-text-muted)]"}`
@@ -3355,8 +3285,6 @@ export {
3355
3285
  GenreSearch,
3356
3286
  GenreNodeActionBand,
3357
3287
  GenreInfoPanelBody,
3358
- PanelEdgeTab,
3359
- useLeftDetailPanelLayout,
3360
3288
  TrackPositionPlayPause,
3361
3289
  TrackItem,
3362
3290
  GenreTreeView,
@@ -3369,4 +3297,4 @@ export {
3369
3297
  GenreTreeViewSkeleton2 as GenreTreeViewSkeleton,
3370
3298
  GenreTreeOutlineSkeleton
3371
3299
  };
3372
- //# sourceMappingURL=chunk-2HE2HDFA.js.map
3300
+ //# sourceMappingURL=chunk-QGGVPIXY.js.map