@behindthemusictree/app-kit-ui 30.0.0 → 31.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 useRef4, useState as useState7 } 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)]"
@@ -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({
@@ -1106,7 +1070,7 @@ function GenrePlaylistTracks({
1106
1070
  };
1107
1071
  return /* @__PURE__ */ jsxs9("div", { className: "gtv-info-panel-children", children: [
1108
1072
  /* @__PURE__ */ jsxs9("span", { className: "gtv-info-panel-children-title", children: [
1109
- capitalize4(trackNoun.plural),
1073
+ capitalize(trackNoun.plural),
1110
1074
  total !== void 0 && ` (${total})`
1111
1075
  ] }),
1112
1076
  renderTracksToolbar?.(),
@@ -1177,7 +1141,7 @@ function GenrePlaylistTracks({
1177
1141
  }
1178
1142
 
1179
1143
  // src/genre-tree/GenreDetailExtras.tsx
1180
- import { useTrackNoun as useTrackNoun5 } from "@behindthemusictree/app-kit/track-noun";
1144
+ import { useTrackNoun as useTrackNoun2 } from "@behindthemusictree/app-kit/track-noun";
1181
1145
  import { SiWikidata, SiWikipedia } from "react-icons/si";
1182
1146
  import { Fragment as Fragment6, jsx as jsx14, jsxs as jsxs10 } from "react/jsx-runtime";
1183
1147
  function GenreDetailExtras({
@@ -1195,7 +1159,7 @@ function GenreDetailExtras({
1195
1159
  family,
1196
1160
  ...trackSlots
1197
1161
  }) {
1198
- const trackNoun = useTrackNoun5();
1162
+ const trackNoun = useTrackNoun2();
1199
1163
  const { data: overview, isPending } = useFetchGenreOverview(
1200
1164
  genreUuid,
1201
1165
  scope,
@@ -1329,69 +1293,13 @@ function GenreInfoPanelBody({
1329
1293
  ] });
1330
1294
  }
1331
1295
 
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
1296
  // src/genre-tree/ChangeParentPopup.tsx
1389
- import { useEffect as useEffect5, useMemo as useMemo6 } from "react";
1297
+ import { useEffect as useEffect4, useMemo as useMemo6 } from "react";
1390
1298
  import {
1391
1299
  getReparentCandidates,
1392
1300
  useUpdateGenre
1393
1301
  } from "@behindthemusictree/app-kit/genre-tree";
1394
- import { jsx as jsx17, jsxs as jsxs13 } from "react/jsx-runtime";
1302
+ import { jsx as jsx16, jsxs as jsxs12 } from "react/jsx-runtime";
1395
1303
  function ChangeParentPopup({
1396
1304
  genrePlaylist,
1397
1305
  genrePlaylists,
@@ -1419,19 +1327,19 @@ function ChangeParentPopup({
1419
1327
  }
1420
1328
  );
1421
1329
  };
1422
- useEffect5(() => {
1330
+ useEffect4(() => {
1423
1331
  const handleKeyDown = (event) => {
1424
1332
  if (event.key === "Escape") onClose();
1425
1333
  };
1426
1334
  window.addEventListener("keydown", handleKeyDown);
1427
1335
  return () => window.removeEventListener("keydown", handleKeyDown);
1428
1336
  }, [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: [
1337
+ return /* @__PURE__ */ jsxs12(BasePopup_default, { title: `Change parent of ${genrePlaylist.name}`, onClose, showCancelButton: true, fitContent: true, children: [
1338
+ /* @__PURE__ */ jsxs12("p", { className: "mb-3 text-sm text-[var(--color-text-muted)]", children: [
1431
1339
  "Current parent: ",
1432
1340
  genrePlaylist.parent?.name ?? "none"
1433
1341
  ] }),
1434
- /* @__PURE__ */ jsx17("div", { "aria-busy": isPending, className: isPending ? "pointer-events-none opacity-60" : void 0, children: /* @__PURE__ */ jsx17(
1342
+ /* @__PURE__ */ jsx16("div", { "aria-busy": isPending, className: isPending ? "pointer-events-none opacity-60" : void 0, children: /* @__PURE__ */ jsx16(
1435
1343
  GenreSearch,
1436
1344
  {
1437
1345
  genrePlaylists: candidates,
@@ -1439,7 +1347,7 @@ function ChangeParentPopup({
1439
1347
  placeholder: "Search a new parent\u2026"
1440
1348
  }
1441
1349
  ) }),
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(" ") })
1350
+ 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
1351
  ] });
1444
1352
  }
1445
1353
 
@@ -1463,7 +1371,7 @@ import {
1463
1371
  SiYoutube
1464
1372
  } from "react-icons/si";
1465
1373
  import { Chip } from "@behindthemusictree/ui";
1466
- import { jsx as jsx18 } from "react/jsx-runtime";
1374
+ import { jsx as jsx17 } from "react/jsx-runtime";
1467
1375
  var LINK_KINDS = {
1468
1376
  official: { label: "Official website", Icon: Globe },
1469
1377
  wikipedia: { label: "Wikipedia", Icon: SiWikipedia2 },
@@ -1500,7 +1408,7 @@ function artistLinks(artist) {
1500
1408
  return links;
1501
1409
  }
1502
1410
  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)) });
1411
+ 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
1412
  }
1505
1413
 
1506
1414
  // src/genre-tree/artist/formatYears.ts
@@ -1511,7 +1419,7 @@ function formatYears(beginYear, endYear, ended) {
1511
1419
  }
1512
1420
 
1513
1421
  // src/genre-tree/artist/ArtistHeader.tsx
1514
- import { jsx as jsx19 } from "react/jsx-runtime";
1422
+ import { jsx as jsx18 } from "react/jsx-runtime";
1515
1423
  var countryNames = new Intl.DisplayNames(["en"], { type: "region" });
1516
1424
  function ArtistHeader({ artist }) {
1517
1425
  const fields = [];
@@ -1521,35 +1429,35 @@ function ArtistHeader({ artist }) {
1521
1429
  const years = formatYears(artist.beginYear, artist.endYear, artist.ended);
1522
1430
  if (years) fields.push({ label: "Active", value: years });
1523
1431
  const links = artistLinks(artist);
1524
- if (links.length > 0) fields.push({ label: "Links", value: /* @__PURE__ */ jsx19(ArtistLinks, { links }) });
1432
+ if (links.length > 0) fields.push({ label: "Links", value: /* @__PURE__ */ jsx18(ArtistLinks, { links }) });
1525
1433
  if (fields.length === 0) return null;
1526
- return /* @__PURE__ */ jsx19(FieldList, { items: fields });
1434
+ return /* @__PURE__ */ jsx18(FieldList, { items: fields });
1527
1435
  }
1528
1436
 
1529
1437
  // src/genre-tree/artist/ArtistPhoto.tsx
1530
- import { jsx as jsx20, jsxs as jsxs14 } from "react/jsx-runtime";
1438
+ import { jsx as jsx19, jsxs as jsxs13 } from "react/jsx-runtime";
1531
1439
  function ExternalLink({ href, children }) {
1532
- return /* @__PURE__ */ jsx20("a", { href, target: "_blank", rel: "noopener noreferrer", className: "underline-offset-2 hover:underline", children });
1440
+ return /* @__PURE__ */ jsx19("a", { href, target: "_blank", rel: "noopener noreferrer", className: "underline-offset-2 hover:underline", children });
1533
1441
  }
1534
1442
  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: [
1443
+ return /* @__PURE__ */ jsxs13("figure", { className: "flex flex-col gap-1.5 px-6 py-2.5", children: [
1444
+ /* @__PURE__ */ jsx19("img", { src: image.url, alt: name, className: "h-64 w-full object-contain bg-[var(--color-surface-muted)] rounded" }),
1445
+ /* @__PURE__ */ jsxs13("figcaption", { className: "text-xs text-[var(--color-text-muted)]", children: [
1538
1446
  "Photo: ",
1539
- /* @__PURE__ */ jsx20(ExternalLink, { href: image.pageUrl, children: image.author ?? "Wikimedia Commons" }),
1447
+ /* @__PURE__ */ jsx19(ExternalLink, { href: image.pageUrl, children: image.author ?? "Wikimedia Commons" }),
1540
1448
  ",",
1541
1449
  " ",
1542
- image.licenseUrl ? /* @__PURE__ */ jsx20(ExternalLink, { href: image.licenseUrl, children: image.license }) : image.license
1450
+ image.licenseUrl ? /* @__PURE__ */ jsx19(ExternalLink, { href: image.licenseUrl, children: image.license }) : image.license
1543
1451
  ] })
1544
1452
  ] });
1545
1453
  }
1546
1454
 
1547
1455
  // src/genre-tree/artist/ArtistMembers.tsx
1548
1456
  import { PanelGroup, PanelSection } from "@behindthemusictree/ui";
1549
- import { jsx as jsx21, jsxs as jsxs15 } from "react/jsx-runtime";
1457
+ import { jsx as jsx20, jsxs as jsxs14 } from "react/jsx-runtime";
1550
1458
  function MembershipList({ title, memberships, renderArtistLink }) {
1551
1459
  if (memberships.length === 0) return null;
1552
- return /* @__PURE__ */ jsx21(PanelGroup, { title, children: /* @__PURE__ */ jsx21("ul", { children: memberships.map((membership, index) => {
1460
+ return /* @__PURE__ */ jsx20(PanelGroup, { title, children: /* @__PURE__ */ jsx20("ul", { children: memberships.map((membership, index) => {
1553
1461
  const details = [
1554
1462
  formatYears(membership.beginYear, membership.endYear, membership.ended),
1555
1463
  ...membership.roles
@@ -1557,9 +1465,9 @@ function MembershipList({ title, memberships, renderArtistLink }) {
1557
1465
  const detailsText = details.filter(Boolean).join(" \xB7 ");
1558
1466
  return (
1559
1467
  // 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: [
1468
+ /* @__PURE__ */ jsxs14("li", { className: "truncate py-0.5", title: detailsText || void 0, children: [
1469
+ /* @__PURE__ */ jsx20("span", { className: "font-medium text-[var(--color-text)]", children: renderArtistLink(membership.artist) }),
1470
+ detailsText && /* @__PURE__ */ jsxs14("span", { className: "text-[var(--color-text-muted)]", children: [
1563
1471
  " \u2014 ",
1564
1472
  detailsText
1565
1473
  ] })
@@ -1570,9 +1478,9 @@ function MembershipList({ title, memberships, renderArtistLink }) {
1570
1478
  function ArtistMembers({ title, memberships, renderArtistLink }) {
1571
1479
  if (memberships.length === 0) return null;
1572
1480
  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(
1481
+ return /* @__PURE__ */ jsxs14(PanelSection, { title, children: [
1482
+ /* @__PURE__ */ jsx20(MembershipList, { title: "Current", memberships: memberships.filter(isCurrent), renderArtistLink }),
1483
+ /* @__PURE__ */ jsx20(
1576
1484
  MembershipList,
1577
1485
  {
1578
1486
  title: "Past",
@@ -1585,7 +1493,7 @@ function ArtistMembers({ title, memberships, renderArtistLink }) {
1585
1493
 
1586
1494
  // src/genre-tree/artist/ArtistDiscography.tsx
1587
1495
  import { PanelGroup as PanelGroup2, PanelSection as PanelSection2 } from "@behindthemusictree/ui";
1588
- import { jsx as jsx22, jsxs as jsxs16 } from "react/jsx-runtime";
1496
+ import { jsx as jsx21, jsxs as jsxs15 } from "react/jsx-runtime";
1589
1497
  var GROUPS = ["Albums", "EPs", "Singles", "Live", "Compilations", "Other"];
1590
1498
  var PRIMARY_GROUPS = { album: "Albums", ep: "EPs", single: "Singles" };
1591
1499
  function discographyGroup({ primaryType, secondaryTypes }) {
@@ -1605,12 +1513,12 @@ function byYear(a, b) {
1605
1513
  function ArtistDiscography({ albums }) {
1606
1514
  if (albums.length === 0) return null;
1607
1515
  const sorted = [...albums].sort(byYear);
1608
- return /* @__PURE__ */ jsx22(PanelSection2, { title: "Discography", children: GROUPS.map((group) => {
1516
+ return /* @__PURE__ */ jsx21(PanelSection2, { title: "Discography", children: GROUPS.map((group) => {
1609
1517
  const groupAlbums = sorted.filter((album) => discographyGroup(album) === group);
1610
1518
  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 })
1519
+ 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: [
1520
+ /* @__PURE__ */ jsx21("span", { className: "tabular-nums text-[var(--color-text-muted)]", children: album.year ?? "\u2014" }),
1521
+ /* @__PURE__ */ jsx21("span", { className: "truncate text-[var(--color-text)]", title: album.name, children: album.name })
1614
1522
  ] }, album.uuid)) }) }, group);
1615
1523
  }) });
1616
1524
  }
@@ -1637,7 +1545,7 @@ import { useTrackList as useTrackList3 } from "@behindthemusictree/app-kit/genre
1637
1545
  import { FaPlay as FaPlay2, FaPause as FaPause2 } from "react-icons/fa";
1638
1546
  import { usePlayer as usePlayer6 } from "@behindthemusictree/app-kit/player";
1639
1547
  import { PlayStates as PlayStates2 } from "@behindthemusictree/app-kit/player";
1640
- import { jsx as jsx23, jsxs as jsxs17 } from "react/jsx-runtime";
1548
+ import { jsx as jsx22, jsxs as jsxs16 } from "react/jsx-runtime";
1641
1549
  function TrackPositionPlayPause({
1642
1550
  position,
1643
1551
  uuid,
@@ -1651,14 +1559,14 @@ function TrackPositionPlayPause({
1651
1559
  const isPlaying = isThisTrack && isConfirmedPlayable && playState === PlayStates2.PLAYING;
1652
1560
  const isPaused = isThisTrack && isConfirmedPlayable && playState === PlayStates2.PAUSED;
1653
1561
  const isCurrentTrackActive = isPlaying || isPaused;
1654
- return /* @__PURE__ */ jsxs17(
1562
+ return /* @__PURE__ */ jsxs16(
1655
1563
  "div",
1656
1564
  {
1657
1565
  className: "track-position-play-pause relative flex items-center justify-center gap-2 text-lg w-16 cursor-pointer",
1658
1566
  style: { minWidth: "4rem", maxWidth: "4rem" },
1659
1567
  onClick: handlePlayPauseClick,
1660
1568
  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(
1569
+ /* @__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
1570
  "span",
1663
1571
  {
1664
1572
  className: "track-position-play-pause__bars",
@@ -1666,18 +1574,18 @@ function TrackPositionPlayPause({
1666
1574
  role: "img",
1667
1575
  "aria-label": isPlaying ? "Playing" : "Paused",
1668
1576
  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" })
1577
+ /* @__PURE__ */ jsx22("span", { className: "track-position-play-pause__bar" }),
1578
+ /* @__PURE__ */ jsx22("span", { className: "track-position-play-pause__bar" }),
1579
+ /* @__PURE__ */ jsx22("span", { className: "track-position-play-pause__bar" })
1672
1580
  ]
1673
1581
  }
1674
1582
  ) : position }),
1675
- /* @__PURE__ */ jsx23(
1583
+ /* @__PURE__ */ jsx22(
1676
1584
  "div",
1677
1585
  {
1678
1586
  className: `hidden group-hover:flex items-center justify-center ${isCurrentTrackActive ? "" : "absolute inset-0"}`,
1679
1587
  "aria-hidden": "true",
1680
- children: isAwaitingPlayback ? /* @__PURE__ */ jsx23("span", { className: "track-position-play-pause__spinner" }) : isPlaying ? /* @__PURE__ */ jsx23(FaPause2, {}) : /* @__PURE__ */ jsx23(FaPlay2, {})
1588
+ children: isAwaitingPlayback ? /* @__PURE__ */ jsx22("span", { className: "track-position-play-pause__spinner" }) : isPlaying ? /* @__PURE__ */ jsx22(FaPause2, {}) : /* @__PURE__ */ jsx22(FaPlay2, {})
1681
1589
  }
1682
1590
  )
1683
1591
  ]
@@ -1688,7 +1596,7 @@ function TrackPositionPlayPause({
1688
1596
  // src/genre-tree/track-list-sidebar/TrackItem.tsx
1689
1597
  import { isSong as isSong2, songUnplayableReason as songUnplayableReason2 } from "@behindthemusictree/app-kit/genre-tree";
1690
1598
  import { ArtistNames as ArtistNames2 } from "@behindthemusictree/app-kit/genre-tree";
1691
- import { jsx as jsx24, jsxs as jsxs18 } from "react/jsx-runtime";
1599
+ import { jsx as jsx23, jsxs as jsxs17 } from "react/jsx-runtime";
1692
1600
  function TrackItem({
1693
1601
  track,
1694
1602
  position,
@@ -1712,12 +1620,12 @@ function TrackItem({
1712
1620
  loadTrackForPlayer(track.uuid);
1713
1621
  }
1714
1622
  };
1715
- return /* @__PURE__ */ jsxs18(
1623
+ return /* @__PURE__ */ jsxs17(
1716
1624
  "div",
1717
1625
  {
1718
1626
  className: `track-item flex h-14 text-[var(--color-text)] ${isUnplayable ? "opacity-50" : "hover:bg-[var(--color-surface-muted)] group"}`,
1719
1627
  children: [
1720
- isUnplayable ? /* @__PURE__ */ jsx24(
1628
+ isUnplayable ? /* @__PURE__ */ jsx23(
1721
1629
  "div",
1722
1630
  {
1723
1631
  className: "flex items-center justify-center text-lg w-16",
@@ -1725,7 +1633,7 @@ function TrackItem({
1725
1633
  "aria-disabled": "true",
1726
1634
  children: position
1727
1635
  }
1728
- ) : /* @__PURE__ */ jsx24(
1636
+ ) : /* @__PURE__ */ jsx23(
1729
1637
  TrackPositionPlayPause,
1730
1638
  {
1731
1639
  position,
@@ -1734,12 +1642,12 @@ function TrackItem({
1734
1642
  isSelected: selectedTrack?.uuid === track.uuid
1735
1643
  }
1736
1644
  ),
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 }) }) : ""
1645
+ /* @__PURE__ */ jsxs17("div", { className: "title-artist-container flex flex-col items-start justify-center min-w-0 flex-1", children: [
1646
+ /* @__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 }) }),
1647
+ 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
1648
  ] }),
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(
1649
+ /* @__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 }) : "" }),
1650
+ renderDuration && /* @__PURE__ */ jsx23(
1743
1651
  "div",
1744
1652
  {
1745
1653
  className: "duration flex text-base w-16 items-center justify-center",
@@ -1747,14 +1655,14 @@ function TrackItem({
1747
1655
  children: renderDuration(track)
1748
1656
  }
1749
1657
  ),
1750
- renderActions && /* @__PURE__ */ jsx24("div", { className: "edit flex text-base w-6 items-center justify-center mr-2 cursor-pointer", children: renderActions(track) })
1658
+ renderActions && /* @__PURE__ */ jsx23("div", { className: "edit flex text-base w-6 items-center justify-center mr-2 cursor-pointer", children: renderActions(track) })
1751
1659
  ]
1752
1660
  }
1753
1661
  );
1754
1662
  }
1755
1663
 
1756
1664
  // src/genre-tree/artist/ArtistSongs.tsx
1757
- import { Fragment as Fragment8, jsx as jsx25, jsxs as jsxs19 } from "react/jsx-runtime";
1665
+ import { Fragment as Fragment8, jsx as jsx24, jsxs as jsxs18 } from "react/jsx-runtime";
1758
1666
  var SongPageSchema2 = PaginatedResponseSchema2(SongDetailedSchema2);
1759
1667
  var SORTS = [
1760
1668
  { ordering: "title", label: "Title" },
@@ -1787,21 +1695,21 @@ function ArtistSongs({ artist, getBackendBaseUrl, renderArtistLink, renderSongLi
1787
1695
  event.stopPropagation();
1788
1696
  void play(song.uuid);
1789
1697
  };
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(
1698
+ return /* @__PURE__ */ jsx24(PanelSection3, { 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: [
1699
+ /* @__PURE__ */ jsxs18("div", { className: "flex flex-wrap items-center justify-between gap-2 mb-2", children: [
1700
+ /* @__PURE__ */ jsxs18(
1793
1701
  "button",
1794
1702
  {
1795
1703
  type: "button",
1796
1704
  onClick: () => void play(),
1797
1705
  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
1706
  children: [
1799
- /* @__PURE__ */ jsx25(Play2, { className: "h-3.5 w-3.5", "aria-hidden": "true" }),
1707
+ /* @__PURE__ */ jsx24(Play2, { className: "h-3.5 w-3.5", "aria-hidden": "true" }),
1800
1708
  "Play all"
1801
1709
  ]
1802
1710
  }
1803
1711
  ),
1804
- /* @__PURE__ */ jsx25("div", { role: "group", "aria-label": "Sort songs by", className: "flex gap-1 text-xs", children: SORTS.map((sort) => /* @__PURE__ */ jsx25(
1712
+ /* @__PURE__ */ jsx24("div", { role: "group", "aria-label": "Sort songs by", className: "flex gap-1 text-xs", children: SORTS.map((sort) => /* @__PURE__ */ jsx24(
1805
1713
  "button",
1806
1714
  {
1807
1715
  type: "button",
@@ -1816,7 +1724,7 @@ function ArtistSongs({ artist, getBackendBaseUrl, renderArtistLink, renderSongLi
1816
1724
  sort.ordering
1817
1725
  )) })
1818
1726
  ] }),
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(
1727
+ /* @__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
1728
  TrackItem,
1821
1729
  {
1822
1730
  track: song,
@@ -1825,12 +1733,12 @@ function ArtistSongs({ artist, getBackendBaseUrl, renderArtistLink, renderSongLi
1825
1733
  renderSongLink
1826
1734
  }
1827
1735
  ) }, song.uuid)) }),
1828
- /* @__PURE__ */ jsx25(Pagination, { currentPage: page, totalPages: data.totalPages, onPageChange: setPage })
1736
+ /* @__PURE__ */ jsx24(Pagination, { currentPage: page, totalPages: data.totalPages, onPageChange: setPage })
1829
1737
  ] }) });
1830
1738
  }
1831
1739
 
1832
1740
  // src/genre-tree/artist/useArtistPanel.tsx
1833
- import { Fragment as Fragment9, jsx as jsx26, jsxs as jsxs20 } from "react/jsx-runtime";
1741
+ import { Fragment as Fragment9, jsx as jsx25, jsxs as jsxs19 } from "react/jsx-runtime";
1834
1742
  function useArtistPanel({
1835
1743
  uuid,
1836
1744
  scope,
@@ -1851,26 +1759,26 @@ function useArtistPanel({
1851
1759
  return {
1852
1760
  ...panel,
1853
1761
  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" })
1762
+ content: /* @__PURE__ */ jsxs19("div", { "aria-busy": "true", className: "flex flex-col gap-2.5 px-6 py-2.5", children: [
1763
+ /* @__PURE__ */ jsx25(Skeleton4, { className: "h-3 w-16" }),
1764
+ /* @__PURE__ */ jsx25(Skeleton4, { className: "h-4 w-32" }),
1765
+ /* @__PURE__ */ jsx25(Skeleton4, { className: "h-3 w-16" }),
1766
+ /* @__PURE__ */ jsx25(Skeleton4, { className: "h-4 w-24" }),
1767
+ /* @__PURE__ */ jsx25(Skeleton4, { className: "h-16 mt-2.5" }),
1768
+ /* @__PURE__ */ jsx25(Skeleton4, { className: "h-16" })
1861
1769
  ] })
1862
1770
  };
1863
1771
  case "not-found":
1864
1772
  return {
1865
1773
  ...panel,
1866
1774
  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." })
1775
+ content: /* @__PURE__ */ jsx25("p", { className: "px-6 py-2.5 text-[var(--color-text-muted)]", children: "This artist does not exist." })
1868
1776
  };
1869
1777
  case "error":
1870
1778
  return {
1871
1779
  ...panel,
1872
1780
  title: "Artist",
1873
- content: /* @__PURE__ */ jsx26("p", { className: "px-6 py-2.5 text-red-600", children: "Could not load this artist." })
1781
+ content: /* @__PURE__ */ jsx25("p", { className: "px-6 py-2.5 text-red-600", children: "Could not load this artist." })
1874
1782
  };
1875
1783
  case "success": {
1876
1784
  const { artist } = result;
@@ -1879,13 +1787,13 @@ function useArtistPanel({
1879
1787
  return {
1880
1788
  ...panel,
1881
1789
  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(
1790
+ content: /* @__PURE__ */ jsxs19(Fragment9, { children: [
1791
+ 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 }) }) }),
1792
+ artist.image && /* @__PURE__ */ jsx25(ArtistPhoto, { image: artist.image, name: artist.name }),
1793
+ /* @__PURE__ */ jsx25(ArtistHeader, { artist }),
1794
+ /* @__PURE__ */ jsx25(ArtistMembers, { title: "Members", memberships: artist.members, renderArtistLink }),
1795
+ /* @__PURE__ */ jsx25(ArtistMembers, { title: "Member of", memberships: artist.memberOf, renderArtistLink }),
1796
+ artist.tracksCount > 0 && /* @__PURE__ */ jsx25(
1889
1797
  ArtistSongs,
1890
1798
  {
1891
1799
  artist: { uuid: artist.uuid, name: artist.name },
@@ -1895,7 +1803,7 @@ function useArtistPanel({
1895
1803
  }
1896
1804
  )
1897
1805
  ] }),
1898
- extraContent: artist.albums.length > 0 ? /* @__PURE__ */ jsx26(ArtistDiscography, { albums: artist.albums }) : void 0
1806
+ extraContent: artist.albums.length > 0 ? /* @__PURE__ */ jsx25(ArtistDiscography, { albums: artist.albums }) : void 0
1899
1807
  };
1900
1808
  }
1901
1809
  }
@@ -1919,7 +1827,7 @@ import {
1919
1827
  useSongHistory,
1920
1828
  useTrackList as useTrackList5
1921
1829
  } from "@behindthemusictree/app-kit/genre-tree";
1922
- import { Fragment as Fragment10, jsx as jsx27, jsxs as jsxs21 } from "react/jsx-runtime";
1830
+ import { Fragment as Fragment10, jsx as jsx26, jsxs as jsxs20 } from "react/jsx-runtime";
1923
1831
  var numberFormat = new Intl.NumberFormat("en");
1924
1832
  var dateFormat = new Intl.DateTimeFormat("en", { dateStyle: "medium" });
1925
1833
  var externalLink = "text-[var(--color-primary)] hover:underline focus-visible:underline";
@@ -1927,32 +1835,32 @@ function SongHistory({ uuid, getBackendBaseUrl }) {
1927
1835
  const [open, setOpen] = useState6(false);
1928
1836
  const history = useSongHistory(uuid, getBackendBaseUrl, open);
1929
1837
  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." });
1838
+ if (history.status === "loading") body = /* @__PURE__ */ jsx26(Skeleton5, { className: "h-10", "aria-busy": "true" });
1839
+ else if (history.status === "error") body = /* @__PURE__ */ jsx26("p", { className: "text-red-600", children: "Could not load the history." });
1932
1840
  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: [
1841
+ 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: [
1842
+ /* @__PURE__ */ jsx26("span", { className: "font-medium", children: entry.action }),
1843
+ entry.oldValue !== null || entry.newValue !== null ? /* @__PURE__ */ jsxs20("span", { className: "text-[var(--color-text-muted)]", children: [
1936
1844
  " ",
1937
1845
  "\u2014 ",
1938
1846
  entry.oldValue ?? "none",
1939
1847
  " \u2192 ",
1940
1848
  entry.newValue ?? "none"
1941
1849
  ] }) : null,
1942
- /* @__PURE__ */ jsxs21("span", { className: "block text-xs text-[var(--color-text-muted)]", children: [
1850
+ /* @__PURE__ */ jsxs20("span", { className: "block text-xs text-[var(--color-text-muted)]", children: [
1943
1851
  entry.actorPseudo ?? "Anonymous",
1944
1852
  " \xB7 ",
1945
1853
  dateFormat.format(new Date(entry.createdOn))
1946
1854
  ] })
1947
1855
  ] }, entry.uuid)) });
1948
- return /* @__PURE__ */ jsxs21(
1856
+ return /* @__PURE__ */ jsxs20(
1949
1857
  "details",
1950
1858
  {
1951
1859
  onToggle: (event) => setOpen(event.currentTarget.open),
1952
1860
  className: "px-6 pb-2.5 mt-2.5 border-t border-[var(--color-border)]",
1953
1861
  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 })
1862
+ /* @__PURE__ */ jsx26("summary", { className: "mt-2.5 cursor-pointer text-[0.6875rem] font-semibold uppercase text-[var(--color-text-muted)]", children: "History" }),
1863
+ /* @__PURE__ */ jsx26("div", { className: "mt-1.5", children: body })
1956
1864
  ]
1957
1865
  }
1958
1866
  );
@@ -1978,13 +1886,13 @@ function SongPanelContent({
1978
1886
  const path = genrePath.length > 0 ? genrePath : null;
1979
1887
  fields.push({
1980
1888
  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 })
1889
+ value: path ? /* @__PURE__ */ jsx26("span", { className: "flex flex-wrap items-center gap-x-1", children: path.map((genrePlaylist, index) => /* @__PURE__ */ jsxs20("span", { children: [
1890
+ index > 0 && /* @__PURE__ */ jsx26("span", { "aria-hidden": "true", children: "\u203A " }),
1891
+ /* @__PURE__ */ jsx26("button", { type: "button", onClick: () => onSelectGenre(genrePlaylist), className: "hover:underline", children: genrePlaylist.name })
1984
1892
  ] }, genrePlaylist.uuid)) }) : song.genre.name
1985
1893
  });
1986
1894
  } else {
1987
- fields.push({ label: "Genre", value: /* @__PURE__ */ jsx27("span", { className: "text-[var(--color-text-muted)]", children: "No genre" }) });
1895
+ fields.push({ label: "Genre", value: /* @__PURE__ */ jsx26("span", { className: "text-[var(--color-text-muted)]", children: "No genre" }) });
1988
1896
  }
1989
1897
  if (song.firstReleaseYear) fields.push({ label: "First released", value: song.firstReleaseYear });
1990
1898
  const viewCount = song.youtubeVideo?.viewCount;
@@ -1994,11 +1902,11 @@ function SongPanelContent({
1994
1902
  const links = [];
1995
1903
  if (musicBrainzUrl)
1996
1904
  links.push(
1997
- /* @__PURE__ */ jsx27("a", { href: musicBrainzUrl, target: "_blank", rel: "noopener noreferrer", className: externalLink, children: "MusicBrainz" }, "mb")
1905
+ /* @__PURE__ */ jsx26("a", { href: musicBrainzUrl, target: "_blank", rel: "noopener noreferrer", className: externalLink, children: "MusicBrainz" }, "mb")
1998
1906
  );
1999
1907
  if (song.youtubeVideo)
2000
1908
  links.push(
2001
- /* @__PURE__ */ jsx27(
1909
+ /* @__PURE__ */ jsx26(
2002
1910
  "a",
2003
1911
  {
2004
1912
  href: `https://www.youtube.com/watch?v=${encodeURIComponent(song.youtubeVideo.id)}`,
@@ -2013,33 +1921,33 @@ function SongPanelContent({
2013
1921
  if (links.length > 0)
2014
1922
  fields.push({
2015
1923
  label: "Links",
2016
- value: /* @__PURE__ */ jsx27("span", { className: "flex gap-3", children: links })
1924
+ value: /* @__PURE__ */ jsx26("span", { className: "flex gap-3", children: links })
2017
1925
  });
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(
1926
+ return /* @__PURE__ */ jsxs20(Fragment10, { children: [
1927
+ /* @__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: [
1928
+ unplayableReason ? /* @__PURE__ */ jsx26("p", { className: "flex items-center px-2.5 text-sm text-[var(--color-text-muted)]", children: unplayableReason }) : /* @__PURE__ */ jsxs20(
2021
1929
  "button",
2022
1930
  {
2023
1931
  type: "button",
2024
1932
  onClick: () => isCurrent ? handlePlayPauseAction() : playNewTrackListFromTrackUuid(song, scope),
2025
1933
  className: "group flex flex-col items-center gap-1 w-16 text-xs text-center text-[var(--color-text)] focus-visible:outline-none",
2026
1934
  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" }) }),
1935
+ /* @__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
1936
  isPlaying ? "Pause" : "Play"
2029
1937
  ]
2030
1938
  }
2031
1939
  ),
2032
- shareUrl && /* @__PURE__ */ jsx27(ShareActionButton, { target: shareTarget, url: shareUrl })
1940
+ shareUrl && /* @__PURE__ */ jsx26(ShareActionButton, { target: shareTarget, url: shareUrl })
2033
1941
  ] }) }),
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 })
1942
+ 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 }) }),
1943
+ /* @__PURE__ */ jsx26(FieldList2, { items: fields }),
1944
+ song.essentialGenres && song.essentialGenres.length > 0 && /* @__PURE__ */ jsx26(PanelSection4, { title: "Essential in", children: /* @__PURE__ */ jsx26("ul", { children: song.essentialGenres.map((genre) => /* @__PURE__ */ jsx26("li", { className: "py-0.5", children: genre.name }, genre.uuid)) }) }),
1945
+ /* @__PURE__ */ jsx26(SongHistory, { uuid: song.uuid, getBackendBaseUrl })
2038
1946
  ] });
2039
1947
  }
2040
1948
 
2041
1949
  // src/genre-tree/song/useSongPanel.tsx
2042
- import { jsx as jsx28, jsxs as jsxs22 } from "react/jsx-runtime";
1950
+ import { jsx as jsx27, jsxs as jsxs21 } from "react/jsx-runtime";
2043
1951
  function useSongPanel({
2044
1952
  uuid,
2045
1953
  scope,
@@ -2061,24 +1969,24 @@ function useSongPanel({
2061
1969
  return {
2062
1970
  ...panel,
2063
1971
  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" })
1972
+ content: /* @__PURE__ */ jsxs21("div", { "aria-busy": "true", className: "flex flex-col gap-2.5 px-6 py-2.5", children: [
1973
+ /* @__PURE__ */ jsx27(Skeleton6, { className: "h-3 w-16" }),
1974
+ /* @__PURE__ */ jsx27(Skeleton6, { className: "h-4 w-32" }),
1975
+ /* @__PURE__ */ jsx27(Skeleton6, { className: "h-3 w-16" }),
1976
+ /* @__PURE__ */ jsx27(Skeleton6, { className: "h-4 w-24" })
2069
1977
  ] })
2070
1978
  };
2071
1979
  case "not-found":
2072
1980
  return {
2073
1981
  ...panel,
2074
1982
  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." })
1983
+ content: /* @__PURE__ */ jsx27("p", { className: "px-6 py-2.5 text-[var(--color-text-muted)]", children: "This song does not exist." })
2076
1984
  };
2077
1985
  case "error":
2078
1986
  return {
2079
1987
  ...panel,
2080
1988
  title: "Song",
2081
- content: /* @__PURE__ */ jsx28("p", { className: "px-6 py-2.5 text-red-600", children: "Could not load this song." })
1989
+ content: /* @__PURE__ */ jsx27("p", { className: "px-6 py-2.5 text-red-600", children: "Could not load this song." })
2082
1990
  };
2083
1991
  case "success": {
2084
1992
  const { song } = result;
@@ -2086,7 +1994,7 @@ function useSongPanel({
2086
1994
  return {
2087
1995
  ...panel,
2088
1996
  title: song.title,
2089
- content: /* @__PURE__ */ jsx28(
1997
+ content: /* @__PURE__ */ jsx27(
2090
1998
  SongPanelContent,
2091
1999
  {
2092
2000
  song,
@@ -2106,12 +2014,12 @@ function useSongPanel({
2106
2014
  // src/genre-tree/SizeMetricToggle.tsx
2107
2015
  import { MdMusicNote, MdVisibility } from "react-icons/md";
2108
2016
  import { SegmentedControl } from "@behindthemusictree/ui";
2109
- import { jsx as jsx29 } from "react/jsx-runtime";
2017
+ import { jsx as jsx28 } from "react/jsx-runtime";
2110
2018
  function SizeMetricToggle({
2111
2019
  value,
2112
2020
  onChange
2113
2021
  }) {
2114
- return /* @__PURE__ */ jsx29(
2022
+ return /* @__PURE__ */ jsx28(
2115
2023
  SegmentedControl,
2116
2024
  {
2117
2025
  variant: "floating",
@@ -2119,26 +2027,19 @@ function SizeMetricToggle({
2119
2027
  value,
2120
2028
  onChange,
2121
2029
  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" }
2030
+ { value: "songs", label: /* @__PURE__ */ jsx28(MdMusicNote, { size: 18, "aria-hidden": true }), "aria-label": "Size by songs", title: "Node size by song count" },
2031
+ { value: "views", label: /* @__PURE__ */ jsx28(MdVisibility, { size: 18, "aria-hidden": true }), "aria-label": "Size by views", title: "Node size by song views" }
2124
2032
  ]
2125
2033
  }
2126
2034
  );
2127
2035
  }
2128
2036
 
2129
2037
  // 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
- `;
2038
+ import { Fragment as Fragment11, jsx as jsx29, jsxs as jsxs22 } from "react/jsx-runtime";
2140
2039
  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]";
2040
+ var REGIONS_CLASS = "[--gtv-center-left:var(--layout-center-left)] [--gtv-center-right:var(--layout-center-right)] [--gtv-center-top:var(--layout-band-top)]";
2041
+ var noop = () => {
2042
+ };
2142
2043
  function GenreTreeView({
2143
2044
  scope,
2144
2045
  handleGenreCreationAction,
@@ -2277,7 +2178,7 @@ function GenreTreeView({
2277
2178
  const openSongRef = useRef4(openSong);
2278
2179
  openSongRef.current = openSong;
2279
2180
  const resolvedRenderSongLink = useCallback6(
2280
- (song) => renderSongLink ? renderSongLink(song) : /* @__PURE__ */ jsx30("button", { type: "button", onClick: () => openSongRef.current(song.uuid), className: "hover:underline", children: song.title }),
2181
+ (song) => renderSongLink ? renderSongLink(song) : /* @__PURE__ */ jsx29("button", { type: "button", onClick: () => openSongRef.current(song.uuid), className: "hover:underline", children: song.title }),
2281
2182
  [renderSongLink]
2282
2183
  );
2283
2184
  const [expandedSongUuid, setExpandedSongUuid] = useState7(null);
@@ -2289,7 +2190,7 @@ function GenreTreeView({
2289
2190
  scope,
2290
2191
  getBackendBaseUrl: artistBaseUrl,
2291
2192
  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 })),
2193
+ renderArtistLink: renderArtistLink ?? ((artist) => artist.isSpecialPurpose ? artist.name : /* @__PURE__ */ jsx29("button", { type: "button", onClick: () => openArtist(artist.uuid), className: "hover:underline", children: artist.name })),
2293
2194
  expanded: controlledArtistExpanded ?? (artistUuid !== null && expandedArtistUuid === artistUuid),
2294
2195
  onExpandedChange: (on) => {
2295
2196
  if (controlledArtistExpanded === void 0) setExpandedArtistUuid(on ? artistUuid : null);
@@ -2298,7 +2199,7 @@ function GenreTreeView({
2298
2199
  renderSongLink: resolvedRenderSongLink,
2299
2200
  onClose: () => changeArtist(null)
2300
2201
  });
2301
- const resolvedRenderArtistLink = renderArtistLink ?? ((artist) => artist.isSpecialPurpose ? artist.name : /* @__PURE__ */ jsx30("button", { type: "button", onClick: () => openArtist(artist.uuid), className: "hover:underline", children: artist.name }));
2202
+ const resolvedRenderArtistLink = renderArtistLink ?? ((artist) => artist.isSpecialPurpose ? artist.name : /* @__PURE__ */ jsx29("button", { type: "button", onClick: () => openArtist(artist.uuid), className: "hover:underline", children: artist.name }));
2302
2203
  const getGenrePath = (genre) => {
2303
2204
  const path = [];
2304
2205
  let current = visibleGenrePlaylists.find((playlist) => playlist.criteria?.uuid === genre.uuid);
@@ -2330,7 +2231,6 @@ function GenreTreeView({
2330
2231
  const builtInSongPanel = overlayPanel || hasSelectedGenreNode ? void 0 : songPanel;
2331
2232
  const builtInArtistPanel = overlayPanel || hasSelectedGenreNode || builtInSongPanel ? void 0 : artistPanel;
2332
2233
  const builtInPanel = builtInSongPanel ?? builtInArtistPanel;
2333
- const activeOverlayPanel = overlayPanel ?? (builtInPanel && { ...builtInPanel, hideTitle: true, hideClose: true });
2334
2234
  const [uncontrolledCollapsed, setUncontrolledCollapsed] = useState7(false);
2335
2235
  const [uncontrolledExpanded, setUncontrolledExpanded] = useState7(false);
2336
2236
  const isCollapsed = collapsed ?? uncontrolledCollapsed;
@@ -2343,38 +2243,39 @@ function GenreTreeView({
2343
2243
  setUncontrolledExpanded(next);
2344
2244
  onExpandedChange?.(next);
2345
2245
  };
2346
- useEffect6(() => {
2246
+ useEffect5(() => {
2347
2247
  setUncontrolledCollapsed(false);
2348
2248
  }, [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
2249
  const [isWheelRevealed, setIsWheelRevealed] = useState7(false);
2363
2250
  const usesHandoff = viewMode === "wheel" || viewMode === "pop-core";
2364
2251
  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]);
2252
+ const [resolveStyle, setResolveStyle] = useState7(null);
2253
+ const handleNodeStyleResolverChange = useCallback6(
2254
+ (resolver) => setResolveStyle(() => resolver),
2255
+ []
2256
+ );
2257
+ const panelRef = useRef4(null);
2258
+ const obscuredInset = useObscuredInset(panelRef);
2259
+ const hostPanel = { obscuredInset, onNodeStyleResolverChange: handleNodeStyleResolverChange };
2260
+ const nodeMetric = isArtistFiltered || !canSizeByViews ? "songs" : deferredSizeMetric;
2261
+ const treeNodes = useMemo7(() => toTreeNodes(visibleGenrePlaylists, nodeMetric), [visibleGenrePlaylists, nodeMetric]);
2262
+ const relations = useGenreNodeRelations(
2263
+ treeNodes,
2264
+ !overlayPanel && hasSelectedGenreNode ? selectedNodeId : null,
2265
+ resolveStyle
2266
+ );
2267
+ const [selectionHistory, setSelectionHistory] = useState7({
2268
+ current: selectedNodeId,
2269
+ previous: null
2270
+ });
2271
+ if (selectionHistory.current !== selectedNodeId) {
2272
+ setSelectionHistory({
2273
+ current: selectedNodeId,
2274
+ previous: selectionHistory.current ?? selectionHistory.previous
2275
+ });
2276
+ }
2277
+ const previousPlaylist = selectionHistory.previous !== null && selectionHistory.previous !== selectedNodeId ? genrePlaylistByNodeId.get(selectionHistory.previous) : void 0;
2278
+ const handlePrevious = previousPlaylist && visibleGenrePlaylists.includes(previousPlaylist) ? () => handleGenreSearchSelect(previousPlaylist) : void 0;
2378
2279
  const { showPopup, hidePopup } = usePopup5();
2379
2280
  const [searchText, setSearchText] = useState7("");
2380
2281
  const artistSearchResult = useArtistSearch(artistBaseUrl, artistSearch ? searchText : "");
@@ -2383,15 +2284,15 @@ function GenreTreeView({
2383
2284
  label: "Artists",
2384
2285
  items: artistSearchResult.artists.map((artist) => ({
2385
2286
  key: artist.uuid,
2386
- content: /* @__PURE__ */ jsxs23(Fragment11, { children: [
2387
- /* @__PURE__ */ jsxs23("span", { className: "block truncate font-medium", children: [
2287
+ content: /* @__PURE__ */ jsxs22(Fragment11, { children: [
2288
+ /* @__PURE__ */ jsxs22("span", { className: "block truncate font-medium", children: [
2388
2289
  artist.name,
2389
- artist.disambiguation && /* @__PURE__ */ jsxs23("span", { className: "font-normal text-[var(--color-neutral-500)]", children: [
2290
+ artist.disambiguation && /* @__PURE__ */ jsxs22("span", { className: "font-normal text-[var(--color-neutral-500)]", children: [
2390
2291
  " \xB7 ",
2391
2292
  artist.disambiguation
2392
2293
  ] })
2393
2294
  ] }),
2394
- artist.matchedAlias && /* @__PURE__ */ jsxs23("span", { className: "block truncate text-xs text-[var(--color-neutral-500)]", children: [
2295
+ artist.matchedAlias && /* @__PURE__ */ jsxs22("span", { className: "block truncate text-xs text-[var(--color-neutral-500)]", children: [
2395
2296
  "also known as ",
2396
2297
  artist.matchedAlias
2397
2298
  ] })
@@ -2410,9 +2311,9 @@ function GenreTreeView({
2410
2311
  label: "Songs",
2411
2312
  items: songSearchResult.songs.map((song) => ({
2412
2313
  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 ") })
2314
+ content: /* @__PURE__ */ jsxs22("span", { className: isSongSearchResultPlayable(song) ? void 0 : "opacity-60", children: [
2315
+ /* @__PURE__ */ jsx29("span", { className: "block truncate font-medium", children: song.title }),
2316
+ /* @__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
2317
  ] }),
2417
2318
  onSelect: () => {
2418
2319
  openSong(song.uuid);
@@ -2440,34 +2341,47 @@ function GenreTreeView({
2440
2341
  };
2441
2342
  const zoomInRef = useRef4(void 0);
2442
2343
  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(
2344
+ const selectGenreFromPanel = (nodeId) => {
2345
+ const genrePlaylist = genrePlaylistByNodeId.get(nodeId);
2346
+ if (genrePlaylist) handleGenreSearchSelect(genrePlaylist);
2347
+ };
2348
+ const renderGenreBody = (relation) => {
2349
+ const { node } = relation;
2350
+ const family = /* @__PURE__ */ jsx29(
2351
+ GenreNodeFamily,
2352
+ {
2353
+ node,
2354
+ fill: relation.fill,
2355
+ textColor: relation.textColor,
2356
+ parentNode: relation.parentNode,
2357
+ childNodes: relation.childNodes,
2358
+ onSelectNode: selectGenreFromPanel
2359
+ }
2360
+ );
2361
+ const ancestors = /* @__PURE__ */ jsx29(GenreNodeAncestors, { ancestorNodes: relation.ancestorNodes, onSelectNode: selectGenreFromPanel });
2362
+ const genrePlaylist = genrePlaylistByNodeId.get(node.id);
2363
+ const criteria = genrePlaylist?.criteria;
2364
+ if (!genrePlaylist || !criteria) return { primary: family, aside: ancestors };
2365
+ const genreUuid = criteria.uuid;
2366
+ const parentPlaylist = visibleGenrePlaylists.find(({ uuid }) => uuid === genrePlaylist.parent?.uuid);
2367
+ const canToggleSide = !readOnly && parentPlaylist !== void 0 && parentPlaylist.parent === null;
2368
+ const side = criteria.side ?? "core";
2369
+ const shareTarget = {
2370
+ kind: "genre",
2371
+ genrePlaylistUuid: genrePlaylist.uuid,
2372
+ name: criteria.name,
2373
+ scope,
2374
+ artistUuid
2375
+ };
2376
+ const shareUrl = getShareUrlRef.current?.(shareTarget) ?? void 0;
2377
+ const popSibling = canToggleSide && side === "core" ? visibleGenrePlaylists.find(
2378
+ (sibling) => sibling.parent?.uuid === parentPlaylist.uuid && sibling.uuid !== genrePlaylist.uuid && sibling.criteria?.side === "pop"
2379
+ ) : void 0;
2380
+ return {
2381
+ primary: /* @__PURE__ */ jsxs22(Fragment11, { children: [
2382
+ /* @__PURE__ */ jsx29(
2468
2383
  GenreInfoPanelBody,
2469
2384
  {
2470
- toolbar,
2471
2385
  family,
2472
2386
  genreUuid,
2473
2387
  genrePlaylist,
@@ -2483,7 +2397,7 @@ function GenreTreeView({
2483
2397
  onAddChild: readOnly ? void 0 : () => handleGenreCreationAction(criteria),
2484
2398
  onRename: readOnly ? void 0 : () => handleGenreRenameAction(criteria),
2485
2399
  onChangeParent: readOnly ? void 0 : () => showPopup(
2486
- /* @__PURE__ */ jsx30(
2400
+ /* @__PURE__ */ jsx29(
2487
2401
  ChangeParentPopup,
2488
2402
  {
2489
2403
  genrePlaylist: { ...genrePlaylist, criteria },
@@ -2514,49 +2428,65 @@ function GenreTreeView({
2514
2428
  }
2515
2429
  }
2516
2430
  ),
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
- );
2431
+ renderDangerZone?.(node)
2432
+ ] }),
2433
+ aside: ancestors
2434
+ };
2435
+ };
2546
2436
  const isRotationControlled = controlledAllowWheelRotation !== void 0;
2547
2437
  const allowWheelRotation = controlledAllowWheelRotation ?? internalAllowWheelRotation;
2548
- const detailTab = placement === "regions" && (hasSelectedGenreNode || detailOverlay) ? /* @__PURE__ */ jsx30(
2549
- PanelEdgeTab,
2438
+ let activePanel = null;
2439
+ if (overlayPanel) {
2440
+ activePanel = {
2441
+ title: overlayPanel.title,
2442
+ content: overlayPanel.content,
2443
+ aside: overlayPanel.extraContent,
2444
+ expanded: overlayPanel.expanded ?? false,
2445
+ onExpandedChange: overlayPanel.onExpandedChange,
2446
+ onClose: overlayPanel.onClose,
2447
+ compact: false
2448
+ };
2449
+ } else if (relations) {
2450
+ const { primary, aside } = renderGenreBody(relations);
2451
+ activePanel = {
2452
+ title: relations.node.name,
2453
+ content: primary,
2454
+ aside,
2455
+ expanded: isExpanded,
2456
+ onExpandedChange: changeExpanded,
2457
+ onClose: clearSelection,
2458
+ compact: true,
2459
+ headerAccent: { background: relations.fill, color: relations.textColor }
2460
+ };
2461
+ } else if (builtInPanel) {
2462
+ activePanel = {
2463
+ title: builtInPanel.title,
2464
+ content: builtInPanel.content,
2465
+ aside: builtInPanel.extraContent,
2466
+ expanded: builtInPanel.expanded,
2467
+ onExpandedChange: builtInPanel.onExpandedChange,
2468
+ onClose: builtInPanel.onClose,
2469
+ compact: true
2470
+ };
2471
+ }
2472
+ const isPanelShown = isTreeReady && activePanel !== null;
2473
+ const detailTab = placement === "regions" && activePanel?.onExpandedChange ? {
2474
+ edge: activePanel.expanded ? { label: "Shrink panel", onClick: () => activePanel.onExpandedChange?.(false) } : isCollapsed ? null : { label: "Hide panel", onClick: () => changeCollapsed(true) },
2475
+ center: activePanel.expanded ? null : isCollapsed ? { label: "Show panel", onClick: () => changeCollapsed(false) } : activePanel.onExpandedChange ? { label: "Extend panel", onClick: () => activePanel.onExpandedChange?.(true) } : null
2476
+ } : void 0;
2477
+ const expandButton = placement === "default" && activePanel?.onExpandedChange ? /* @__PURE__ */ jsx29(
2478
+ IconButton,
2550
2479
  {
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
2480
+ "aria-label": activePanel.expanded ? "Shrink panel" : "Extend panel",
2481
+ style: activePanel.headerAccent && { color: "inherit" },
2482
+ onClick: () => activePanel.onExpandedChange?.(!activePanel.expanded),
2483
+ 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
2484
  }
2555
2485
  ) : null;
2556
2486
  const renderArtistFilter = () => {
2557
2487
  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(
2488
+ return /* @__PURE__ */ jsxs22("span", { className: "inline-flex items-center overflow-hidden rounded-full bg-[var(--color-neutral-100)] text-sm", children: [
2489
+ /* @__PURE__ */ jsx29(
2560
2490
  "button",
2561
2491
  {
2562
2492
  type: "button",
@@ -2568,7 +2498,7 @@ function GenreTreeView({
2568
2498
  children: filterArtist.name
2569
2499
  }
2570
2500
  ),
2571
- /* @__PURE__ */ jsx30(
2501
+ /* @__PURE__ */ jsx29(
2572
2502
  "button",
2573
2503
  {
2574
2504
  type: "button",
@@ -2580,22 +2510,22 @@ function GenreTreeView({
2580
2510
  )
2581
2511
  ] });
2582
2512
  };
2583
- const actions = /* @__PURE__ */ jsxs23(Fragment11, { children: [
2584
- !isLoading && /* @__PURE__ */ jsx30(
2513
+ const actions = /* @__PURE__ */ jsxs22(Fragment11, { children: [
2514
+ !isLoading && /* @__PURE__ */ jsx29(
2585
2515
  GenreSearch,
2586
2516
  {
2587
2517
  genrePlaylists: visibleGenrePlaylists,
2588
2518
  onSelect: handleGenreSearchSelect,
2589
- selectedName: selectedGenrePlaylist?.name ?? builtInPanel?.title ?? null,
2519
+ selectedName: selectedGenrePlaylist?.name ?? (typeof builtInPanel?.title === "string" ? builtInPanel.title : null),
2590
2520
  onClear: builtInSongPanel ? () => changeSong(null) : builtInArtistPanel ? () => changeArtist(null) : handleGenreSearchClear,
2591
2521
  onPrevious: handlePrevious,
2592
2522
  onQueryChange: artistSearch || songSearch ? setSearchText : void 0,
2593
2523
  extraGroups
2594
2524
  }
2595
2525
  ),
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(
2526
+ !isLoading && filterArtist && /* @__PURE__ */ jsx29("div", { className: "hidden w-full justify-center max-md:flex", children: renderArtistFilter() }),
2527
+ /* @__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: [
2528
+ !isLoading && !isControlled && /* @__PURE__ */ jsx29(
2599
2529
  SegmentedControl2,
2600
2530
  {
2601
2531
  "aria-label": "Tree view mode",
@@ -2609,13 +2539,13 @@ function GenreTreeView({
2609
2539
  ]
2610
2540
  }
2611
2541
  ),
2612
- !isLoading && showDisplayOptions && /* @__PURE__ */ jsx30(
2542
+ !isLoading && showDisplayOptions && /* @__PURE__ */ jsx29(
2613
2543
  "div",
2614
2544
  {
2615
2545
  className: "flex items-center gap-1",
2616
2546
  role: "group",
2617
2547
  "aria-label": "Tree display options",
2618
- children: !isRotationControlled && viewMode !== "stacked" && viewMode !== "outline" && /* @__PURE__ */ jsx30(
2548
+ children: !isRotationControlled && viewMode !== "stacked" && viewMode !== "outline" && /* @__PURE__ */ jsx29(
2619
2549
  Button,
2620
2550
  {
2621
2551
  variant: allowWheelRotation ? "default" : "outline",
@@ -2626,7 +2556,7 @@ function GenreTreeView({
2626
2556
  )
2627
2557
  }
2628
2558
  ),
2629
- !isLoading && !readOnly && showAddRoot && /* @__PURE__ */ jsx30(
2559
+ !isLoading && !readOnly && showAddRoot && /* @__PURE__ */ jsx29(
2630
2560
  IconTextButton,
2631
2561
  {
2632
2562
  icon: Plus2,
@@ -2636,42 +2566,34 @@ function GenreTreeView({
2636
2566
  )
2637
2567
  ] })
2638
2568
  ] });
2639
- return /* @__PURE__ */ jsxs23(
2569
+ return /* @__PURE__ */ jsxs22(
2640
2570
  "div",
2641
2571
  {
2642
- ref: rootRef,
2572
+ "data-left-panel": isPanelShown && !isCollapsed ? void 0 : "hidden",
2643
2573
  className: ["genre-tree-view relative flex flex-col h-full", placement === "regions" && REGIONS_CLASS, className2].filter(Boolean).join(" "),
2644
2574
  children: [
2645
- /* @__PURE__ */ jsx30("style", { children: PREVIOUS_BUTTON_CSS }),
2646
- /* @__PURE__ */ jsx30(
2575
+ /* @__PURE__ */ jsx29(
2647
2576
  "div",
2648
2577
  {
2649
2578
  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
2579
  children: actions
2651
2580
  }
2652
2581
  ),
2653
- !isLoading && filterArtist && /* @__PURE__ */ jsx30(
2582
+ !isLoading && filterArtist && /* @__PURE__ */ jsx29(
2654
2583
  "div",
2655
2584
  {
2656
2585
  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
2586
  children: renderArtistFilter()
2658
2587
  }
2659
2588
  ),
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" ? (
2589
+ /* @__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: [
2590
+ !isLoading && viewMode === "outline" && floatingControlsLeading && /* @__PURE__ */ jsx29("div", { className: "absolute bottom-3 right-3 z-10", children: floatingControlsLeading }),
2591
+ isLoading ? /* @__PURE__ */ jsx29(GenreTreeViewSkeleton, { viewMode }) : viewMode === "wheel" ? (
2670
2592
  // The info panel's top edge aligns with the tree's; the search bar floats `--layout-panel-inset` below it,
2671
2593
  // and the header inset (the bar's height plus that inset) extends the header's fill behind the bar,
2672
2594
  // Google Maps style; the 8px bottom padding mirrors the inset above the bar, and the hidden title's height is zeroed.
2673
2595
  // 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(
2596
+ /* @__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
2597
  GenrePlaylistTreeWheel,
2676
2598
  {
2677
2599
  scope,
@@ -2680,29 +2602,26 @@ function GenreTreeView({
2680
2602
  handleGenreRenameAction,
2681
2603
  additionalActions,
2682
2604
  onSelectedNodeChange: handleSelectedNodeChange,
2683
- hideInfoPanelClose: true,
2684
- hideInfoPanelTitle: true,
2685
2605
  onNodeHover: handleNodeHover,
2686
- renderInfoPanelBody,
2606
+ ...hostPanel,
2687
2607
  playback,
2688
2608
  selectedNodeId,
2689
2609
  readOnly,
2690
2610
  allowWheelRotation,
2691
2611
  sizeMetric: isArtistFiltered || !canSizeByViews ? "songs" : deferredSizeMetric,
2692
2612
  busy,
2693
- floatingControlsLeading: floatingControlsLeading || canSizeByViews ? /* @__PURE__ */ jsxs23("div", { className: "flex items-center gap-2", children: [
2613
+ floatingControlsLeading: floatingControlsLeading || canSizeByViews ? /* @__PURE__ */ jsxs22("div", { className: "flex items-center gap-2", children: [
2694
2614
  floatingControlsLeading,
2695
- canSizeByViews && /* @__PURE__ */ jsx30(SizeMetricToggle, { value: sizeMetric, onChange: changeSizeMetric })
2615
+ canSizeByViews && /* @__PURE__ */ jsx29(SizeMetricToggle, { value: sizeMetric, onChange: changeSizeMetric })
2696
2616
  ] }) : void 0,
2697
2617
  defaultViewport,
2698
2618
  viewportRestore,
2699
- onViewportChange,
2700
- ...infoPanelCuration
2619
+ onViewportChange
2701
2620
  }
2702
2621
  ) }) })
2703
2622
  ) : viewMode === "pop-core" ? (
2704
2623
  // 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(
2624
+ /* @__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
2625
  GenrePlaylistTreeWheelRadialPopCore,
2707
2626
  {
2708
2627
  scope,
@@ -2711,28 +2630,25 @@ function GenreTreeView({
2711
2630
  handleGenreRenameAction,
2712
2631
  additionalActions,
2713
2632
  onSelectedNodeChange: handleSelectedNodeChange,
2714
- hideInfoPanelClose: true,
2715
- hideInfoPanelTitle: true,
2716
2633
  onNodeHover: handleNodeHover,
2717
- renderInfoPanelBody,
2634
+ ...hostPanel,
2718
2635
  playback,
2719
2636
  selectedNodeId,
2720
2637
  readOnly,
2721
2638
  allowWheelRotation,
2722
2639
  sizeMetric: isArtistFiltered || !canSizeByViews ? "songs" : deferredSizeMetric,
2723
2640
  busy,
2724
- floatingControlsLeading: floatingControlsLeading || canSizeByViews ? /* @__PURE__ */ jsxs23("div", { className: "flex items-center gap-2", children: [
2641
+ floatingControlsLeading: floatingControlsLeading || canSizeByViews ? /* @__PURE__ */ jsxs22("div", { className: "flex items-center gap-2", children: [
2725
2642
  floatingControlsLeading,
2726
- canSizeByViews && /* @__PURE__ */ jsx30(SizeMetricToggle, { value: sizeMetric, onChange: changeSizeMetric })
2643
+ canSizeByViews && /* @__PURE__ */ jsx29(SizeMetricToggle, { value: sizeMetric, onChange: changeSizeMetric })
2727
2644
  ] }) : void 0,
2728
2645
  defaultViewport,
2729
2646
  viewportRestore,
2730
2647
  onViewportChange,
2731
- ...infoPanelCuration,
2732
2648
  ...focus
2733
2649
  }
2734
2650
  ) }) })
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(
2651
+ ) : viewMode === "outline" ? /* @__PURE__ */ jsx29("div", { className: "tree-container flex-1 min-h-0 w-full overflow-y-auto relative", children: /* @__PURE__ */ jsx29(
2736
2652
  GenrePlaylistTreeWheelRadialPopCore,
2737
2653
  {
2738
2654
  outline: true,
@@ -2742,22 +2658,19 @@ function GenreTreeView({
2742
2658
  handleGenreRenameAction,
2743
2659
  additionalActions,
2744
2660
  onSelectedNodeChange: handleSelectedNodeChange,
2745
- hideInfoPanelClose: true,
2746
- hideInfoPanelTitle: true,
2747
2661
  onNodeHover: handleNodeHover,
2748
- renderInfoPanelBody,
2662
+ ...hostPanel,
2749
2663
  playback,
2750
2664
  selectedNodeId,
2751
- readOnly,
2752
- ...infoPanelCuration
2665
+ readOnly
2753
2666
  }
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(
2667
+ ) }) : /* @__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(
2668
+ ([uuid, genrePlaylistTreePerRoot]) => {
2669
+ return /* @__PURE__ */ jsx29(
2757
2670
  "div",
2758
2671
  {
2759
2672
  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(
2673
+ children: /* @__PURE__ */ jsx29("div", { className: "graph-container relative z-10 h-full", children: /* @__PURE__ */ jsx29(
2761
2674
  GenrePlaylistTreePerRoot,
2762
2675
  {
2763
2676
  scope,
@@ -2767,26 +2680,45 @@ function GenreTreeView({
2767
2680
  handleGenreRenameAction,
2768
2681
  additionalActions,
2769
2682
  onSelectedNodeChange: handleSelectedNodeChange,
2770
- hideInfoPanelClose: true,
2771
- hideInfoPanelTitle: true,
2772
2683
  onNodeHover: handleNodeHover,
2773
- renderInfoPanelBody,
2684
+ ...hostPanel,
2774
2685
  playback,
2775
2686
  selectedNodeId: uuid === selectedGenrePlaylist?.root.uuid ? selectedNodeId : null,
2776
2687
  readOnly,
2777
2688
  wheelZoom: "modifier",
2778
2689
  defaultViewport: uuid === selectedGenrePlaylist?.root.uuid ? defaultViewport : void 0,
2779
2690
  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
2691
+ onViewportChange: uuid === selectedGenrePlaylist?.root.uuid ? onViewportChange : void 0
2783
2692
  }
2784
2693
  ) })
2785
2694
  },
2786
2695
  uuid
2787
2696
  );
2788
2697
  }
2789
- ) })
2698
+ ) }),
2699
+ /* @__PURE__ */ jsx29(
2700
+ DetailPanel,
2701
+ {
2702
+ ref: panelRef,
2703
+ side: "left",
2704
+ title: activePanel?.title ?? "",
2705
+ hideTitle: activePanel?.compact,
2706
+ hideClose: activePanel?.compact,
2707
+ headerAccent: activePanel?.headerAccent,
2708
+ headerActions: expandButton,
2709
+ onClose: activePanel?.onClose ?? noop,
2710
+ collapsed: isCollapsed || !isTreeReady || !activePanel,
2711
+ expanded: activePanel?.expanded,
2712
+ onExpandedChange: activePanel?.onExpandedChange,
2713
+ aside: activePanel?.aside,
2714
+ edgeTab: detailTab,
2715
+ headerClassName: activePanel?.compact ? "pt-[calc(var(--layout-search-h)+var(--layout-panel-inset)+0.5rem)]" : void 0,
2716
+ children: activePanel?.aside && !activePanel.expanded ? /* @__PURE__ */ jsxs22(Fragment11, { children: [
2717
+ activePanel.content,
2718
+ activePanel.aside
2719
+ ] }) : activePanel?.content
2720
+ }
2721
+ )
2790
2722
  ] }) })
2791
2723
  ]
2792
2724
  }
@@ -2797,17 +2729,18 @@ function GenreTreeView({
2797
2729
  import { useMemo as useMemo8 } from "react";
2798
2730
  import {
2799
2731
  ACCENT_TEXT_COLOR,
2732
+ GenreNodeAncestors as GenreNodeAncestors2,
2733
+ GenreNodeFamily as GenreNodeFamily2,
2800
2734
  getGenreTreeColor as getGenreTreeColor2,
2801
- InfoPanel,
2802
2735
  TEXT_COLOR,
2803
2736
  tintSurface
2804
2737
  } from "@behindthemusictree/genre-tree-view";
2805
- import { Skeleton as Skeleton7 } from "@behindthemusictree/ui";
2738
+ import { DetailPanel as DetailPanel2, IconButton as IconButton2, Skeleton as Skeleton7 } from "@behindthemusictree/ui";
2739
+ import { Maximize2 as Maximize22, Minimize2 as Minimize22 } from "lucide-react";
2806
2740
  import {
2807
2741
  useFetchGenreDetail
2808
2742
  } from "@behindthemusictree/app-kit/genre-tree";
2809
- import { jsx as jsx31, jsxs as jsxs24 } from "react/jsx-runtime";
2810
- var ITEM_COUNT_LABEL = "Songs";
2743
+ import { jsx as jsx30, jsxs as jsxs23 } from "react/jsx-runtime";
2811
2744
  var RELATIVE_ITEM_COUNT = 1;
2812
2745
  function toNode(genre, parentId) {
2813
2746
  return {
@@ -2831,6 +2764,7 @@ function GenreDetailPanel({
2831
2764
  collapsed,
2832
2765
  expanded,
2833
2766
  onExpandedChange,
2767
+ edgeTab,
2834
2768
  extras
2835
2769
  }) {
2836
2770
  const { data: detail, isPending, isError } = useFetchGenreDetail(genreUuid, scope, getBackendBaseUrl);
@@ -2843,7 +2777,7 @@ function GenreDetailPanel({
2843
2777
  fill: tintSurface(rootColor),
2844
2778
  textColor: TEXT_COLOR
2845
2779
  });
2846
- const ancestors = [...detail.ascendants].sort((a, b) => b.degree - a.degree);
2780
+ const ancestors2 = [...detail.ascendants].sort((a, b) => b.degree - a.degree);
2847
2781
  return {
2848
2782
  node: {
2849
2783
  id: detail.uuid,
@@ -2855,57 +2789,72 @@ function GenreDetailPanel({
2855
2789
  fill: rootColor,
2856
2790
  parentNode: detail.parent ? relative(detail.parent, null) : null,
2857
2791
  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)
2792
+ ancestorNodes: ancestors2.map(
2793
+ ({ ascendant }, index) => relative(ascendant, index === 0 ? null : ancestors2[index - 1].ascendant.uuid)
2860
2794
  )
2861
2795
  };
2862
2796
  }, [detail]);
2863
2797
  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." });
2798
+ return /* @__PURE__ */ jsx30("div", { role: "alert", className: "p-4 text-sm text-[var(--color-text)]", children: "This genre could not be loaded." });
2865
2799
  }
2866
2800
  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" })
2801
+ return /* @__PURE__ */ jsxs23("div", { className: "flex flex-col gap-3 p-4", "aria-busy": "true", "data-testid": "genre-detail-panel-skeleton", children: [
2802
+ /* @__PURE__ */ jsx30(Skeleton7, { className: "h-6 w-2/3" }),
2803
+ /* @__PURE__ */ jsx30(Skeleton7, { className: "h-4 w-full" }),
2804
+ /* @__PURE__ */ jsx30(Skeleton7, { className: "h-4 w-3/4" })
2871
2805
  ] });
2872
2806
  }
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,
2807
+ const selectGenre = (nodeId) => onSelectGenre(nodeId);
2808
+ const family = /* @__PURE__ */ jsx30(
2809
+ GenreNodeFamily2,
2893
2810
  {
2894
2811
  node: view.node,
2895
2812
  fill: view.fill,
2896
2813
  textColor: ACCENT_TEXT_COLOR,
2897
2814
  parentNode: view.parentNode,
2898
2815
  childNodes: view.childNodes,
2899
- ancestorNodes: view.ancestorNodes,
2816
+ onSelectNode: selectGenre
2817
+ }
2818
+ );
2819
+ const ancestors = /* @__PURE__ */ jsx30(GenreNodeAncestors2, { ancestorNodes: view.ancestorNodes, onSelectNode: selectGenre });
2820
+ return /* @__PURE__ */ jsxs23(
2821
+ DetailPanel2,
2822
+ {
2900
2823
  side: "left",
2824
+ title: view.node.name,
2825
+ headerAccent: { background: view.fill, color: ACCENT_TEXT_COLOR },
2826
+ headerActions: onExpandedChange ? /* @__PURE__ */ jsx30(
2827
+ IconButton2,
2828
+ {
2829
+ "aria-label": expanded ? "Shrink panel" : "Extend panel",
2830
+ style: { color: "inherit" },
2831
+ onClick: () => onExpandedChange(!expanded),
2832
+ children: expanded ? /* @__PURE__ */ jsx30(Minimize22, { "aria-hidden": "true", className: "h-4 w-4" }) : /* @__PURE__ */ jsx30(Maximize22, { "aria-hidden": "true", className: "h-4 w-4" })
2833
+ }
2834
+ ) : void 0,
2901
2835
  onClose,
2902
- onSelectNode: (nodeId) => onSelectGenre(nodeId),
2903
- itemCountLabel: ITEM_COUNT_LABEL,
2904
- renderInfoPanelBody: renderBody,
2836
+ collapsed,
2905
2837
  expanded,
2906
2838
  onExpandedChange,
2907
- collapsed,
2908
- hideExpandButton: onExpandedChange === void 0
2839
+ edgeTab,
2840
+ aside: ancestors,
2841
+ children: [
2842
+ /* @__PURE__ */ jsx30(
2843
+ GenreInfoPanelBody,
2844
+ {
2845
+ family,
2846
+ genreUuid: detail.uuid,
2847
+ genrePlaylist: { ...detail.criteriaPlaylist, tracksCount: detail.tracksCount },
2848
+ wikidataId: detail.wikidataId,
2849
+ scope,
2850
+ getBackendBaseUrl,
2851
+ playback,
2852
+ band: { node: view.node, onZoomIn, shareUrl, shareTarget: shareUrl ? shareTarget : void 0 },
2853
+ extras
2854
+ }
2855
+ ),
2856
+ !expanded && ancestors
2857
+ ]
2909
2858
  }
2910
2859
  );
2911
2860
  }
@@ -2919,13 +2868,13 @@ import {
2919
2868
  } from "@behindthemusictree/genre-tree-view";
2920
2869
 
2921
2870
  // src/genre-tree/track-list-sidebar/TrackListSidebar.tsx
2922
- import { useEffect as useEffect7, useLayoutEffect as useLayoutEffect2, useRef as useRef5 } from "react";
2871
+ import { useEffect as useEffect6, useLayoutEffect as useLayoutEffect2, useRef as useRef5 } from "react";
2923
2872
  import { ListMusic } from "lucide-react";
2924
2873
  import { useTrackList as useTrackList6 } from "@behindthemusictree/app-kit/genre-tree";
2925
2874
  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;
2875
+ import { useTrackNoun as useTrackNoun3 } from "@behindthemusictree/app-kit/track-noun";
2876
+ import { jsx as jsx31, jsxs as jsxs24 } from "react/jsx-runtime";
2877
+ var useBrowserLayoutEffect = typeof window === "undefined" ? useEffect6 : useLayoutEffect2;
2929
2878
  function TrackListSidebar({
2930
2879
  className: className2 = "",
2931
2880
  renderDuration,
@@ -2938,7 +2887,7 @@ function TrackListSidebar({
2938
2887
  renderSongLink
2939
2888
  }) {
2940
2889
  const { trackList, loadMore, autoAdvanceScrollRequest } = useTrackList6();
2941
- const trackNoun = useTrackNoun6();
2890
+ const trackNoun = useTrackNoun3();
2942
2891
  const trackListRef = useRef5(null);
2943
2892
  const previousOriginRef = useRef5(trackList?.origin ?? null);
2944
2893
  const handledAutoAdvanceRequestIdRef = useRef5(null);
@@ -2948,7 +2897,7 @@ function TrackListSidebar({
2948
2897
  const userScrollIntentTimerRef = useRef5(null);
2949
2898
  const loadMoreSentinelRef = useRef5(null);
2950
2899
  const hasMore = !!trackList && trackList.nextPage !== null;
2951
- useEffect7(() => {
2900
+ useEffect6(() => {
2952
2901
  const sentinel = loadMoreSentinelRef.current;
2953
2902
  if (!hasMore || !sentinel) return;
2954
2903
  const observer = new IntersectionObserver((entries) => {
@@ -2968,11 +2917,11 @@ function TrackListSidebar({
2968
2917
  programmaticScrollTopRef.current = null;
2969
2918
  if (trackListRef.current) trackListRef.current.scrollTop = 0;
2970
2919
  }, [trackList?.origin]);
2971
- useEffect7(() => {
2920
+ useEffect6(() => {
2972
2921
  const list = trackListRef.current;
2973
2922
  if (!isCollapsed && list?.scrollTop === 0) shouldFollowPlaybackRef.current = true;
2974
2923
  }, [isCollapsed]);
2975
- useEffect7(
2924
+ useEffect6(
2976
2925
  () => () => {
2977
2926
  if (userScrollIntentTimerRef.current !== null) clearTimeout(userScrollIntentTimerRef.current);
2978
2927
  },
@@ -3026,25 +2975,25 @@ function TrackListSidebar({
3026
2975
  }
3027
2976
  };
3028
2977
  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(
2978
+ return trackList ? /* @__PURE__ */ jsxs24(
3030
2979
  "div",
3031
2980
  {
3032
2981
  "data-theme": "dark",
3033
2982
  className: `track-list-sidebar ${positionClasses} rounded-2xl bg-[var(--color-surface)] pb-1 ${className2}`,
3034
2983
  style: layout === "fixed" ? { top: "63.5px", maxHeight: "calc(100vh - 63.5px - 79px)" } : void 0,
3035
2984
  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 }) })
2985
+ /* @__PURE__ */ jsxs24("div", { className: "header flex h-16 items-center px-4 py-2 text-[var(--color-text)]", children: [
2986
+ /* @__PURE__ */ jsxs24("div", { className: "min-w-0 flex flex-1 flex-col justify-center", children: [
2987
+ /* @__PURE__ */ jsxs24("div", { className: "origin flex text-xl", children: [
2988
+ /* @__PURE__ */ jsx31("div", { className: "from h-auto flex flex-col justify-center items-center mr-2", children: "From" }),
2989
+ /* @__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
2990
  ] }),
3042
- /* @__PURE__ */ jsxs25("div", { className: "info flex items-center text-sm text-[var(--color-text-muted)]", children: [
2991
+ /* @__PURE__ */ jsxs24("div", { className: "info flex items-center text-sm text-[var(--color-text-muted)]", children: [
3043
2992
  trackList.origin.type === TrackListOriginType2.GENRE_PLAYLIST ? "Genre \u2022 " : trackList.origin.type === TrackListOriginType2.ARTIST ? "Artist \u2022 " : `${trackNoun.singular} playlist \u2022 `,
3044
2993
  `${trackList.total} ${trackList.total > 1 ? trackNoun.plural : trackNoun.singular}`
3045
2994
  ] })
3046
2995
  ] }),
3047
- (isCollapsed && onExpand || !isCollapsed && onMinimize) && /* @__PURE__ */ jsx32(
2996
+ (isCollapsed && onExpand || !isCollapsed && onMinimize) && /* @__PURE__ */ jsx31(
3048
2997
  "button",
3049
2998
  {
3050
2999
  type: "button",
@@ -3053,11 +3002,11 @@ function TrackListSidebar({
3053
3002
  "aria-expanded": !isCollapsed,
3054
3003
  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
3004
  onClick: isCollapsed ? onExpand : onMinimize,
3056
- children: /* @__PURE__ */ jsx32(ListMusic, { "aria-hidden": "true", className: "h-5 w-5" })
3005
+ children: /* @__PURE__ */ jsx31(ListMusic, { "aria-hidden": "true", className: "h-5 w-5" })
3057
3006
  }
3058
3007
  )
3059
3008
  ] }),
3060
- !isCollapsed && /* @__PURE__ */ jsxs25(
3009
+ !isCollapsed && /* @__PURE__ */ jsxs24(
3061
3010
  "ul",
3062
3011
  {
3063
3012
  ref: trackListRef,
@@ -3067,7 +3016,7 @@ function TrackListSidebar({
3067
3016
  onPointerDown: markUserScrollIntent,
3068
3017
  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
3018
  children: [
3070
- trackList.tracks.map((track, index) => /* @__PURE__ */ jsx32("li", { "data-track-uuid": track.uuid, children: /* @__PURE__ */ jsx32(
3019
+ trackList.tracks.map((track, index) => /* @__PURE__ */ jsx31("li", { "data-track-uuid": track.uuid, children: /* @__PURE__ */ jsx31(
3071
3020
  TrackItem,
3072
3021
  {
3073
3022
  track,
@@ -3078,7 +3027,7 @@ function TrackListSidebar({
3078
3027
  renderSongLink
3079
3028
  }
3080
3029
  ) }, track.uuid)),
3081
- hasMore && /* @__PURE__ */ jsx32("li", { ref: loadMoreSentinelRef, "aria-hidden": "true", className: "h-px" })
3030
+ hasMore && /* @__PURE__ */ jsx31("li", { ref: loadMoreSentinelRef, "aria-hidden": "true", className: "h-px" })
3082
3031
  ]
3083
3032
  }
3084
3033
  )
@@ -3088,8 +3037,8 @@ function TrackListSidebar({
3088
3037
  }
3089
3038
 
3090
3039
  // src/genre-tree/PlayerPanel.tsx
3091
- import { useEffect as useEffect8, useState as useState8 } from "react";
3092
- import { twMerge as twMerge2 } from "tailwind-merge";
3040
+ import { useEffect as useEffect7, useState as useState8 } from "react";
3041
+ import { twMerge } from "tailwind-merge";
3093
3042
  import {
3094
3043
  usePlayer as usePlayer11,
3095
3044
  PlayerVideoSurface
@@ -3097,12 +3046,13 @@ import {
3097
3046
  import {
3098
3047
  useTrackListSidebarVisibility
3099
3048
  } from "@behindthemusictree/app-kit/genre-tree";
3049
+ import { PanelEdgeTab } from "@behindthemusictree/ui";
3100
3050
 
3101
3051
  // src/genre-tree/PlayerControls.tsx
3102
3052
  import { Pause as Pause3, Play as Play4, SkipBack, SkipForward, Volume2, VolumeX } from "lucide-react";
3103
3053
  import { formatTime, useTrackList as useTrackList7 } from "@behindthemusictree/app-kit/genre-tree";
3104
3054
  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";
3055
+ import { jsx as jsx32, jsxs as jsxs25 } from "react/jsx-runtime";
3106
3056
  function PlayerControls() {
3107
3057
  const {
3108
3058
  duration,
@@ -3121,15 +3071,15 @@ function PlayerControls() {
3121
3071
  const safeDuration = Number.isFinite(duration) && duration > 0 ? duration : 0;
3122
3072
  const safeCurrentTime = Number.isFinite(currentTime) ? Math.max(0, Math.min(currentTime, safeDuration)) : 0;
3123
3073
  const isPlaying = playState === PlayStates4.PLAYING;
3124
- return /* @__PURE__ */ jsxs26(
3074
+ return /* @__PURE__ */ jsxs25(
3125
3075
  "section",
3126
3076
  {
3127
3077
  "aria-label": "Player controls",
3128
3078
  className: "player-controls flex shrink-0 flex-col gap-2 border-b border-zinc-800 px-3 py-2 text-zinc-200",
3129
3079
  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(
3080
+ /* @__PURE__ */ jsxs25("div", { className: "flex min-w-0 items-center gap-2 text-xs tabular-nums text-zinc-400", children: [
3081
+ /* @__PURE__ */ jsx32("span", { "aria-label": "Elapsed time", children: formatTime(safeCurrentTime) }),
3082
+ /* @__PURE__ */ jsx32(
3133
3083
  "input",
3134
3084
  {
3135
3085
  "aria-label": "Seek",
@@ -3143,11 +3093,11 @@ function PlayerControls() {
3143
3093
  onChange: (event) => seekTo(Number(event.currentTarget.value))
3144
3094
  }
3145
3095
  ),
3146
- /* @__PURE__ */ jsx33("span", { "aria-label": "Duration", children: formatTime(safeDuration) })
3096
+ /* @__PURE__ */ jsx32("span", { "aria-label": "Duration", children: formatTime(safeDuration) })
3147
3097
  ] }),
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(
3098
+ /* @__PURE__ */ jsxs25("div", { className: "grid min-h-9 grid-cols-[1fr_auto_1fr] items-center gap-3", children: [
3099
+ /* @__PURE__ */ jsxs25("div", { className: "col-start-2 flex items-center justify-center gap-2", children: [
3100
+ /* @__PURE__ */ jsx32(
3151
3101
  "button",
3152
3102
  {
3153
3103
  type: "button",
@@ -3156,10 +3106,10 @@ function PlayerControls() {
3156
3106
  disabled: !isReady || !hasPreviousTrack,
3157
3107
  onClick: playPreviousTrack,
3158
3108
  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" })
3109
+ children: /* @__PURE__ */ jsx32(SkipBack, { "aria-hidden": "true", className: "h-4 w-4" })
3160
3110
  }
3161
3111
  ),
3162
- /* @__PURE__ */ jsx33(
3112
+ /* @__PURE__ */ jsx32(
3163
3113
  "button",
3164
3114
  {
3165
3115
  type: "button",
@@ -3168,10 +3118,10 @@ function PlayerControls() {
3168
3118
  disabled: !isReady,
3169
3119
  onClick: handlePlayPauseAction,
3170
3120
  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" })
3121
+ 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
3122
  }
3173
3123
  ),
3174
- /* @__PURE__ */ jsx33(
3124
+ /* @__PURE__ */ jsx32(
3175
3125
  "button",
3176
3126
  {
3177
3127
  type: "button",
@@ -3180,14 +3130,14 @@ function PlayerControls() {
3180
3130
  disabled: !isReady || !hasNextTrack,
3181
3131
  onClick: playNextTrack,
3182
3132
  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" })
3133
+ children: /* @__PURE__ */ jsx32(SkipForward, { "aria-hidden": "true", className: "h-4 w-4" })
3184
3134
  }
3185
3135
  )
3186
3136
  ] }),
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(
3137
+ /* @__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: [
3138
+ 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" }),
3139
+ /* @__PURE__ */ jsx32("span", { className: "sr-only", children: "Volume" }),
3140
+ /* @__PURE__ */ jsx32(
3191
3141
  "input",
3192
3142
  {
3193
3143
  "aria-label": "Volume",
@@ -3209,7 +3159,7 @@ function PlayerControls() {
3209
3159
  }
3210
3160
 
3211
3161
  // src/genre-tree/PlayerPanel.tsx
3212
- import { jsx as jsx34, jsxs as jsxs27 } from "react/jsx-runtime";
3162
+ import { jsx as jsx33, jsxs as jsxs26 } from "react/jsx-runtime";
3213
3163
  var PANEL_ID = "player-panel";
3214
3164
  var DEFAULT_EXTENDED_WIDTH = "calc(100vw - var(--layout-center-left) - var(--layout-gutter))";
3215
3165
  function PlayerPanel({
@@ -3225,7 +3175,7 @@ function PlayerPanel({
3225
3175
  const { toggleTrackListSidebar, isTrackListSidebarVisible } = useTrackListSidebarVisibility();
3226
3176
  const [lastLoadedTrackTitle, setLastLoadedTrackTitle] = useState8("");
3227
3177
  const loadedTrackTitle = playerTrackObject?.track.title;
3228
- useEffect8(() => {
3178
+ useEffect7(() => {
3229
3179
  if (loadedTrackTitle) setLastLoadedTrackTitle(loadedTrackTitle);
3230
3180
  }, [loadedTrackTitle]);
3231
3181
  const [uncontrolledExtended, setUncontrolledExtended] = useState8(false);
@@ -3249,11 +3199,11 @@ function PlayerPanel({
3249
3199
  onClick: () => setIsExtendRequested(true),
3250
3200
  expanded: false
3251
3201
  } : null;
3252
- return /* @__PURE__ */ jsxs27(
3202
+ return /* @__PURE__ */ jsxs26(
3253
3203
  "div",
3254
3204
  {
3255
3205
  id: PANEL_ID,
3256
- className: twMerge2(
3206
+ className: twMerge(
3257
3207
  "fixed right-[var(--layout-gutter)] top-[var(--layout-band-top)] z-40 max-h-[calc(100vh-var(--layout-band-top)-3.5rem)]",
3258
3208
  !isExtended && "w-[var(--layout-right-w)]",
3259
3209
  isTrackListSidebarVisible && "h-[calc(100vh-var(--layout-band-top)-3.5rem)]",
@@ -3262,7 +3212,7 @@ function PlayerPanel({
3262
3212
  ),
3263
3213
  style: isExtended ? { width: extendedWidth } : void 0,
3264
3214
  children: [
3265
- /* @__PURE__ */ jsx34(
3215
+ /* @__PURE__ */ jsx33(
3266
3216
  PanelEdgeTab,
3267
3217
  {
3268
3218
  side: "right",
@@ -3272,30 +3222,30 @@ function PlayerPanel({
3272
3222
  className: "absolute right-full top-1/2 -translate-y-1/2"
3273
3223
  }
3274
3224
  ),
3275
- /* @__PURE__ */ jsxs27(
3225
+ /* @__PURE__ */ jsxs26(
3276
3226
  "div",
3277
3227
  {
3278
- className: twMerge2(
3228
+ className: twMerge(
3279
3229
  "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
3230
  isExtended ? "flex-row" : "flex-col"
3281
3231
  ),
3282
3232
  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 })
3233
+ /* @__PURE__ */ jsxs26("div", { className: "flex w-[var(--layout-right-w)] shrink-0 flex-col", children: [
3234
+ /* @__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 }) }),
3235
+ /* @__PURE__ */ jsxs26("div", { className: "relative w-full shrink-0", children: [
3236
+ /* @__PURE__ */ jsx33(PlayerVideoSurface, { className: "aspect-video w-full bg-black" }),
3237
+ playerTrackObject?.loadError && /* @__PURE__ */ jsx33("span", { className: "absolute bottom-2 left-2 right-2 text-xs text-red-400", children: playerTrackObject.loadError })
3288
3238
  ] }),
3289
- /* @__PURE__ */ jsx34(PlayerControls, {})
3239
+ /* @__PURE__ */ jsx33(PlayerControls, {})
3290
3240
  ] }),
3291
- /* @__PURE__ */ jsx34(
3241
+ /* @__PURE__ */ jsx33(
3292
3242
  TrackListSidebar,
3293
3243
  {
3294
3244
  layout: "inline",
3295
3245
  isCollapsed: !isTrackListSidebarVisible,
3296
3246
  onExpand: toggleTrackListSidebar,
3297
3247
  onMinimize: hideSongList,
3298
- className: twMerge2(
3248
+ className: twMerge(
3299
3249
  "min-h-0 min-w-0 flex-1",
3300
3250
  isExtended && "border-l border-zinc-800"
3301
3251
  ),
@@ -3315,11 +3265,11 @@ function PlayerPanel({
3315
3265
  import { v4 as uuidv4 } from "uuid";
3316
3266
  import { AiFillStar, AiOutlineStar } from "react-icons/ai";
3317
3267
  import { FORM_RATING_NULL_VALUE } from "@behindthemusictree/app-kit/genre-tree";
3318
- import { jsx as jsx35, jsxs as jsxs28 } from "react/jsx-runtime";
3268
+ import { jsx as jsx34, jsxs as jsxs27 } from "react/jsx-runtime";
3319
3269
  function Rating({ rating, handleChange }) {
3320
3270
  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(
3271
+ 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: [
3272
+ /* @__PURE__ */ jsx34(
3323
3273
  "input",
3324
3274
  {
3325
3275
  id: `star-${instanceId}-${i}`,
@@ -3331,16 +3281,16 @@ function Rating({ rating, handleChange }) {
3331
3281
  onChange: handleChange
3332
3282
  }
3333
3283
  ),
3334
- /* @__PURE__ */ jsx35("label", { htmlFor: `star-${instanceId}-${i}`, className: "cursor-pointer", children: i === 0 ? /* @__PURE__ */ jsxs28("div", { className: "flex", children: [
3335
- /* @__PURE__ */ jsx35(
3284
+ /* @__PURE__ */ jsx34("label", { htmlFor: `star-${instanceId}-${i}`, className: "cursor-pointer", children: i === 0 ? /* @__PURE__ */ jsxs27("div", { className: "flex", children: [
3285
+ /* @__PURE__ */ jsx34(
3336
3286
  AiOutlineStar,
3337
3287
  {
3338
3288
  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
3289
  }
3340
3290
  ),
3341
- /* @__PURE__ */ jsx35("span", { className: "flex-grow w-8" }),
3291
+ /* @__PURE__ */ jsx34("span", { className: "flex-grow w-8" }),
3342
3292
  " "
3343
- ] }) : /* @__PURE__ */ jsx35(
3293
+ ] }) : /* @__PURE__ */ jsx34(
3344
3294
  AiFillStar,
3345
3295
  {
3346
3296
  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 +3305,6 @@ export {
3355
3305
  GenreSearch,
3356
3306
  GenreNodeActionBand,
3357
3307
  GenreInfoPanelBody,
3358
- PanelEdgeTab,
3359
- useLeftDetailPanelLayout,
3360
3308
  TrackPositionPlayPause,
3361
3309
  TrackItem,
3362
3310
  GenreTreeView,
@@ -3369,4 +3317,4 @@ export {
3369
3317
  GenreTreeViewSkeleton2 as GenreTreeViewSkeleton,
3370
3318
  GenreTreeOutlineSkeleton
3371
3319
  };
3372
- //# sourceMappingURL=chunk-2HE2HDFA.js.map
3320
+ //# sourceMappingURL=chunk-AQASG5KT.js.map