@behindthemusictree/app-kit-ui 28.0.2 → 29.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.
@@ -20,76 +20,41 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
20
20
  // src/genre-tree/index.ts
21
21
  var genre_tree_exports = {};
22
22
  __export(genre_tree_exports, {
23
+ GenreDetailPanel: () => GenreDetailPanel,
24
+ GenreInfoPanelBody: () => GenreInfoPanelBody,
25
+ GenreNodeActionBand: () => GenreNodeActionBand,
23
26
  GenrePlaylistTreePerRoot: () => GenrePlaylistTreePerRoot,
24
27
  GenreSearch: () => GenreSearch,
25
- GenreTreeOutlineSkeleton: () => import_genre_tree_view5.GenreTreeOutlineSkeleton,
26
- GenreTreeSkeleton: () => import_genre_tree_view5.GenreTreeSkeleton,
28
+ GenreTreeOutlineSkeleton: () => import_genre_tree_view6.GenreTreeOutlineSkeleton,
29
+ GenreTreeSkeleton: () => import_genre_tree_view6.GenreTreeSkeleton,
27
30
  GenreTreeView: () => GenreTreeView,
28
- GenreTreeViewSkeleton: () => import_genre_tree_view5.GenreTreeViewSkeleton,
29
- GenreTreeWheelSkeleton: () => import_genre_tree_view5.GenreTreeWheelSkeleton,
31
+ GenreTreeViewSkeleton: () => import_genre_tree_view6.GenreTreeViewSkeleton,
32
+ GenreTreeWheelSkeleton: () => import_genre_tree_view6.GenreTreeWheelSkeleton,
30
33
  PanelEdgeTab: () => PanelEdgeTab,
31
34
  PlayerPanel: () => PlayerPanel,
32
35
  Rating: () => Rating,
33
36
  TrackItem: () => TrackItem,
34
37
  TrackListSidebar: () => TrackListSidebar,
35
- TrackPositionPlayPause: () => TrackPositionPlayPause
38
+ TrackPositionPlayPause: () => TrackPositionPlayPause,
39
+ useGenrePlaylistPlayback: () => useGenrePlaylistPlayback,
40
+ useLeftDetailPanelLayout: () => useLeftDetailPanelLayout
36
41
  });
37
42
  module.exports = __toCommonJS(genre_tree_exports);
38
43
 
39
44
  // src/genre-tree/GenreTreeView.tsx
40
- var import_react13 = require("react");
45
+ var import_react15 = require("react");
41
46
  var import_react_query3 = require("@tanstack/react-query");
42
47
  var import_lucide_react11 = require("lucide-react");
43
48
  var import_ui13 = require("@behindthemusictree/ui");
44
49
  var import_genre_tree_view4 = require("@behindthemusictree/genre-tree-view");
45
50
  var import_genre_tree16 = require("@behindthemusictree/app-kit/genre-tree");
46
51
  var import_genre_tree17 = require("@behindthemusictree/app-kit/genre-tree");
47
- var import_player10 = require("@behindthemusictree/app-kit/player");
48
52
  var import_popup5 = require("@behindthemusictree/app-kit/popup");
49
53
 
50
- // src/genre-tree/GenreDetailExtras.tsx
51
- var import_ui4 = require("@behindthemusictree/ui");
52
- var import_genre_tree7 = require("@behindthemusictree/app-kit/genre-tree");
53
-
54
- // src/genre-tree/ClampedText.tsx
55
- var import_react = require("react");
56
- var import_jsx_runtime = require("react/jsx-runtime");
57
- function ClampedText({ children, className: className2 = "" }) {
58
- const ref = (0, import_react.useRef)(null);
59
- const [expanded, setExpanded] = (0, import_react.useState)(false);
60
- const [overflows, setOverflows] = (0, import_react.useState)(false);
61
- (0, import_react.useLayoutEffect)(() => {
62
- const element = ref.current;
63
- if (!element || expanded) return;
64
- setOverflows(element.scrollHeight > element.clientHeight);
65
- }, [children, expanded]);
66
- return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_jsx_runtime.Fragment, { children: [
67
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)("p", { ref, className: `${className2} ${expanded ? "" : "line-clamp-3"}`, children }),
68
- (overflows || expanded) && /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
69
- "button",
70
- {
71
- type: "button",
72
- "aria-expanded": expanded,
73
- onClick: () => setExpanded((value) => !value),
74
- className: "mt-1 text-xs text-[var(--color-text-muted)] underline hover:text-[var(--color-text)]",
75
- children: expanded ? "Show less" : "Show more"
76
- }
77
- )
78
- ] });
79
- }
80
-
81
- // src/genre-tree/GenrePlaylistTracks.tsx
54
+ // src/genre-tree/playlist-tree/TreePerRoot.tsx
82
55
  var import_react3 = require("react");
83
- var import_react_query = require("@tanstack/react-query");
84
- var import_zod = require("zod");
85
- var import_fa = require("react-icons/fa");
86
- var import_ui3 = require("@behindthemusictree/ui");
87
- var import_auth = require("@behindthemusictree/app-kit/auth");
88
- var import_transport = require("@behindthemusictree/app-kit/transport");
89
- var import_transport2 = require("@behindthemusictree/app-kit/transport");
90
- var import_transport3 = require("@behindthemusictree/app-kit/transport");
56
+ var import_genre_tree_view = require("@behindthemusictree/genre-tree-view");
91
57
  var import_player = require("@behindthemusictree/app-kit/player");
92
- var import_player2 = require("@behindthemusictree/app-kit/player");
93
58
  var import_popup = require("@behindthemusictree/app-kit/popup");
94
59
 
95
60
  // src/popup/InternalErrorPopup.tsx
@@ -97,12 +62,12 @@ var import_lucide_react2 = require("lucide-react");
97
62
 
98
63
  // src/popup/BasePopup.tsx
99
64
  var import_react_dom = require("react-dom");
100
- var import_react2 = require("react");
65
+ var import_react = require("react");
101
66
 
102
67
  // src/popup/PopupTitle.tsx
103
68
  var import_lucide_react = require("lucide-react");
104
69
  var import_ui = require("@behindthemusictree/ui");
105
- var import_jsx_runtime2 = require("react/jsx-runtime");
70
+ var import_jsx_runtime = require("react/jsx-runtime");
106
71
  function PopupTitle({
107
72
  title,
108
73
  onClose,
@@ -112,37 +77,37 @@ function PopupTitle({
112
77
  titleClassName = "",
113
78
  closeButtonClassName = ""
114
79
  }) {
115
- return /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("div", { className: `popup-title flex items-center justify-between bg-black p-4 rounded-t-lg ${className2}`, children: [
116
- /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("div", { className: "flex items-center gap-2", children: [
117
- Icon && /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(Icon, { className: "h-5 w-5 text-white" }),
118
- /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("h3", { id: "popup-title", className: `text-white font-semibold ${titleClassName}`, children: title })
80
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: `popup-title flex items-center justify-between bg-black p-4 rounded-t-lg ${className2}`, children: [
81
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "flex items-center gap-2", children: [
82
+ Icon && /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Icon, { className: "h-5 w-5 text-white" }),
83
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("h3", { id: "popup-title", className: `text-white font-semibold ${titleClassName}`, children: title })
119
84
  ] }),
120
- isDismissable && /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
85
+ isDismissable && /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
121
86
  import_ui.Button,
122
87
  {
123
88
  onClick: onClose,
124
89
  className: `h-8 w-8 text-white hover:text-white/80 ${closeButtonClassName}`,
125
90
  "aria-label": "Close popup",
126
- children: /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(import_lucide_react.X, { className: "h-4 w-4" })
91
+ children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_lucide_react.X, { className: "h-4 w-4" })
127
92
  }
128
93
  )
129
94
  ] });
130
95
  }
131
96
 
132
97
  // src/popup/PopupButtons.tsx
133
- var import_jsx_runtime3 = require("react/jsx-runtime");
98
+ var import_jsx_runtime2 = require("react/jsx-runtime");
134
99
  function PopupButtons({ children, alignment = "center", className: className2 = "" }) {
135
100
  const alignmentClasses = {
136
101
  left: "justify-start",
137
102
  center: "justify-center",
138
103
  right: "justify-end"
139
104
  };
140
- return /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("div", { className: `flex gap-3 ${alignmentClasses[alignment]} ${className2}`, children });
105
+ return /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("div", { className: `flex gap-3 ${alignmentClasses[alignment]} ${className2}`, children });
141
106
  }
142
107
 
143
108
  // src/popup/BasePopup.tsx
144
109
  var import_ui2 = require("@behindthemusictree/ui");
145
- var import_jsx_runtime4 = require("react/jsx-runtime");
110
+ var import_jsx_runtime3 = require("react/jsx-runtime");
146
111
  var typeClasses = {
147
112
  default: "w-full max-w-lg",
148
113
  success: "w-full max-w-lg",
@@ -184,8 +149,8 @@ function BasePopup(props) {
184
149
  closeOnEscape = false,
185
150
  closeOnBackdrop = false
186
151
  } = props;
187
- const dialogRef = (0, import_react2.useRef)(null);
188
- (0, import_react2.useEffect)(() => {
152
+ const dialogRef = (0, import_react.useRef)(null);
153
+ (0, import_react.useEffect)(() => {
189
154
  const previousFocus = document.activeElement instanceof HTMLElement ? document.activeElement : null;
190
155
  const dialog = dialogRef.current;
191
156
  const focusableSelector = 'a[href], button:not([disabled]), input:not([disabled]), select:not([disabled]), textarea:not([disabled]), [tabindex]:not([tabindex="-1"])';
@@ -225,14 +190,14 @@ function BasePopup(props) {
225
190
  const baseClasses = "bg-white rounded-lg";
226
191
  const renderButtons = () => {
227
192
  if (!showOkButton && !showCancelButton) return null;
228
- return /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("div", { className: "mt-4 pt-4 border-t", children: /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)(PopupButtons, { alignment: buttonAlignment, children: [
229
- showCancelButton && /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(import_ui2.Button, { onClick: onCancel || onClose || (() => {
193
+ return /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("div", { className: "mt-4 pt-4 border-t", children: /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(PopupButtons, { alignment: buttonAlignment, children: [
194
+ showCancelButton && /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(import_ui2.Button, { onClick: onCancel || onClose || (() => {
230
195
  }), variant: "secondary", children: cancelButtonText }),
231
- showOkButton && /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(import_ui2.Button, { onClick: onOk || onClose || (() => {
196
+ showOkButton && /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(import_ui2.Button, { onClick: onOk || onClose || (() => {
232
197
  }), disabled: okButtonDisabled, children: okButtonText })
233
198
  ] }) });
234
199
  };
235
- const popupComponent = /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
200
+ const popupComponent = /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
236
201
  "div",
237
202
  {
238
203
  className: `fixed z-50 flex items-center justify-center bg-transparent ${overlayClassName}`,
@@ -246,7 +211,7 @@ function BasePopup(props) {
246
211
  "aria-modal": "true",
247
212
  "aria-labelledby": "popup-title",
248
213
  onClick: closeOnBackdrop && isDismissable ? onClose : void 0,
249
- children: /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)(
214
+ children: /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(
250
215
  "div",
251
216
  {
252
217
  ref: dialogRef,
@@ -254,7 +219,7 @@ function BasePopup(props) {
254
219
  className: `${baseClasses} ${fitContent ? typeClasses[type].replace("w-full", "w-fit") : typeClasses[type]} ${className2 || ""}`,
255
220
  onClick: (e) => e.stopPropagation(),
256
221
  children: [
257
- /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
222
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
258
223
  PopupTitle,
259
224
  {
260
225
  title,
@@ -267,7 +232,7 @@ function BasePopup(props) {
267
232
  closeButtonClassName
268
233
  }
269
234
  ),
270
- /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("div", { className: `popup-content rounded-b-lg p-4 ${contentBgClasses[type] ?? ""} ${contentClassName}`, children: [
235
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { className: `popup-content rounded-b-lg p-4 ${contentBgClasses[type] ?? ""} ${contentClassName}`, children: [
271
236
  children,
272
237
  renderButtons()
273
238
  ] })
@@ -281,23 +246,23 @@ function BasePopup(props) {
281
246
  var BasePopup_default = BasePopup;
282
247
 
283
248
  // src/popup/InternalErrorPopup.tsx
284
- var import_jsx_runtime5 = require("react/jsx-runtime");
249
+ var import_jsx_runtime4 = require("react/jsx-runtime");
285
250
  function InternalErrorPopup({ errorCode, contactEmail, ...rest }) {
286
- return /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(
251
+ return /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
287
252
  BasePopup,
288
253
  {
289
254
  ...rest,
290
255
  title: "Internal Error",
291
256
  isDismissable: false,
292
257
  icon: import_lucide_react2.AlertTriangle,
293
- children: /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)("div", { className: "flex flex-col items-center space-y-6 py-4", children: [
294
- /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(import_lucide_react2.AlertCircle, { className: "h-16 w-16 text-[var(--color-red-500)]", strokeWidth: 1.5 }),
295
- /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("div", { children: /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)("p", { className: "text-center text-[var(--color-neutral-600)]", children: [
258
+ children: /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("div", { className: "flex flex-col items-center space-y-6 py-4", children: [
259
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(import_lucide_react2.AlertCircle, { className: "h-16 w-16 text-[var(--color-red-500)]", strokeWidth: 1.5 }),
260
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("div", { children: /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("p", { className: "text-center text-[var(--color-neutral-600)]", children: [
296
261
  "Please try again. If the problem persists, contact us at",
297
262
  " ",
298
- /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("a", { href: `mailto:${contactEmail}`, className: "text-[var(--color-primary)] hover:text-[var(--color-primary-hover)]", children: contactEmail })
263
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("a", { href: `mailto:${contactEmail}`, className: "text-[var(--color-primary)] hover:text-[var(--color-primary-hover)]", children: contactEmail })
299
264
  ] }) }),
300
- errorCode && /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)("div", { className: "text-sm text-[var(--color-neutral-500)] bg-[var(--color-neutral-100)] p-3 rounded-lg border border-[var(--color-neutral-100)]", children: [
265
+ errorCode && /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("div", { className: "text-sm text-[var(--color-neutral-500)] bg-[var(--color-neutral-100)] p-3 rounded-lg border border-[var(--color-neutral-100)]", children: [
301
266
  "Error Code: ",
302
267
  errorCode
303
268
  ] })
@@ -306,377 +271,96 @@ function InternalErrorPopup({ errorCode, contactEmail, ...rest }) {
306
271
  );
307
272
  }
308
273
 
309
- // src/genre-tree/GenrePlaylistTracks.tsx
310
- var import_transport4 = require("@behindthemusictree/app-kit/transport");
311
- var import_genre_tree2 = require("@behindthemusictree/app-kit/genre-tree");
312
- var import_genre_tree3 = require("@behindthemusictree/app-kit/genre-tree");
313
- var import_genre_tree4 = require("@behindthemusictree/app-kit/genre-tree");
314
- var import_genre_tree5 = require("@behindthemusictree/app-kit/genre-tree");
274
+ // src/genre-tree/playlist-tree/TreePerRoot.tsx
275
+ var import_transport = require("@behindthemusictree/app-kit/transport");
315
276
 
316
- // src/genre-tree/MusicBrainzRecordingLink.tsx
277
+ // src/genre-tree/playlist-tree/useGenrePlaylistPlayback.ts
278
+ var import_react2 = require("react");
317
279
  var import_genre_tree = require("@behindthemusictree/app-kit/genre-tree");
318
- var import_jsx_runtime6 = require("react/jsx-runtime");
319
- function MusicBrainzRecordingLink({
320
- song,
321
- children,
322
- className: className2 = ""
280
+ function useGenrePlaylistPlayback({
281
+ scope,
282
+ genrePlaylists,
283
+ artist = null
323
284
  }) {
324
- const url = (0, import_genre_tree.musicBrainzRecordingUrl)(song);
325
- if (!url) return /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(import_jsx_runtime6.Fragment, { children });
326
- return /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(
327
- "a",
328
- {
329
- href: url,
330
- target: "_blank",
331
- rel: "noopener noreferrer",
332
- "aria-label": `View ${song.title} on MusicBrainz`,
333
- className: className2,
334
- onClick: (event) => event.stopPropagation(),
335
- children
336
- }
285
+ const { trackList, playNewTrackListFromGenrePlaylist, playNewTrackListFromArtist } = (0, import_genre_tree.useTrackList)();
286
+ const origin = trackList?.origin;
287
+ const playingNodeId = (0, import_react2.useMemo)(() => {
288
+ if (!origin) return null;
289
+ if (!artist) return origin.type === import_genre_tree.TrackListOriginType.GENRE_PLAYLIST ? origin.uuid : null;
290
+ if (origin.type !== import_genre_tree.TrackListOriginType.ARTIST || origin.uuid !== artist.uuid) return null;
291
+ const { genreUuid } = origin;
292
+ return genreUuid ? genrePlaylists.find((gp) => gp.criteria?.uuid === genreUuid)?.uuid ?? null : null;
293
+ }, [origin, artist, genrePlaylists]);
294
+ const play = (0, import_react2.useCallback)(
295
+ async (genrePlaylist) => {
296
+ if (!artist) return playNewTrackListFromGenrePlaylist(genrePlaylist, scope);
297
+ if (!genrePlaylist.criteria) return;
298
+ return playNewTrackListFromArtist(artist, scope, { ordering: "title", genreUuid: genrePlaylist.criteria.uuid });
299
+ },
300
+ [artist, scope, playNewTrackListFromGenrePlaylist, playNewTrackListFromArtist]
337
301
  );
302
+ return (0, import_react2.useMemo)(() => ({ playingNodeId, play }), [playingNodeId, play]);
338
303
  }
339
304
 
340
- // src/genre-tree/GenrePlaylistTracks.tsx
341
- var import_genre_tree6 = require("@behindthemusictree/app-kit/genre-tree");
305
+ // src/genre-tree/playlist-tree/TreePerRoot.tsx
342
306
  var import_track_noun = require("@behindthemusictree/app-kit/track-noun");
343
- var import_jsx_runtime7 = require("react/jsx-runtime");
344
- var CATALOGUE_PAGE_SIZE = 100;
345
- var SongPageSchema = (0, import_transport2.PaginatedResponseSchema)(import_genre_tree4.SongDetailedSchema).extend({
346
- overallTotal: import_zod.z.number().nullable(),
347
- page: import_zod.z.number().nullable(),
348
- totalPages: import_zod.z.number().nullable()
349
- });
350
- var TEXT_CELL = "min-w-0 flex-1 truncate [@container(min-width:40rem)]:flex [@container(min-width:40rem)]:gap-2";
351
- var TITLE_CELL = "[@container(min-width:40rem)]:flex-[2] [@container(min-width:40rem)]:truncate";
352
- var COLUMN_CELL = "[@container(min-width:40rem)]:flex-1 [@container(min-width:40rem)]:truncate";
353
- var ALBUM_CELL = `hidden [@container(min-width:40rem)]:block ${COLUMN_CELL}`;
354
- var SEPARATOR = "[@container(min-width:40rem)]:hidden";
355
- var nextCursor = (next) => {
356
- const cursor = new URL(next).searchParams.get("cursor");
357
- if (!cursor) throw new Error(`Songs next link has no cursor: ${next}`);
358
- return cursor;
359
- };
360
- function GenrePlaylistTracks({
361
- genreUuid,
362
- genrePlaylist,
307
+ var import_jsx_runtime5 = require("react/jsx-runtime");
308
+ function GenrePlaylistTreePerRoot({
363
309
  scope,
364
- getBackendBaseUrl,
365
- renderArtistLink,
366
- renderSongLeading,
367
- renderSongActions,
368
- renderTracksToolbar,
369
- artist = null
310
+ className: className2,
311
+ rootUuid,
312
+ genrePlaylistTreePerRoot,
313
+ handleGenreCreationAction,
314
+ handleGenreRenameAction,
315
+ additionalActions,
316
+ onNodeClick,
317
+ onNodeHover,
318
+ renderInfoPanelBody,
319
+ selectedNodeId,
320
+ onSelectedNodeChange,
321
+ hideInfoPanelClose,
322
+ hideInfoPanelTitle,
323
+ readOnly = false,
324
+ showToolbar,
325
+ wheelZoom,
326
+ defaultViewport,
327
+ viewportRestore,
328
+ onViewportChange,
329
+ playback,
330
+ ...infoPanelCuration
370
331
  }) {
371
- const { fetchGenrePlaylistTracksPage, playNewTrackListFromGenrePlaylist, playNewTrackListFromArtist } = (0, import_genre_tree5.useTrackList)();
372
- const { session, sessionRestored } = (0, import_auth.useSession)();
373
- const { playerTrackObject, playState, handlePlayPauseAction } = (0, import_player.usePlayer)();
374
- const { showPopup } = (0, import_popup.usePopup)();
332
+ const { isPlaying, setIsPlaying } = (0, import_player.usePlayer)();
333
+ const defaultPlayback = useGenrePlaylistPlayback({ scope, genrePlaylists: genrePlaylistTreePerRoot });
334
+ const { playingNodeId, play } = playback ?? defaultPlayback;
375
335
  const trackNoun = (0, import_track_noun.useTrackNoun)();
376
- const { uuid } = genrePlaylist;
377
- const { fetch } = (0, import_transport.useFetchWrapper)(getBackendBaseUrl);
378
- const { data, isPending, isError, refetch, hasNextPage, isFetchingNextPage, fetchNextPage } = (0, import_react_query.useInfiniteQuery)({
379
- queryKey: scope === "me" ? import_genre_tree2.genrePlaylistQueryKeys.me.tracks(uuid) : artist ? [...import_genre_tree3.libraryQueryKeys.reference.songs.byGenre(getBackendBaseUrl(), genreUuid), "artist", artist.uuid] : import_genre_tree3.libraryQueryKeys.reference.songs.byGenre(getBackendBaseUrl(), genreUuid),
380
- queryFn: async ({ pageParam }) => {
381
- if (scope === "me") return fetchGenrePlaylistTracksPage(uuid, scope, Number(pageParam));
382
- const response = await fetch(
383
- import_genre_tree3.libraryEndpoints.reference.songs.list(),
384
- true,
385
- false,
386
- {},
387
- { genre: genreUuid, ...artist && { artist: artist.uuid }, pageSize: CATALOGUE_PAGE_SIZE, withTotal: false, ...pageParam && { cursor: pageParam } }
388
- );
389
- const parsed = (0, import_transport3.parseWithLog)(SongPageSchema, response, "GenrePlaylistTracks");
390
- return { tracks: parsed.results, total: parsed.overallTotal, nextPage: parsed.next ? nextCursor(parsed.next) : null };
391
- },
392
- initialPageParam: scope === "me" ? 1 : "",
393
- getNextPageParam: (lastPage) => lastPage.nextPage ?? void 0,
394
- enabled: scope !== "me" || sessionRestored && !!session?.accessToken
395
- });
396
- const sentinelRef = (0, import_react3.useRef)(null);
397
- (0, import_react3.useEffect)(() => {
398
- const sentinel = sentinelRef.current;
399
- if (!hasNextPage || isFetchingNextPage || !sentinel) return;
400
- const observer = new IntersectionObserver((entries) => {
401
- if (entries.some((entry) => entry.isIntersecting)) {
402
- void fetchNextPage();
403
- }
404
- });
405
- observer.observe(sentinel);
406
- return () => observer.disconnect();
407
- }, [hasNextPage, isFetchingNextPage, fetchNextPage]);
408
- const pages = data?.pages ?? [];
409
- const tracks = pages.flatMap((page) => page.tracks);
410
- const total = pages[0]?.total ?? void 0;
411
- const play = (startSongUuid) => {
412
- const started = artist ? playNewTrackListFromArtist(artist, scope, { startSongUuid, ordering: "title", genreUuid }) : playNewTrackListFromGenrePlaylist(genrePlaylist, scope, { startSongUuid });
413
- started.catch((error) => {
414
- console.error("Failed to play genre playlist:", error);
415
- showPopup(/* @__PURE__ */ (0, import_jsx_runtime7.jsx)(InternalErrorPopup, { errorCode: import_transport4.ErrorCode.CLIENT_UNKNOWN }));
416
- });
417
- };
418
- return /* @__PURE__ */ (0, import_jsx_runtime7.jsxs)("div", { className: "gtv-info-panel-children", children: [
419
- /* @__PURE__ */ (0, import_jsx_runtime7.jsxs)("span", { className: "gtv-info-panel-children-title", children: [
420
- (0, import_track_noun.capitalize)(trackNoun.plural),
421
- total !== void 0 && ` (${total})`
422
- ] }),
423
- renderTracksToolbar?.(),
424
- isPending ? /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(import_ui3.Skeleton, { className: "h-4 w-3/4", "aria-busy": "true" }) : isError && tracks.length === 0 ? /* @__PURE__ */ (0, import_jsx_runtime7.jsxs)("p", { role: "alert", children: [
425
- "Couldn't load ",
426
- trackNoun.plural,
427
- ".",
428
- " ",
429
- /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("button", { type: "button", className: "underline", onClick: () => void refetch(), children: "Retry" })
430
- ] }) : tracks.length === 0 ? /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("p", { children: "\u2014" }) : /* @__PURE__ */ (0, import_jsx_runtime7.jsxs)("ul", { className: "mt-1 max-h-64 overflow-y-auto text-[0.8125rem] leading-[1.45] [container-type:inline-size]", "aria-label": `${genrePlaylist.name} ${trackNoun.plural}`, children: [
431
- tracks.map((track, index) => {
432
- const label = track.artists?.length ? `${track.title} \u2014 ${track.artists.map((artist2) => artist2.name).join(", ")}` : track.title;
433
- const isCurrent = playerTrackObject?.track.id === track.uuid;
434
- const isPlaying = isCurrent && playState === import_player2.PlayStates.PLAYING;
435
- const text = /* @__PURE__ */ (0, import_jsx_runtime7.jsxs)("span", { className: TEXT_CELL, children: [
436
- /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("span", { className: TITLE_CELL, children: (0, import_genre_tree4.isSong)(track) ? /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(MusicBrainzRecordingLink, { song: track, className: "hover:underline focus-visible:underline", children: track.title }) : track.title }),
437
- track.artists?.length ? /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("span", { className: SEPARATOR, children: " \u2014 " }) : null,
438
- /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("span", { className: COLUMN_CELL, children: track.artists?.length ? /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(import_genre_tree6.ArtistNames, { artists: track.artists, renderArtistLink }) : null }),
439
- track.album && /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("span", { className: ALBUM_CELL, children: track.album.name })
440
- ] });
441
- const leading = renderSongLeading?.(track, index);
442
- const actions = renderSongActions?.(track);
443
- const isUnplayable = (0, import_genre_tree4.isSong)(track) && (0, import_genre_tree4.songUnplayableReason)(track) !== null;
444
- if (isUnplayable) {
445
- return /* @__PURE__ */ (0, import_jsx_runtime7.jsxs)("li", { className: "flex min-h-8 items-center gap-1 px-1 opacity-50", children: [
446
- leading,
447
- /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(
448
- "button",
449
- {
450
- type: "button",
451
- disabled: true,
452
- "aria-disabled": "true",
453
- "aria-label": label,
454
- className: "flex h-6 w-6 shrink-0 items-center justify-center rounded-full opacity-0 cursor-not-allowed",
455
- children: /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(import_fa.FaPlay, { size: 10 })
456
- }
457
- ),
458
- text,
459
- actions
460
- ] }, track.uuid);
461
- }
462
- return /* @__PURE__ */ (0, import_jsx_runtime7.jsxs)(
463
- "li",
464
- {
465
- className: `group flex min-h-8 items-center gap-1 rounded px-1 hover:bg-[var(--color-neutral-100)] ${isCurrent ? "font-medium text-[var(--color-primary)]" : ""}`,
466
- children: [
467
- leading,
468
- /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(
469
- "button",
470
- {
471
- type: "button",
472
- "aria-label": `${isPlaying ? "Pause" : "Play"} ${label}`,
473
- className: `flex h-6 w-6 shrink-0 items-center justify-center rounded-full cursor-pointer group-hover:opacity-100 focus-visible:opacity-100 ${isCurrent && playState !== import_player2.PlayStates.STOPPED ? "opacity-100" : "opacity-0 [@media(pointer:coarse)]:opacity-100"}`,
474
- onClick: () => isCurrent ? handlePlayPauseAction() : play(track.uuid),
475
- children: isCurrent && playState === import_player2.PlayStates.LOADING ? /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(import_ui3.RingLoader, { size: 10 }) : isPlaying ? /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(import_fa.FaPause, { size: 10 }) : /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(import_fa.FaPlay, { size: 10 })
476
- }
477
- ),
478
- text,
479
- actions
480
- ]
481
- },
482
- track.uuid
483
- );
484
- }),
485
- hasNextPage && /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("li", { ref: sentinelRef, "aria-hidden": "true", className: "h-4" })
486
- ] })
487
- ] });
488
- }
489
-
490
- // src/genre-tree/GenreDetailExtras.tsx
491
- var import_track_noun2 = require("@behindthemusictree/app-kit/track-noun");
492
- var import_si = require("react-icons/si");
493
- var import_jsx_runtime8 = require("react/jsx-runtime");
494
- function GenreDetailExtras({
495
- genreUuid,
496
- genrePlaylist,
497
- wikidataId,
498
- scope,
499
- getBackendBaseUrl,
500
- criteriaOverviewSchema,
501
- renderGenreDetailExtras,
502
- renderArtistLink,
503
- renderSummary,
504
- renderEssentialTracks,
505
- family,
506
- ...trackSlots
507
- }) {
508
- const trackNoun = (0, import_track_noun2.useTrackNoun)();
509
- const { data: overview, isPending } = (0, import_genre_tree7.useFetchGenreOverview)(
510
- genreUuid,
511
- scope,
512
- getBackendBaseUrl,
513
- criteriaOverviewSchema
514
- );
515
- if (isPending) {
516
- return /* @__PURE__ */ (0, import_jsx_runtime8.jsxs)(import_jsx_runtime8.Fragment, { children: [
517
- /* @__PURE__ */ (0, import_jsx_runtime8.jsxs)("div", { className: "gtv-info-panel-children", "aria-busy": "true", "data-testid": "genre-detail-extras-skeleton", children: [
518
- /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("span", { className: "gtv-info-panel-children-title", children: "Summary" }),
519
- /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(import_ui4.Skeleton, { className: "h-4 w-3/4" })
520
- ] }),
521
- family
522
- ] });
523
- }
524
- if (!overview) return /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(import_jsx_runtime8.Fragment, { children: family });
525
- const { summary, summarySourceUrl, essentialSongs } = overview;
526
- const wikidataUrl = wikidataId ? `https://www.wikidata.org/wiki/${encodeURIComponent(wikidataId)}` : null;
527
- return /* @__PURE__ */ (0, import_jsx_runtime8.jsxs)(import_jsx_runtime8.Fragment, { children: [
528
- (summarySourceUrl || wikidataUrl) && /* @__PURE__ */ (0, import_jsx_runtime8.jsxs)("div", { className: "gtv-info-panel-children text-[0.8125rem] leading-[1.45] text-[var(--color-text)]", children: [
529
- /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("span", { className: "gtv-info-panel-children-title", children: "Links" }),
530
- /* @__PURE__ */ (0, import_jsx_runtime8.jsxs)("div", { className: "mt-1 flex flex-wrap gap-x-3 gap-y-1 text-xs", children: [
531
- summarySourceUrl && /* @__PURE__ */ (0, import_jsx_runtime8.jsxs)(
532
- "a",
533
- {
534
- href: summarySourceUrl,
535
- target: "_blank",
536
- rel: "noopener noreferrer",
537
- className: "inline-flex items-center gap-1.5 text-[var(--color-text-muted)] hover:text-[var(--color-text)]",
538
- children: [
539
- /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(import_si.SiWikipedia, { "aria-hidden": "true", className: "h-3.5 w-3.5" }),
540
- "Wikipedia"
541
- ]
542
- }
543
- ),
544
- wikidataUrl && /* @__PURE__ */ (0, import_jsx_runtime8.jsxs)(
545
- "a",
546
- {
547
- href: wikidataUrl,
548
- target: "_blank",
549
- rel: "noopener noreferrer",
550
- className: "inline-flex items-center gap-1.5 text-[var(--color-text-muted)] hover:text-[var(--color-text)]",
551
- children: [
552
- /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(import_si.SiWikidata, { "aria-hidden": "true", className: "h-3.5 w-3.5" }),
553
- "Wikidata"
554
- ]
555
- }
556
- )
557
- ] })
558
- ] }),
559
- renderSummary ? renderSummary(overview) : /* @__PURE__ */ (0, import_jsx_runtime8.jsxs)("div", { className: "gtv-info-panel-children text-[0.8125rem] leading-[1.45] text-[var(--color-text)]", children: [
560
- /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("span", { className: "gtv-info-panel-children-title", children: "Summary" }),
561
- /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(ClampedText, { className: "mt-1", children: summary ?? "\u2014" }, genreUuid)
562
- ] }),
563
- family,
564
- renderGenreDetailExtras?.(overview),
565
- renderEssentialTracks ? renderEssentialTracks(overview) : essentialSongs && /* @__PURE__ */ (0, import_jsx_runtime8.jsxs)("div", { className: "gtv-info-panel-children text-[0.8125rem] leading-[1.45] text-[var(--color-text)]", children: [
566
- /* @__PURE__ */ (0, import_jsx_runtime8.jsxs)("span", { className: "gtv-info-panel-children-title", children: [
567
- "Essential ",
568
- trackNoun.plural
569
- ] }),
570
- essentialSongs.length > 0 ? /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("ul", { className: "mt-1 divide-y divide-[var(--color-border)]", children: essentialSongs.map((track) => /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("li", { className: "py-1", children: track.title }, track.uuid)) }) : /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("p", { className: "mt-1", children: "\u2014" })
571
- ] }),
572
- /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(
573
- GenrePlaylistTracks,
574
- {
575
- genreUuid,
576
- genrePlaylist,
577
- scope,
578
- getBackendBaseUrl,
579
- renderArtistLink,
580
- ...trackSlots
581
- }
582
- )
583
- ] });
584
- }
585
-
586
- // src/genre-tree/playlist-tree/TreePerRoot.tsx
587
- var import_react5 = require("react");
588
- var import_genre_tree_view = require("@behindthemusictree/genre-tree-view");
589
- var import_player3 = require("@behindthemusictree/app-kit/player");
590
- var import_popup2 = require("@behindthemusictree/app-kit/popup");
591
- var import_transport5 = require("@behindthemusictree/app-kit/transport");
592
-
593
- // src/genre-tree/playlist-tree/useGenrePlaylistPlayback.ts
594
- var import_react4 = require("react");
595
- var import_genre_tree8 = require("@behindthemusictree/app-kit/genre-tree");
596
- function useGenrePlaylistPlayback({
597
- scope,
598
- genrePlaylists,
599
- artist = null
600
- }) {
601
- const { trackList, playNewTrackListFromGenrePlaylist, playNewTrackListFromArtist } = (0, import_genre_tree8.useTrackList)();
602
- const origin = trackList?.origin;
603
- const playingNodeId = (0, import_react4.useMemo)(() => {
604
- if (!origin) return null;
605
- if (!artist) return origin.type === import_genre_tree8.TrackListOriginType.GENRE_PLAYLIST ? origin.uuid : null;
606
- if (origin.type !== import_genre_tree8.TrackListOriginType.ARTIST || origin.uuid !== artist.uuid) return null;
607
- const { genreUuid } = origin;
608
- return genreUuid ? genrePlaylists.find((gp) => gp.criteria?.uuid === genreUuid)?.uuid ?? null : null;
609
- }, [origin, artist, genrePlaylists]);
610
- const play = (0, import_react4.useCallback)(
611
- async (genrePlaylist) => {
612
- if (!artist) return playNewTrackListFromGenrePlaylist(genrePlaylist, scope);
613
- if (!genrePlaylist.criteria) return;
614
- return playNewTrackListFromArtist(artist, scope, { ordering: "title", genreUuid: genrePlaylist.criteria.uuid });
615
- },
616
- [artist, scope, playNewTrackListFromGenrePlaylist, playNewTrackListFromArtist]
617
- );
618
- return (0, import_react4.useMemo)(() => ({ playingNodeId, play }), [playingNodeId, play]);
619
- }
620
-
621
- // src/genre-tree/playlist-tree/TreePerRoot.tsx
622
- var import_track_noun3 = require("@behindthemusictree/app-kit/track-noun");
623
- var import_jsx_runtime9 = require("react/jsx-runtime");
624
- function GenrePlaylistTreePerRoot({
625
- scope,
626
- className: className2,
627
- rootUuid,
628
- genrePlaylistTreePerRoot,
629
- handleGenreCreationAction,
630
- handleGenreRenameAction,
631
- additionalActions,
632
- onNodeClick,
633
- onNodeHover,
634
- renderInfoPanelBody,
635
- selectedNodeId,
636
- onSelectedNodeChange,
637
- hideInfoPanelClose,
638
- hideInfoPanelTitle,
639
- readOnly = false,
640
- showToolbar,
641
- wheelZoom,
642
- controlsOrientation,
643
- defaultViewport,
644
- onViewportChange,
645
- playback,
646
- ...infoPanelCuration
647
- }) {
648
- const { isPlaying, setIsPlaying } = (0, import_player3.usePlayer)();
649
- const defaultPlayback = useGenrePlaylistPlayback({ scope, genrePlaylists: genrePlaylistTreePerRoot });
650
- const { playingNodeId, play } = playback ?? defaultPlayback;
651
- const trackNoun = (0, import_track_noun3.useTrackNoun)();
652
- const { showPopup } = (0, import_popup2.usePopup)();
653
- const nodes = (0, import_react5.useMemo)(
654
- () => genrePlaylistTreePerRoot.map((genrePlaylist) => ({
655
- id: genrePlaylist.uuid,
656
- parentId: genrePlaylist.parent?.uuid ?? null,
657
- name: genrePlaylist.name,
658
- itemCount: genrePlaylist.tracksCount,
659
- actionable: Boolean(genrePlaylist.criteria),
660
- side: genrePlaylist.criteria?.side ?? void 0,
661
- wikidataId: genrePlaylist.criteria?.wikidataId ?? null,
662
- umbrella: genrePlaylist.isUmbrella
663
- })),
664
- [genrePlaylistTreePerRoot]
665
- );
666
- const handlePlayPause = (0, import_react5.useCallback)(
667
- (nodeId) => {
668
- const genrePlaylist = genrePlaylistTreePerRoot.find((g) => g.uuid === nodeId);
669
- if (!genrePlaylist) return;
670
- if (nodeId === playingNodeId) {
671
- setIsPlaying(!isPlaying);
672
- return;
336
+ const { showPopup } = (0, import_popup.usePopup)();
337
+ const nodes = (0, import_react3.useMemo)(
338
+ () => genrePlaylistTreePerRoot.map((genrePlaylist) => ({
339
+ id: genrePlaylist.uuid,
340
+ parentId: genrePlaylist.parent?.uuid ?? null,
341
+ name: genrePlaylist.name,
342
+ itemCount: genrePlaylist.tracksCount,
343
+ actionable: Boolean(genrePlaylist.criteria),
344
+ side: genrePlaylist.criteria?.side ?? void 0,
345
+ wikidataId: genrePlaylist.criteria?.wikidataId ?? null,
346
+ umbrella: genrePlaylist.isUmbrella
347
+ })),
348
+ [genrePlaylistTreePerRoot]
349
+ );
350
+ const handlePlayPause = (0, import_react3.useCallback)(
351
+ (nodeId) => {
352
+ const genrePlaylist = genrePlaylistTreePerRoot.find((g) => g.uuid === nodeId);
353
+ if (!genrePlaylist) return;
354
+ if (nodeId === playingNodeId) {
355
+ setIsPlaying(!isPlaying);
356
+ return;
673
357
  }
674
358
  if (genrePlaylist.tracksCount === 0) {
675
359
  return;
676
360
  }
677
361
  play(genrePlaylist).catch((error) => {
678
362
  console.error("Failed to load genre playlist tracks:", error);
679
- showPopup(/* @__PURE__ */ (0, import_jsx_runtime9.jsx)(InternalErrorPopup, { errorCode: import_transport5.ErrorCode.CLIENT_UNKNOWN }));
363
+ showPopup(/* @__PURE__ */ (0, import_jsx_runtime5.jsx)(InternalErrorPopup, { errorCode: import_transport.ErrorCode.CLIENT_UNKNOWN }));
680
364
  });
681
365
  },
682
366
  [
@@ -688,7 +372,7 @@ function GenrePlaylistTreePerRoot({
688
372
  showPopup
689
373
  ]
690
374
  );
691
- const handleAddChild = (0, import_react5.useCallback)(
375
+ const handleAddChild = (0, import_react3.useCallback)(
692
376
  (parentId) => {
693
377
  const genrePlaylist = genrePlaylistTreePerRoot.find((g) => g.uuid === parentId);
694
378
  if (!genrePlaylist?.criteria) return;
@@ -696,7 +380,7 @@ function GenrePlaylistTreePerRoot({
696
380
  },
697
381
  [genrePlaylistTreePerRoot, handleGenreCreationAction]
698
382
  );
699
- const handleRenameRequest = (0, import_react5.useCallback)(
383
+ const handleRenameRequest = (0, import_react3.useCallback)(
700
384
  (node) => {
701
385
  const genrePlaylist = genrePlaylistTreePerRoot.find((g) => g.uuid === node.id);
702
386
  if (!genrePlaylist?.criteria) return;
@@ -704,16 +388,16 @@ function GenrePlaylistTreePerRoot({
704
388
  },
705
389
  [genrePlaylistTreePerRoot, handleGenreRenameAction]
706
390
  );
707
- const handleDeleteRequest = (0, import_react5.useCallback)((node) => {
391
+ const handleDeleteRequest = (0, import_react3.useCallback)((node) => {
708
392
  if (confirm(`Are you sure you want to delete "${node.name}"?`)) {
709
393
  }
710
394
  }, []);
711
- return /* @__PURE__ */ (0, import_jsx_runtime9.jsx)(
395
+ return /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(
712
396
  import_genre_tree_view.GenreTree,
713
397
  {
714
398
  className: className2,
715
399
  nodes,
716
- itemCountLabel: `${(0, import_track_noun3.capitalize)(trackNoun.singular)} count`,
400
+ itemCountLabel: `${(0, import_track_noun.capitalize)(trackNoun.singular)} count`,
717
401
  rootColor: (0, import_genre_tree_view.getGenreTreeColor)(rootUuid),
718
402
  playingNodeId,
719
403
  playState: isPlaying ? "playing" : "paused",
@@ -731,8 +415,8 @@ function GenrePlaylistTreePerRoot({
731
415
  hideInfoPanelTitle,
732
416
  showToolbar,
733
417
  wheelZoom,
734
- controlsOrientation,
735
418
  defaultViewport,
419
+ viewportRestore,
736
420
  onViewportChange,
737
421
  ...infoPanelCuration,
738
422
  readOnly
@@ -741,11 +425,11 @@ function GenrePlaylistTreePerRoot({
741
425
  }
742
426
 
743
427
  // src/genre-tree/playlist-tree/TreeWheel.tsx
744
- var import_react6 = require("react");
428
+ var import_react4 = require("react");
745
429
  var import_genre_tree_view2 = require("@behindthemusictree/genre-tree-view");
746
- var import_player4 = require("@behindthemusictree/app-kit/player");
747
- var import_popup3 = require("@behindthemusictree/app-kit/popup");
748
- var import_transport6 = require("@behindthemusictree/app-kit/transport");
430
+ var import_player2 = require("@behindthemusictree/app-kit/player");
431
+ var import_popup2 = require("@behindthemusictree/app-kit/popup");
432
+ var import_transport2 = require("@behindthemusictree/app-kit/transport");
749
433
 
750
434
  // src/genre-tree/playlist-tree/size-metric.ts
751
435
  var hasViewsCount = (genrePlaylists) => genrePlaylists.every((genrePlaylist) => genrePlaylist.viewsCount !== void 0);
@@ -758,8 +442,8 @@ var genrePlaylistSize = (genrePlaylist, metric) => {
758
442
  };
759
443
 
760
444
  // src/genre-tree/playlist-tree/TreeWheel.tsx
761
- var import_track_noun4 = require("@behindthemusictree/app-kit/track-noun");
762
- var import_jsx_runtime10 = require("react/jsx-runtime");
445
+ var import_track_noun2 = require("@behindthemusictree/app-kit/track-noun");
446
+ var import_jsx_runtime6 = require("react/jsx-runtime");
763
447
  function GenrePlaylistTreeWheel({
764
448
  scope,
765
449
  className: className2,
@@ -777,21 +461,21 @@ function GenrePlaylistTreeWheel({
777
461
  readOnly = false,
778
462
  allowWheelRotation,
779
463
  showToolbar,
780
- controlsOrientation,
781
464
  sizeMetric = "songs",
782
465
  floatingControlsLeading,
783
466
  busy,
784
467
  defaultViewport,
468
+ viewportRestore,
785
469
  onViewportChange,
786
470
  playback,
787
471
  ...infoPanelCuration
788
472
  }) {
789
- const { isPlaying, setIsPlaying } = (0, import_player4.usePlayer)();
473
+ const { isPlaying, setIsPlaying } = (0, import_player2.usePlayer)();
790
474
  const defaultPlayback = useGenrePlaylistPlayback({ scope, genrePlaylists });
791
475
  const { playingNodeId, play } = playback ?? defaultPlayback;
792
- const trackNoun = (0, import_track_noun4.useTrackNoun)();
793
- const { showPopup } = (0, import_popup3.usePopup)();
794
- const nodes = (0, import_react6.useMemo)(
476
+ const trackNoun = (0, import_track_noun2.useTrackNoun)();
477
+ const { showPopup } = (0, import_popup2.usePopup)();
478
+ const nodes = (0, import_react4.useMemo)(
795
479
  () => genrePlaylists.map((genrePlaylist) => ({
796
480
  id: genrePlaylist.uuid,
797
481
  parentId: genrePlaylist.parent?.uuid ?? null,
@@ -803,7 +487,7 @@ function GenrePlaylistTreeWheel({
803
487
  })),
804
488
  [genrePlaylists, sizeMetric]
805
489
  );
806
- const handlePlayPause = (0, import_react6.useCallback)(
490
+ const handlePlayPause = (0, import_react4.useCallback)(
807
491
  (nodeId) => {
808
492
  const genrePlaylist = genrePlaylists.find((g) => g.uuid === nodeId);
809
493
  if (!genrePlaylist) return;
@@ -816,7 +500,7 @@ function GenrePlaylistTreeWheel({
816
500
  }
817
501
  play(genrePlaylist).catch((error) => {
818
502
  console.error("Failed to load genre playlist tracks:", error);
819
- showPopup(/* @__PURE__ */ (0, import_jsx_runtime10.jsx)(InternalErrorPopup, { errorCode: import_transport6.ErrorCode.CLIENT_UNKNOWN }));
503
+ showPopup(/* @__PURE__ */ (0, import_jsx_runtime6.jsx)(InternalErrorPopup, { errorCode: import_transport2.ErrorCode.CLIENT_UNKNOWN }));
820
504
  });
821
505
  },
822
506
  [
@@ -828,7 +512,7 @@ function GenrePlaylistTreeWheel({
828
512
  showPopup
829
513
  ]
830
514
  );
831
- const handleAddChild = (0, import_react6.useCallback)(
515
+ const handleAddChild = (0, import_react4.useCallback)(
832
516
  (parentId) => {
833
517
  const genrePlaylist = genrePlaylists.find((g) => g.uuid === parentId);
834
518
  if (!genrePlaylist?.criteria) return;
@@ -836,7 +520,7 @@ function GenrePlaylistTreeWheel({
836
520
  },
837
521
  [genrePlaylists, handleGenreCreationAction]
838
522
  );
839
- const handleRenameRequest = (0, import_react6.useCallback)(
523
+ const handleRenameRequest = (0, import_react4.useCallback)(
840
524
  (node) => {
841
525
  const genrePlaylist = genrePlaylists.find((g) => g.uuid === node.id);
842
526
  if (!genrePlaylist?.criteria) return;
@@ -844,16 +528,16 @@ function GenrePlaylistTreeWheel({
844
528
  },
845
529
  [genrePlaylists, handleGenreRenameAction]
846
530
  );
847
- const handleDeleteRequest = (0, import_react6.useCallback)((node) => {
531
+ const handleDeleteRequest = (0, import_react4.useCallback)((node) => {
848
532
  if (confirm(`Are you sure you want to delete "${node.name}"?`)) {
849
533
  }
850
534
  }, []);
851
- return /* @__PURE__ */ (0, import_jsx_runtime10.jsx)(
535
+ return /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(
852
536
  import_genre_tree_view2.GenreTreeWheel,
853
537
  {
854
538
  className: className2,
855
539
  nodes,
856
- itemCountLabel: sizeMetric === "views" ? "Views" : `${(0, import_track_noun4.capitalize)(trackNoun.singular)} count`,
540
+ itemCountLabel: sizeMetric === "views" ? "Views" : `${(0, import_track_noun2.capitalize)(trackNoun.singular)} count`,
857
541
  playingNodeId,
858
542
  playState: isPlaying ? "playing" : "paused",
859
543
  onPlayPause: handlePlayPause,
@@ -870,10 +554,10 @@ function GenrePlaylistTreeWheel({
870
554
  hideInfoPanelTitle,
871
555
  allowWheelRotation,
872
556
  showToolbar,
873
- controlsOrientation,
874
557
  floatingControlsLeading,
875
558
  busy,
876
559
  defaultViewport,
560
+ viewportRestore,
877
561
  onViewportChange,
878
562
  ...infoPanelCuration,
879
563
  readOnly
@@ -882,11 +566,11 @@ function GenrePlaylistTreeWheel({
882
566
  }
883
567
 
884
568
  // src/genre-tree/playlist-tree/TreeWheelRadialPopCore.tsx
885
- var import_react7 = require("react");
569
+ var import_react5 = require("react");
886
570
  var import_genre_tree_view3 = require("@behindthemusictree/genre-tree-view");
887
- var import_player5 = require("@behindthemusictree/app-kit/player");
888
- var import_track_noun5 = require("@behindthemusictree/app-kit/track-noun");
889
- var import_jsx_runtime11 = require("react/jsx-runtime");
571
+ var import_player3 = require("@behindthemusictree/app-kit/player");
572
+ var import_track_noun3 = require("@behindthemusictree/app-kit/track-noun");
573
+ var import_jsx_runtime7 = require("react/jsx-runtime");
890
574
  function GenrePlaylistTreeWheelRadialPopCore(props) {
891
575
  const {
892
576
  scope,
@@ -905,12 +589,12 @@ function GenrePlaylistTreeWheelRadialPopCore(props) {
905
589
  readOnly = false,
906
590
  allowWheelRotation,
907
591
  showToolbar,
908
- controlsOrientation,
909
592
  sizeMetric = "songs",
910
593
  floatingControlsLeading,
911
594
  busy,
912
595
  outline = false,
913
596
  defaultViewport,
597
+ viewportRestore,
914
598
  onViewportChange,
915
599
  playback
916
600
  } = props;
@@ -923,11 +607,11 @@ function GenrePlaylistTreeWheelRadialPopCore(props) {
923
607
  hideExpandButton: props.hideExpandButton,
924
608
  overlayPanel: props.overlayPanel
925
609
  };
926
- const { isPlaying, setIsPlaying } = (0, import_player5.usePlayer)();
610
+ const { isPlaying, setIsPlaying } = (0, import_player3.usePlayer)();
927
611
  const defaultPlayback = useGenrePlaylistPlayback({ scope, genrePlaylists });
928
612
  const { playingNodeId, play } = playback ?? defaultPlayback;
929
- const trackNoun = (0, import_track_noun5.useTrackNoun)();
930
- const nodes = (0, import_react7.useMemo)(
613
+ const trackNoun = (0, import_track_noun3.useTrackNoun)();
614
+ const nodes = (0, import_react5.useMemo)(
931
615
  () => genrePlaylists.map((genrePlaylist) => ({
932
616
  id: genrePlaylist.uuid,
933
617
  parentId: genrePlaylist.parent?.uuid ?? null,
@@ -942,7 +626,7 @@ function GenrePlaylistTreeWheelRadialPopCore(props) {
942
626
  })),
943
627
  [genrePlaylists, sizeMetric]
944
628
  );
945
- const handlePlayPause = (0, import_react7.useCallback)(
629
+ const handlePlayPause = (0, import_react5.useCallback)(
946
630
  (nodeId) => {
947
631
  const genrePlaylist = genrePlaylists.find((g) => g.uuid === nodeId);
948
632
  if (!genrePlaylist) return;
@@ -959,7 +643,7 @@ function GenrePlaylistTreeWheelRadialPopCore(props) {
959
643
  },
960
644
  [genrePlaylists, playingNodeId, isPlaying, setIsPlaying, play]
961
645
  );
962
- const handleAddChild = (0, import_react7.useCallback)(
646
+ const handleAddChild = (0, import_react5.useCallback)(
963
647
  (parentId) => {
964
648
  const genrePlaylist = genrePlaylists.find((g) => g.uuid === parentId);
965
649
  if (!genrePlaylist?.criteria) return;
@@ -967,7 +651,7 @@ function GenrePlaylistTreeWheelRadialPopCore(props) {
967
651
  },
968
652
  [genrePlaylists, handleGenreCreationAction]
969
653
  );
970
- const handleRenameRequest = (0, import_react7.useCallback)(
654
+ const handleRenameRequest = (0, import_react5.useCallback)(
971
655
  (node) => {
972
656
  const genrePlaylist = genrePlaylists.find((g) => g.uuid === node.id);
973
657
  if (!genrePlaylist?.criteria) return;
@@ -975,14 +659,14 @@ function GenrePlaylistTreeWheelRadialPopCore(props) {
975
659
  },
976
660
  [genrePlaylists, handleGenreRenameAction]
977
661
  );
978
- const handleDeleteRequest = (0, import_react7.useCallback)((node) => {
662
+ const handleDeleteRequest = (0, import_react5.useCallback)((node) => {
979
663
  if (confirm(`Are you sure you want to delete "${node.name}"?`)) {
980
664
  }
981
665
  }, []);
982
666
  const treeProps = {
983
667
  className: className2,
984
668
  nodes,
985
- itemCountLabel: sizeMetric === "views" ? "Views" : `${(0, import_track_noun5.capitalize)(trackNoun.singular)} count`,
669
+ itemCountLabel: sizeMetric === "views" ? "Views" : `${(0, import_track_noun3.capitalize)(trackNoun.singular)} count`,
986
670
  playingNodeId,
987
671
  playState: isPlaying ? "playing" : "paused",
988
672
  onPlayPause: handlePlayPause,
@@ -1001,15 +685,15 @@ function GenrePlaylistTreeWheelRadialPopCore(props) {
1001
685
  ...infoPanelCuration,
1002
686
  readOnly
1003
687
  };
1004
- return outline ? /* @__PURE__ */ (0, import_jsx_runtime11.jsx)(import_genre_tree_view3.GenreTreeOutline, { ...treeProps }) : /* @__PURE__ */ (0, import_jsx_runtime11.jsx)(
688
+ return outline ? /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(import_genre_tree_view3.GenreTreeOutline, { ...treeProps }) : /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(
1005
689
  import_genre_tree_view3.GenreTreeWheelRadialPopCore,
1006
690
  {
1007
691
  ...treeProps,
1008
692
  allowWheelRotation,
1009
- controlsOrientation,
1010
693
  floatingControlsLeading,
1011
694
  busy,
1012
695
  defaultViewport,
696
+ viewportRestore,
1013
697
  onViewportChange,
1014
698
  ...focus.onFocusGenreChange ? { focusNodeId: focus.focusGenreUuid, onFocusChange: focus.onFocusGenreChange } : { focusNodeId: void 0, onFocusChange: void 0 }
1015
699
  }
@@ -1017,13 +701,13 @@ function GenrePlaylistTreeWheelRadialPopCore(props) {
1017
701
  }
1018
702
 
1019
703
  // src/genre-tree/GenreTreeWheelHandoff.tsx
1020
- var import_react8 = require("react");
1021
- var import_jsx_runtime12 = require("react/jsx-runtime");
704
+ var import_react6 = require("react");
705
+ var import_jsx_runtime8 = require("react/jsx-runtime");
1022
706
  function GenreTreeWheelHandoff({ skeleton, children, onRevealedChange }) {
1023
- const [revealed, setRevealed] = (0, import_react8.useState)(false);
1024
- const onRevealedChangeRef = (0, import_react8.useRef)(onRevealedChange);
707
+ const [revealed, setRevealed] = (0, import_react6.useState)(false);
708
+ const onRevealedChangeRef = (0, import_react6.useRef)(onRevealedChange);
1025
709
  onRevealedChangeRef.current = onRevealedChange;
1026
- (0, import_react8.useEffect)(() => {
710
+ (0, import_react6.useEffect)(() => {
1027
711
  let innerFrame = 0;
1028
712
  const outerFrame = requestAnimationFrame(() => {
1029
713
  innerFrame = requestAnimationFrame(() => {
@@ -1037,8 +721,8 @@ function GenreTreeWheelHandoff({ skeleton, children, onRevealedChange }) {
1037
721
  cancelAnimationFrame(innerFrame);
1038
722
  };
1039
723
  }, []);
1040
- return /* @__PURE__ */ (0, import_jsx_runtime12.jsxs)("div", { className: "relative w-full h-full", children: [
1041
- /* @__PURE__ */ (0, import_jsx_runtime12.jsx)(
724
+ return /* @__PURE__ */ (0, import_jsx_runtime8.jsxs)("div", { className: "relative w-full h-full", children: [
725
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(
1042
726
  "div",
1043
727
  {
1044
728
  className: "absolute inset-0 transition-opacity duration-150 ease-out",
@@ -1047,15 +731,15 @@ function GenreTreeWheelHandoff({ skeleton, children, onRevealedChange }) {
1047
731
  children
1048
732
  }
1049
733
  ),
1050
- !revealed && /* @__PURE__ */ (0, import_jsx_runtime12.jsx)("div", { className: "absolute inset-0", "aria-hidden": "true", children: skeleton })
734
+ !revealed && /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("div", { className: "absolute inset-0", "aria-hidden": "true", children: skeleton })
1051
735
  ] });
1052
736
  }
1053
737
 
1054
738
  // src/genre-tree/GenreSearch.tsx
1055
739
  var import_lucide_react3 = require("lucide-react");
1056
- var import_ui5 = require("@behindthemusictree/ui");
1057
- var import_genre_tree9 = require("@behindthemusictree/app-kit/genre-tree");
1058
- var import_jsx_runtime13 = require("react/jsx-runtime");
740
+ var import_ui3 = require("@behindthemusictree/ui");
741
+ var import_genre_tree2 = require("@behindthemusictree/app-kit/genre-tree");
742
+ var import_jsx_runtime9 = require("react/jsx-runtime");
1059
743
  function GenreSearch({
1060
744
  genrePlaylists,
1061
745
  onSelect,
@@ -1066,7 +750,7 @@ function GenreSearch({
1066
750
  onQueryChange,
1067
751
  extraResults
1068
752
  }) {
1069
- const { query, setQuery, results, canClear, clear, select } = (0, import_genre_tree9.useGenreSearch)({
753
+ const { query, setQuery, results, canClear, clear, select } = (0, import_genre_tree2.useGenreSearch)({
1070
754
  genrePlaylists,
1071
755
  onSelect,
1072
756
  selectedName,
@@ -1077,8 +761,8 @@ function GenreSearch({
1077
761
  const loading = query !== "" && query !== selectedName && (extraResults?.loading ?? false);
1078
762
  const showList = results.length > 0 || extraItems.length > 0;
1079
763
  const heading = "px-3 pt-2 pb-1 text-xs font-medium uppercase text-[var(--color-text-muted)]";
1080
- return /* @__PURE__ */ (0, import_jsx_runtime13.jsxs)("div", { className: "genre-search relative w-[calc(var(--layout-detail-panel-w,25.5rem)-2*var(--layout-panel-inset,1rem))] shrink-0 max-md:w-full", children: [
1081
- onPrevious && /* @__PURE__ */ (0, import_jsx_runtime13.jsx)(
764
+ return /* @__PURE__ */ (0, import_jsx_runtime9.jsxs)("div", { className: "genre-search relative w-[calc(var(--layout-detail-panel-w,25.5rem)-2*var(--layout-panel-inset,1rem))] shrink-0 max-md:w-full", children: [
765
+ onPrevious && /* @__PURE__ */ (0, import_jsx_runtime9.jsx)(
1082
766
  "button",
1083
767
  {
1084
768
  type: "button",
@@ -1086,11 +770,11 @@ function GenreSearch({
1086
770
  title: "Previous",
1087
771
  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)]",
1088
772
  onClick: onPrevious,
1089
- children: /* @__PURE__ */ (0, import_jsx_runtime13.jsx)(import_lucide_react3.ArrowLeft, { "aria-hidden": "true", className: "w-4 h-4" })
773
+ children: /* @__PURE__ */ (0, import_jsx_runtime9.jsx)(import_lucide_react3.ArrowLeft, { "aria-hidden": "true", className: "w-4 h-4" })
1090
774
  }
1091
775
  ),
1092
- /* @__PURE__ */ (0, import_jsx_runtime13.jsx)(
1093
- import_ui5.Input,
776
+ /* @__PURE__ */ (0, import_jsx_runtime9.jsx)(
777
+ import_ui3.Input,
1094
778
  {
1095
779
  type: "text",
1096
780
  value: query,
@@ -1107,21 +791,21 @@ function GenreSearch({
1107
791
  className: "h-[var(--layout-search-h,3rem)] pl-6 pr-20 text-base transition-[padding] duration-200 ease-out motion-reduce:transition-none bg-[var(--color-surface)] border-[var(--color-border)] rounded-full shadow-md"
1108
792
  }
1109
793
  ),
1110
- /* @__PURE__ */ (0, import_jsx_runtime13.jsxs)("div", { className: "absolute right-3 top-1/2 flex items-center -translate-y-1/2", children: [
1111
- /* @__PURE__ */ (0, import_jsx_runtime13.jsxs)(
794
+ /* @__PURE__ */ (0, import_jsx_runtime9.jsxs)("div", { className: "absolute right-3 top-1/2 flex items-center -translate-y-1/2", children: [
795
+ /* @__PURE__ */ (0, import_jsx_runtime9.jsxs)(
1112
796
  "span",
1113
797
  {
1114
798
  "data-loading": loading,
1115
799
  className: "relative p-2 text-[var(--color-text-muted)] pointer-events-none",
1116
800
  children: [
1117
- /* @__PURE__ */ (0, import_jsx_runtime13.jsx)(
801
+ /* @__PURE__ */ (0, import_jsx_runtime9.jsx)(
1118
802
  import_lucide_react3.Search,
1119
803
  {
1120
804
  "aria-hidden": "true",
1121
805
  className: `w-6 h-6 transition-opacity duration-200 motion-reduce:transition-none ${loading ? "opacity-0" : ""}`
1122
806
  }
1123
807
  ),
1124
- /* @__PURE__ */ (0, import_jsx_runtime13.jsx)(
808
+ /* @__PURE__ */ (0, import_jsx_runtime9.jsx)(
1125
809
  import_lucide_react3.LoaderCircle,
1126
810
  {
1127
811
  "aria-hidden": "true",
@@ -1131,7 +815,7 @@ function GenreSearch({
1131
815
  ]
1132
816
  }
1133
817
  ),
1134
- canClear && /* @__PURE__ */ (0, import_jsx_runtime13.jsx)(
818
+ canClear && /* @__PURE__ */ (0, import_jsx_runtime9.jsx)(
1135
819
  "button",
1136
820
  {
1137
821
  type: "button",
@@ -1141,14 +825,14 @@ function GenreSearch({
1141
825
  clear();
1142
826
  onQueryChange?.("");
1143
827
  },
1144
- children: /* @__PURE__ */ (0, import_jsx_runtime13.jsx)(import_lucide_react3.X, { "aria-hidden": "true", className: "w-6 h-6" })
828
+ children: /* @__PURE__ */ (0, import_jsx_runtime9.jsx)(import_lucide_react3.X, { "aria-hidden": "true", className: "w-6 h-6" })
1145
829
  }
1146
830
  )
1147
831
  ] }),
1148
- extraResults?.status && /* @__PURE__ */ (0, import_jsx_runtime13.jsx)("p", { role: "status", "aria-live": "polite", className: "sr-only", children: extraResults.status }),
1149
- showList && /* @__PURE__ */ (0, import_jsx_runtime13.jsxs)("ul", { className: "genre-search-results absolute z-40 w-full max-h-[min(16rem,50dvh)] mt-1 overflow-y-auto bg-[var(--color-surface)] border border-[var(--color-border)] rounded-xl shadow-md", children: [
1150
- extraResults && results.length > 0 && /* @__PURE__ */ (0, import_jsx_runtime13.jsx)("li", { className: heading, children: "Genres" }),
1151
- results.map((genrePlaylist) => /* @__PURE__ */ (0, import_jsx_runtime13.jsx)("li", { children: /* @__PURE__ */ (0, import_jsx_runtime13.jsxs)(
832
+ extraResults?.status && /* @__PURE__ */ (0, import_jsx_runtime9.jsx)("p", { role: "status", "aria-live": "polite", className: "sr-only", children: extraResults.status }),
833
+ showList && /* @__PURE__ */ (0, import_jsx_runtime9.jsxs)("ul", { className: "genre-search-results absolute z-40 w-full max-h-[min(16rem,50dvh)] mt-1 overflow-y-auto bg-[var(--color-surface)] border border-[var(--color-border)] rounded-xl shadow-md", children: [
834
+ extraResults && results.length > 0 && /* @__PURE__ */ (0, import_jsx_runtime9.jsx)("li", { className: heading, children: "Genres" }),
835
+ results.map((genrePlaylist) => /* @__PURE__ */ (0, import_jsx_runtime9.jsx)("li", { children: /* @__PURE__ */ (0, import_jsx_runtime9.jsxs)(
1152
836
  "button",
1153
837
  {
1154
838
  type: "button",
@@ -1159,12 +843,12 @@ function GenreSearch({
1159
843
  },
1160
844
  children: [
1161
845
  genrePlaylist.name,
1162
- genrePlaylist.isUmbrella && /* @__PURE__ */ (0, import_jsx_runtime13.jsx)("span", { className: "ml-2 text-xs italic text-[var(--color-text-muted)]", children: "umbrella" })
846
+ genrePlaylist.isUmbrella && /* @__PURE__ */ (0, import_jsx_runtime9.jsx)("span", { className: "ml-2 text-xs italic text-[var(--color-text-muted)]", children: "umbrella" })
1163
847
  ]
1164
848
  }
1165
849
  ) }, genrePlaylist.uuid)),
1166
- extraResults && extraItems.length > 0 && /* @__PURE__ */ (0, import_jsx_runtime13.jsx)("li", { className: heading, children: extraResults.label }),
1167
- extraItems.map((item) => /* @__PURE__ */ (0, import_jsx_runtime13.jsx)("li", { children: /* @__PURE__ */ (0, import_jsx_runtime13.jsx)(
850
+ extraResults && extraItems.length > 0 && /* @__PURE__ */ (0, import_jsx_runtime9.jsx)("li", { className: heading, children: extraResults.label }),
851
+ extraItems.map((item) => /* @__PURE__ */ (0, import_jsx_runtime9.jsx)("li", { children: /* @__PURE__ */ (0, import_jsx_runtime9.jsx)(
1168
852
  "button",
1169
853
  {
1170
854
  type: "button",
@@ -1181,33 +865,36 @@ function GenreSearch({
1181
865
  ] });
1182
866
  }
1183
867
 
868
+ // src/genre-tree/GenreInfoPanelBody.tsx
869
+ var import_player6 = require("@behindthemusictree/app-kit/player");
870
+
1184
871
  // src/genre-tree/GenreActionBand.tsx
1185
872
  var import_lucide_react7 = require("lucide-react");
1186
873
 
1187
874
  // src/genre-tree/ShareActionButton.tsx
1188
875
  var import_lucide_react6 = require("lucide-react");
1189
- var import_popup4 = require("@behindthemusictree/app-kit/popup");
876
+ var import_popup3 = require("@behindthemusictree/app-kit/popup");
1190
877
 
1191
878
  // src/genre-tree/SharePopup.tsx
1192
- var import_react10 = require("react");
879
+ var import_react8 = require("react");
1193
880
  var import_lucide_react5 = require("lucide-react");
1194
- var import_fa2 = require("react-icons/fa");
1195
- var import_si2 = require("react-icons/si");
881
+ var import_fa = require("react-icons/fa");
882
+ var import_si = require("react-icons/si");
1196
883
 
1197
884
  // src/popup/Toast.tsx
1198
- var import_react9 = require("react");
885
+ var import_react7 = require("react");
1199
886
  var import_react_dom2 = require("react-dom");
1200
- var import_jsx_runtime14 = require("react/jsx-runtime");
887
+ var import_jsx_runtime10 = require("react/jsx-runtime");
1201
888
  function Toast({ message, onClose, durationMs = 2200 }) {
1202
- const [mounted, setMounted] = (0, import_react9.useState)(false);
1203
- (0, import_react9.useEffect)(() => {
889
+ const [mounted, setMounted] = (0, import_react7.useState)(false);
890
+ (0, import_react7.useEffect)(() => {
1204
891
  setMounted(true);
1205
892
  const timeout = window.setTimeout(onClose, durationMs);
1206
893
  return () => window.clearTimeout(timeout);
1207
894
  }, [durationMs, onClose]);
1208
895
  if (!mounted) return null;
1209
896
  return (0, import_react_dom2.createPortal)(
1210
- /* @__PURE__ */ (0, import_jsx_runtime14.jsx)("div", { className: "pointer-events-none fixed inset-x-0 bottom-8 z-[60] flex justify-center px-4", children: /* @__PURE__ */ (0, import_jsx_runtime14.jsx)(
897
+ /* @__PURE__ */ (0, import_jsx_runtime10.jsx)("div", { className: "pointer-events-none fixed inset-x-0 bottom-8 z-[60] flex justify-center px-4", children: /* @__PURE__ */ (0, import_jsx_runtime10.jsx)(
1211
898
  "div",
1212
899
  {
1213
900
  role: "status",
@@ -1222,12 +909,12 @@ function Toast({ message, onClose, durationMs = 2200 }) {
1222
909
  }
1223
910
 
1224
911
  // src/genre-tree/ShareDestinationButton.tsx
1225
- var import_jsx_runtime15 = require("react/jsx-runtime");
912
+ var import_jsx_runtime11 = require("react/jsx-runtime");
1226
913
  var className = "group flex min-w-0 flex-1 flex-col items-center justify-center gap-1.5 rounded-xl px-2 py-2 text-xs text-[var(--color-text)] transition-colors hover:bg-[var(--color-surface-muted)] focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[var(--color-focus-ring)] disabled:cursor-not-allowed disabled:opacity-50";
1227
914
  function Contents({ icon, label }) {
1228
- return /* @__PURE__ */ (0, import_jsx_runtime15.jsxs)(import_jsx_runtime15.Fragment, { children: [
1229
- /* @__PURE__ */ (0, import_jsx_runtime15.jsx)("span", { className: "flex h-12 w-12 items-center justify-center rounded-full bg-[var(--color-surface-muted)] transition-colors group-hover:bg-[var(--color-border)]", children: icon }),
1230
- /* @__PURE__ */ (0, import_jsx_runtime15.jsx)("span", { className: "max-w-full truncate", children: label })
915
+ return /* @__PURE__ */ (0, import_jsx_runtime11.jsxs)(import_jsx_runtime11.Fragment, { children: [
916
+ /* @__PURE__ */ (0, import_jsx_runtime11.jsx)("span", { className: "flex h-12 w-12 items-center justify-center rounded-full bg-[var(--color-surface-muted)] transition-colors group-hover:bg-[var(--color-border)]", children: icon }),
917
+ /* @__PURE__ */ (0, import_jsx_runtime11.jsx)("span", { className: "max-w-full truncate", children: label })
1231
918
  ] });
1232
919
  }
1233
920
  function ShareDestinationButton({
@@ -1239,29 +926,29 @@ function ShareDestinationButton({
1239
926
  ariaLabel
1240
927
  }) {
1241
928
  if (href) {
1242
- return /* @__PURE__ */ (0, import_jsx_runtime15.jsx)("a", { href, target: "_blank", rel: "noopener noreferrer", "aria-label": ariaLabel, className, children: /* @__PURE__ */ (0, import_jsx_runtime15.jsx)(Contents, { icon, label }) });
929
+ return /* @__PURE__ */ (0, import_jsx_runtime11.jsx)("a", { href, target: "_blank", rel: "noopener noreferrer", "aria-label": ariaLabel, className, children: /* @__PURE__ */ (0, import_jsx_runtime11.jsx)(Contents, { icon, label }) });
1243
930
  }
1244
- return /* @__PURE__ */ (0, import_jsx_runtime15.jsx)("button", { type: "button", onClick, disabled, "aria-label": ariaLabel, className, children: /* @__PURE__ */ (0, import_jsx_runtime15.jsx)(Contents, { icon, label }) });
931
+ return /* @__PURE__ */ (0, import_jsx_runtime11.jsx)("button", { type: "button", onClick, disabled, "aria-label": ariaLabel, className, children: /* @__PURE__ */ (0, import_jsx_runtime11.jsx)(Contents, { icon, label }) });
1245
932
  }
1246
933
 
1247
934
  // src/genre-tree/SharePreviewCard.tsx
1248
935
  var import_lucide_react4 = require("lucide-react");
1249
- var import_jsx_runtime16 = require("react/jsx-runtime");
936
+ var import_jsx_runtime12 = require("react/jsx-runtime");
1250
937
  function SharePreviewCard({ target, url }) {
1251
938
  const Icon = target.kind === "genre" ? import_lucide_react4.Music2 : import_lucide_react4.UserRound;
1252
939
  const typeLabel = target.kind === "genre" ? "Genre" : "Artist";
1253
- return /* @__PURE__ */ (0, import_jsx_runtime16.jsx)("div", { className: "rounded-3xl bg-[var(--color-surface-muted)] p-4", children: /* @__PURE__ */ (0, import_jsx_runtime16.jsxs)("div", { className: "flex min-h-32 overflow-hidden rounded-2xl bg-[var(--color-surface)] shadow-lg", children: [
1254
- /* @__PURE__ */ (0, import_jsx_runtime16.jsx)("div", { className: "flex w-28 shrink-0 items-center justify-center bg-[var(--color-primary)]/15 text-[var(--color-primary)]", children: /* @__PURE__ */ (0, import_jsx_runtime16.jsx)(Icon, { "aria-hidden": "true", className: "h-10 w-10" }) }),
1255
- /* @__PURE__ */ (0, import_jsx_runtime16.jsxs)("div", { className: "flex min-w-0 flex-1 flex-col justify-center gap-1 p-4", children: [
1256
- /* @__PURE__ */ (0, import_jsx_runtime16.jsx)("span", { className: "text-xs font-medium uppercase tracking-wide text-[var(--color-text-muted)]", children: typeLabel }),
1257
- /* @__PURE__ */ (0, import_jsx_runtime16.jsx)("span", { className: "truncate text-base font-semibold text-[var(--color-text)]", title: target.name, children: target.name }),
1258
- /* @__PURE__ */ (0, import_jsx_runtime16.jsx)("span", { className: "truncate text-sm text-[var(--color-primary)]", title: url, children: url })
940
+ return /* @__PURE__ */ (0, import_jsx_runtime12.jsx)("div", { className: "rounded-3xl bg-[var(--color-surface-muted)] p-4", children: /* @__PURE__ */ (0, import_jsx_runtime12.jsxs)("div", { className: "flex min-h-32 overflow-hidden rounded-2xl bg-[var(--color-surface)] shadow-lg", children: [
941
+ /* @__PURE__ */ (0, import_jsx_runtime12.jsx)("div", { className: "flex w-28 shrink-0 items-center justify-center bg-[var(--color-primary)]/15 text-[var(--color-primary)]", children: /* @__PURE__ */ (0, import_jsx_runtime12.jsx)(Icon, { "aria-hidden": "true", className: "h-10 w-10" }) }),
942
+ /* @__PURE__ */ (0, import_jsx_runtime12.jsxs)("div", { className: "flex min-w-0 flex-1 flex-col justify-center gap-1 p-4", children: [
943
+ /* @__PURE__ */ (0, import_jsx_runtime12.jsx)("span", { className: "text-xs font-medium uppercase tracking-wide text-[var(--color-text-muted)]", children: typeLabel }),
944
+ /* @__PURE__ */ (0, import_jsx_runtime12.jsx)("span", { className: "truncate text-base font-semibold text-[var(--color-text)]", title: target.name, children: target.name }),
945
+ /* @__PURE__ */ (0, import_jsx_runtime12.jsx)("span", { className: "truncate text-sm text-[var(--color-primary)]", title: url, children: url })
1259
946
  ] })
1260
947
  ] }) });
1261
948
  }
1262
949
 
1263
950
  // src/genre-tree/SharePopup.tsx
1264
- var import_jsx_runtime17 = require("react/jsx-runtime");
951
+ var import_jsx_runtime13 = require("react/jsx-runtime");
1265
952
  function resolveAbsoluteUrl(url) {
1266
953
  try {
1267
954
  const parsed = new URL(url, window.location.href);
@@ -1294,20 +981,20 @@ async function copyText(text) {
1294
981
  }
1295
982
  }
1296
983
  function SharePopup({ target, url, onClose }) {
1297
- const [absoluteUrl, setAbsoluteUrl] = (0, import_react10.useState)(url);
1298
- const [copyStatus, setCopyStatus] = (0, import_react10.useState)("idle");
1299
- const [canUseNativeShare, setCanUseNativeShare] = (0, import_react10.useState)(false);
1300
- const [shareError, setShareError] = (0, import_react10.useState)(null);
1301
- const [toastId, setToastId] = (0, import_react10.useState)(0);
1302
- (0, import_react10.useEffect)(() => {
984
+ const [absoluteUrl, setAbsoluteUrl] = (0, import_react8.useState)(url);
985
+ const [copyStatus, setCopyStatus] = (0, import_react8.useState)("idle");
986
+ const [canUseNativeShare, setCanUseNativeShare] = (0, import_react8.useState)(false);
987
+ const [shareError, setShareError] = (0, import_react8.useState)(null);
988
+ const [toastId, setToastId] = (0, import_react8.useState)(0);
989
+ (0, import_react8.useEffect)(() => {
1303
990
  setAbsoluteUrl(resolveAbsoluteUrl(url) ?? "");
1304
991
  setCopyStatus("idle");
1305
992
  setShareError(null);
1306
993
  }, [url]);
1307
- (0, import_react10.useEffect)(() => {
994
+ (0, import_react8.useEffect)(() => {
1308
995
  setCanUseNativeShare(typeof navigator !== "undefined" && typeof navigator.share === "function");
1309
996
  }, []);
1310
- const destinationUrls = (0, import_react10.useMemo)(() => {
997
+ const destinationUrls = (0, import_react8.useMemo)(() => {
1311
998
  if (!absoluteUrl) return null;
1312
999
  const message = `${target.name} \u2014 ${absoluteUrl}`;
1313
1000
  return {
@@ -1325,7 +1012,7 @@ function SharePopup({ target, url, onClose }) {
1325
1012
  setCopyStatus(copied ? "idle" : "failed");
1326
1013
  if (copied) setToastId((id) => id + 1);
1327
1014
  };
1328
- const dismissToast = (0, import_react10.useCallback)(() => setToastId(0), []);
1015
+ const dismissToast = (0, import_react8.useCallback)(() => setToastId(0), []);
1329
1016
  const handleNativeShare = async () => {
1330
1017
  if (!absoluteUrl || !navigator.share) return;
1331
1018
  setShareError(null);
@@ -1336,8 +1023,8 @@ function SharePopup({ target, url, onClose }) {
1336
1023
  setShareError("Could not open the share options.");
1337
1024
  }
1338
1025
  };
1339
- return /* @__PURE__ */ (0, import_jsx_runtime17.jsxs)(import_jsx_runtime17.Fragment, { children: [
1340
- /* @__PURE__ */ (0, import_jsx_runtime17.jsx)(
1026
+ return /* @__PURE__ */ (0, import_jsx_runtime13.jsxs)(import_jsx_runtime13.Fragment, { children: [
1027
+ /* @__PURE__ */ (0, import_jsx_runtime13.jsx)(
1341
1028
  BasePopup_default,
1342
1029
  {
1343
1030
  title: "Share",
@@ -1352,175 +1039,550 @@ function SharePopup({ target, url, onClose }) {
1352
1039
  titleTextClassName: "!text-xl !font-semibold !text-[var(--color-text)]",
1353
1040
  closeButtonClassName: "!h-10 !w-10 !rounded-none !border-0 !bg-transparent !p-0 !text-[var(--color-text)] hover:!bg-transparent hover:!text-[var(--color-text)]",
1354
1041
  contentClassName: "!rounded-none !bg-transparent !px-6 !pb-6 !pt-2",
1355
- children: /* @__PURE__ */ (0, import_jsx_runtime17.jsxs)("div", { className: "flex flex-col gap-4", children: [
1356
- /* @__PURE__ */ (0, import_jsx_runtime17.jsx)(SharePreviewCard, { target, url: absoluteUrl || url }),
1357
- /* @__PURE__ */ (0, import_jsx_runtime17.jsxs)("div", { className: "grid grid-cols-3 gap-1 rounded-3xl bg-[var(--color-surface)] p-3 sm:grid-cols-4", children: [
1358
- /* @__PURE__ */ (0, import_jsx_runtime17.jsx)(
1042
+ children: /* @__PURE__ */ (0, import_jsx_runtime13.jsxs)("div", { className: "flex flex-col gap-4", children: [
1043
+ /* @__PURE__ */ (0, import_jsx_runtime13.jsx)(SharePreviewCard, { target, url: absoluteUrl || url }),
1044
+ /* @__PURE__ */ (0, import_jsx_runtime13.jsxs)("div", { className: "grid grid-cols-3 gap-1 rounded-3xl bg-[var(--color-surface)] p-3 sm:grid-cols-4", children: [
1045
+ /* @__PURE__ */ (0, import_jsx_runtime13.jsx)(
1359
1046
  ShareDestinationButton,
1360
1047
  {
1361
1048
  label: "Copy Link",
1362
- icon: /* @__PURE__ */ (0, import_jsx_runtime17.jsx)(import_lucide_react5.Link2, { "aria-hidden": "true", className: "h-6 w-6" }),
1049
+ icon: /* @__PURE__ */ (0, import_jsx_runtime13.jsx)(import_lucide_react5.Link2, { "aria-hidden": "true", className: "h-6 w-6" }),
1363
1050
  onClick: () => void handleCopy(),
1364
1051
  ariaLabel: "Copy link",
1365
1052
  disabled: !absoluteUrl
1366
1053
  }
1367
1054
  ),
1368
- /* @__PURE__ */ (0, import_jsx_runtime17.jsx)(
1055
+ /* @__PURE__ */ (0, import_jsx_runtime13.jsx)(
1369
1056
  ShareDestinationButton,
1370
1057
  {
1371
1058
  label: "WhatsApp",
1372
- icon: /* @__PURE__ */ (0, import_jsx_runtime17.jsx)(import_fa2.FaWhatsapp, { "aria-hidden": "true", className: "h-9 w-9 text-[#25D366]" }),
1059
+ icon: /* @__PURE__ */ (0, import_jsx_runtime13.jsx)(import_fa.FaWhatsapp, { "aria-hidden": "true", className: "h-9 w-9 text-[#25D366]" }),
1373
1060
  href: destinationUrls?.whatsapp,
1374
1061
  ariaLabel: `Share ${target.name} via WhatsApp`,
1375
1062
  disabled: !absoluteUrl
1376
1063
  }
1377
1064
  ),
1378
- /* @__PURE__ */ (0, import_jsx_runtime17.jsx)(
1065
+ /* @__PURE__ */ (0, import_jsx_runtime13.jsx)(
1379
1066
  ShareDestinationButton,
1380
1067
  {
1381
1068
  label: "Gmail",
1382
- icon: /* @__PURE__ */ (0, import_jsx_runtime17.jsx)(import_si2.SiGmail, { "aria-hidden": "true", className: "h-8 w-8 text-[#EA4335]" }),
1069
+ icon: /* @__PURE__ */ (0, import_jsx_runtime13.jsx)(import_si.SiGmail, { "aria-hidden": "true", className: "h-8 w-8 text-[#EA4335]" }),
1383
1070
  href: destinationUrls?.gmail,
1384
1071
  ariaLabel: `Email ${target.name} with Gmail`,
1385
1072
  disabled: !absoluteUrl
1386
1073
  }
1387
1074
  ),
1388
- canUseNativeShare && /* @__PURE__ */ (0, import_jsx_runtime17.jsx)(
1075
+ canUseNativeShare && /* @__PURE__ */ (0, import_jsx_runtime13.jsx)(
1389
1076
  ShareDestinationButton,
1390
1077
  {
1391
1078
  label: "More",
1392
- icon: /* @__PURE__ */ (0, import_jsx_runtime17.jsx)(import_lucide_react5.Share2, { "aria-hidden": "true", className: "h-6 w-6" }),
1079
+ icon: /* @__PURE__ */ (0, import_jsx_runtime13.jsx)(import_lucide_react5.Share2, { "aria-hidden": "true", className: "h-6 w-6" }),
1393
1080
  onClick: () => void handleNativeShare(),
1394
1081
  ariaLabel: "More sharing options",
1395
1082
  disabled: !absoluteUrl
1396
1083
  }
1397
1084
  )
1398
1085
  ] }),
1399
- copyStatus === "failed" && /* @__PURE__ */ (0, import_jsx_runtime17.jsx)("p", { role: "alert", className: "text-center text-sm text-[var(--color-red-500)]", children: "Could not copy the link." }),
1400
- shareError && /* @__PURE__ */ (0, import_jsx_runtime17.jsx)("p", { role: "alert", className: "text-center text-sm text-[var(--color-red-500)]", children: shareError })
1086
+ copyStatus === "failed" && /* @__PURE__ */ (0, import_jsx_runtime13.jsx)("p", { role: "alert", className: "text-center text-sm text-[var(--color-red-500)]", children: "Could not copy the link." }),
1087
+ shareError && /* @__PURE__ */ (0, import_jsx_runtime13.jsx)("p", { role: "alert", className: "text-center text-sm text-[var(--color-red-500)]", children: shareError })
1401
1088
  ] })
1402
1089
  }
1403
1090
  ),
1404
- toastId > 0 && /* @__PURE__ */ (0, import_jsx_runtime17.jsx)(Toast, { message: "Link copied", onClose: dismissToast }, toastId)
1091
+ toastId > 0 && /* @__PURE__ */ (0, import_jsx_runtime13.jsx)(Toast, { message: "Link copied", onClose: dismissToast }, toastId)
1405
1092
  ] });
1406
1093
  }
1407
1094
 
1408
1095
  // src/genre-tree/ShareActionButton.tsx
1409
- var import_jsx_runtime18 = require("react/jsx-runtime");
1096
+ var import_jsx_runtime14 = require("react/jsx-runtime");
1410
1097
  function ShareActionButton({ target, url }) {
1411
- const { showPopup, hidePopup } = (0, import_popup4.usePopup)();
1098
+ const { showPopup, hidePopup } = (0, import_popup3.usePopup)();
1412
1099
  const openSharePopup = () => {
1413
- showPopup(/* @__PURE__ */ (0, import_jsx_runtime18.jsx)(SharePopup, { target, url, onClose: hidePopup }));
1100
+ showPopup(/* @__PURE__ */ (0, import_jsx_runtime14.jsx)(SharePopup, { target, url, onClose: hidePopup }));
1101
+ };
1102
+ return /* @__PURE__ */ (0, import_jsx_runtime14.jsxs)(
1103
+ "button",
1104
+ {
1105
+ type: "button",
1106
+ onClick: openSharePopup,
1107
+ className: "group flex flex-col items-center gap-1 w-16 text-xs text-center text-[var(--color-text)] focus-visible:outline-none",
1108
+ "aria-label": `Share ${target.name}`,
1109
+ title: `Share ${target.name}`,
1110
+ children: [
1111
+ /* @__PURE__ */ (0, import_jsx_runtime14.jsx)("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: /* @__PURE__ */ (0, import_jsx_runtime14.jsx)(import_lucide_react6.Share2, { "aria-hidden": "true", className: "w-5 h-5" }) }),
1112
+ "Share"
1113
+ ]
1114
+ }
1115
+ );
1116
+ }
1117
+
1118
+ // src/genre-tree/GenreActionBand.tsx
1119
+ var import_jsx_runtime15 = require("react/jsx-runtime");
1120
+ function GenreActionBand({
1121
+ node,
1122
+ onPlayPause,
1123
+ isPlaying = false,
1124
+ onZoomIn,
1125
+ onAddChild,
1126
+ onRename,
1127
+ onChangeParent,
1128
+ side,
1129
+ onToggleSide,
1130
+ toggleSideDisabledReason,
1131
+ additionalActions,
1132
+ shareTarget,
1133
+ shareUrl
1134
+ }) {
1135
+ const items = [];
1136
+ const adminItems = [];
1137
+ if (onPlayPause) {
1138
+ items.push({
1139
+ key: "play-pause",
1140
+ label: isPlaying ? "Pause" : "Play",
1141
+ icon: isPlaying ? /* @__PURE__ */ (0, import_jsx_runtime15.jsx)(import_lucide_react7.Pause, { "aria-hidden": "true", className: "w-5 h-5" }) : /* @__PURE__ */ (0, import_jsx_runtime15.jsx)(import_lucide_react7.Play, { "aria-hidden": "true", className: "w-5 h-5" }),
1142
+ onClick: onPlayPause
1143
+ });
1144
+ }
1145
+ if (onZoomIn) {
1146
+ items.push({
1147
+ key: "zoom-in",
1148
+ label: "Focus",
1149
+ icon: /* @__PURE__ */ (0, import_jsx_runtime15.jsx)(import_lucide_react7.ZoomIn, { "aria-hidden": "true", className: "w-5 h-5" }),
1150
+ onClick: onZoomIn
1151
+ });
1152
+ }
1153
+ if (onAddChild) {
1154
+ adminItems.push({ key: "add-child", label: "Add", icon: /* @__PURE__ */ (0, import_jsx_runtime15.jsx)(import_lucide_react7.Plus, { "aria-hidden": "true", className: "w-5 h-5" }), onClick: onAddChild });
1155
+ }
1156
+ if (onRename) {
1157
+ adminItems.push({ key: "rename", label: "Rename", icon: /* @__PURE__ */ (0, import_jsx_runtime15.jsx)(import_lucide_react7.Pencil, { "aria-hidden": "true", className: "w-5 h-5" }), onClick: onRename });
1158
+ }
1159
+ if (onChangeParent) {
1160
+ adminItems.push({ key: "change-parent", label: "Parent", icon: /* @__PURE__ */ (0, import_jsx_runtime15.jsx)(import_lucide_react7.MoveUpRight, { "aria-hidden": "true", className: "w-5 h-5" }), onClick: onChangeParent });
1161
+ }
1162
+ if (onToggleSide) {
1163
+ adminItems.push({
1164
+ key: "toggle-side",
1165
+ label: side === "pop" ? "Make core" : "Make pop",
1166
+ icon: /* @__PURE__ */ (0, import_jsx_runtime15.jsx)(import_lucide_react7.ArrowLeftRight, { "aria-hidden": "true", className: "w-5 h-5" }),
1167
+ onClick: onToggleSide,
1168
+ disabled: !!toggleSideDisabledReason,
1169
+ title: toggleSideDisabledReason
1170
+ });
1171
+ }
1172
+ for (const action of additionalActions?.(node) ?? []) {
1173
+ items.push({
1174
+ key: action.key,
1175
+ label: action.label(node),
1176
+ icon: action.icon(node),
1177
+ onClick: (event) => action.onClick(event, node),
1178
+ disabled: action.enabled ? !action.enabled(node) : false
1179
+ });
1180
+ }
1181
+ if (items.length === 0 && (!shareTarget || !shareUrl) && adminItems.length === 0) return null;
1182
+ const renderItems = (list) => list.map((item) => /* @__PURE__ */ (0, import_jsx_runtime15.jsxs)(
1183
+ "button",
1184
+ {
1185
+ type: "button",
1186
+ disabled: item.disabled,
1187
+ title: item.title,
1188
+ onClick: item.onClick,
1189
+ className: "group flex flex-col items-center gap-1 w-16 text-xs text-center text-[var(--color-text)] focus-visible:outline-none disabled:opacity-50",
1190
+ children: [
1191
+ /* @__PURE__ */ (0, import_jsx_runtime15.jsx)("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)] group-disabled:group-hover:bg-[var(--color-surface-muted)]", children: item.icon }),
1192
+ item.label
1193
+ ]
1194
+ },
1195
+ item.key
1196
+ ));
1197
+ return /* @__PURE__ */ (0, import_jsx_runtime15.jsxs)("div", { className: "genre-action-band border-b border-[var(--color-border)]", children: [
1198
+ (items.length > 0 || shareTarget && shareUrl) && /* @__PURE__ */ (0, import_jsx_runtime15.jsxs)("div", { className: "flex flex-wrap gap-1 px-3.5 py-3", children: [
1199
+ renderItems(items),
1200
+ shareTarget && shareUrl && /* @__PURE__ */ (0, import_jsx_runtime15.jsx)(ShareActionButton, { target: shareTarget, url: shareUrl })
1201
+ ] }),
1202
+ adminItems.length > 0 && /* @__PURE__ */ (0, import_jsx_runtime15.jsxs)(
1203
+ "section",
1204
+ {
1205
+ "aria-label": "Admin actions",
1206
+ className: `px-3.5 py-3 ${items.length > 0 ? "border-t border-[var(--color-border)]" : ""}`,
1207
+ children: [
1208
+ /* @__PURE__ */ (0, import_jsx_runtime15.jsx)("h3", { className: "mb-2 text-xs font-semibold uppercase tracking-wide text-[var(--color-text-muted)]", children: "Admin" }),
1209
+ /* @__PURE__ */ (0, import_jsx_runtime15.jsx)("div", { className: "flex flex-wrap gap-1", children: renderItems(adminItems) })
1210
+ ]
1211
+ }
1212
+ )
1213
+ ] });
1214
+ }
1215
+
1216
+ // src/genre-tree/GenreDetailExtras.tsx
1217
+ var import_ui5 = require("@behindthemusictree/ui");
1218
+ var import_genre_tree9 = require("@behindthemusictree/app-kit/genre-tree");
1219
+
1220
+ // src/genre-tree/ClampedText.tsx
1221
+ var import_react9 = require("react");
1222
+ var import_jsx_runtime16 = require("react/jsx-runtime");
1223
+ function ClampedText({ children, className: className2 = "" }) {
1224
+ const ref = (0, import_react9.useRef)(null);
1225
+ const [expanded, setExpanded] = (0, import_react9.useState)(false);
1226
+ const [overflows, setOverflows] = (0, import_react9.useState)(false);
1227
+ (0, import_react9.useLayoutEffect)(() => {
1228
+ const element = ref.current;
1229
+ if (!element || expanded) return;
1230
+ setOverflows(element.scrollHeight > element.clientHeight);
1231
+ }, [children, expanded]);
1232
+ return /* @__PURE__ */ (0, import_jsx_runtime16.jsxs)(import_jsx_runtime16.Fragment, { children: [
1233
+ /* @__PURE__ */ (0, import_jsx_runtime16.jsx)("p", { ref, className: `${className2} ${expanded ? "" : "line-clamp-3"}`, children }),
1234
+ (overflows || expanded) && /* @__PURE__ */ (0, import_jsx_runtime16.jsx)(
1235
+ "button",
1236
+ {
1237
+ type: "button",
1238
+ "aria-expanded": expanded,
1239
+ onClick: () => setExpanded((value) => !value),
1240
+ className: "mt-1 text-xs text-[var(--color-text-muted)] underline hover:text-[var(--color-text)]",
1241
+ children: expanded ? "Show less" : "Show more"
1242
+ }
1243
+ )
1244
+ ] });
1245
+ }
1246
+
1247
+ // src/genre-tree/GenrePlaylistTracks.tsx
1248
+ var import_react10 = require("react");
1249
+ var import_react_query = require("@tanstack/react-query");
1250
+ var import_zod = require("zod");
1251
+ var import_fa2 = require("react-icons/fa");
1252
+ var import_ui4 = require("@behindthemusictree/ui");
1253
+ var import_auth = require("@behindthemusictree/app-kit/auth");
1254
+ var import_transport3 = require("@behindthemusictree/app-kit/transport");
1255
+ var import_transport4 = require("@behindthemusictree/app-kit/transport");
1256
+ var import_transport5 = require("@behindthemusictree/app-kit/transport");
1257
+ var import_player4 = require("@behindthemusictree/app-kit/player");
1258
+ var import_player5 = require("@behindthemusictree/app-kit/player");
1259
+ var import_popup4 = require("@behindthemusictree/app-kit/popup");
1260
+ var import_transport6 = require("@behindthemusictree/app-kit/transport");
1261
+ var import_genre_tree4 = require("@behindthemusictree/app-kit/genre-tree");
1262
+ var import_genre_tree5 = require("@behindthemusictree/app-kit/genre-tree");
1263
+ var import_genre_tree6 = require("@behindthemusictree/app-kit/genre-tree");
1264
+ var import_genre_tree7 = require("@behindthemusictree/app-kit/genre-tree");
1265
+
1266
+ // src/genre-tree/MusicBrainzRecordingLink.tsx
1267
+ var import_genre_tree3 = require("@behindthemusictree/app-kit/genre-tree");
1268
+ var import_jsx_runtime17 = require("react/jsx-runtime");
1269
+ function MusicBrainzRecordingLink({
1270
+ song,
1271
+ children,
1272
+ className: className2 = ""
1273
+ }) {
1274
+ const url = (0, import_genre_tree3.musicBrainzRecordingUrl)(song);
1275
+ if (!url) return /* @__PURE__ */ (0, import_jsx_runtime17.jsx)(import_jsx_runtime17.Fragment, { children });
1276
+ return /* @__PURE__ */ (0, import_jsx_runtime17.jsx)(
1277
+ "a",
1278
+ {
1279
+ href: url,
1280
+ target: "_blank",
1281
+ rel: "noopener noreferrer",
1282
+ "aria-label": `View ${song.title} on MusicBrainz`,
1283
+ className: className2,
1284
+ onClick: (event) => event.stopPropagation(),
1285
+ children
1286
+ }
1287
+ );
1288
+ }
1289
+
1290
+ // src/genre-tree/GenrePlaylistTracks.tsx
1291
+ var import_genre_tree8 = require("@behindthemusictree/app-kit/genre-tree");
1292
+ var import_track_noun4 = require("@behindthemusictree/app-kit/track-noun");
1293
+ var import_jsx_runtime18 = require("react/jsx-runtime");
1294
+ var CATALOGUE_PAGE_SIZE = 100;
1295
+ var SongPageSchema = (0, import_transport4.PaginatedResponseSchema)(import_genre_tree6.SongDetailedSchema).extend({
1296
+ overallTotal: import_zod.z.number().nullable(),
1297
+ page: import_zod.z.number().nullable(),
1298
+ totalPages: import_zod.z.number().nullable()
1299
+ });
1300
+ var TEXT_CELL = "min-w-0 flex-1 truncate [@container(min-width:40rem)]:flex [@container(min-width:40rem)]:gap-2";
1301
+ var TITLE_CELL = "[@container(min-width:40rem)]:flex-[2] [@container(min-width:40rem)]:truncate";
1302
+ var COLUMN_CELL = "[@container(min-width:40rem)]:flex-1 [@container(min-width:40rem)]:truncate";
1303
+ var ALBUM_CELL = `hidden [@container(min-width:40rem)]:block ${COLUMN_CELL}`;
1304
+ var SEPARATOR = "[@container(min-width:40rem)]:hidden";
1305
+ var nextCursor = (next) => {
1306
+ const cursor = new URL(next).searchParams.get("cursor");
1307
+ if (!cursor) throw new Error(`Songs next link has no cursor: ${next}`);
1308
+ return cursor;
1309
+ };
1310
+ function GenrePlaylistTracks({
1311
+ genreUuid,
1312
+ genrePlaylist,
1313
+ scope,
1314
+ getBackendBaseUrl,
1315
+ renderArtistLink,
1316
+ renderSongLeading,
1317
+ renderSongActions,
1318
+ renderTracksToolbar,
1319
+ artist = null
1320
+ }) {
1321
+ const { fetchGenrePlaylistTracksPage, playNewTrackListFromGenrePlaylist, playNewTrackListFromArtist } = (0, import_genre_tree7.useTrackList)();
1322
+ const { session, sessionRestored } = (0, import_auth.useSession)();
1323
+ const { playerTrackObject, playState, handlePlayPauseAction } = (0, import_player4.usePlayer)();
1324
+ const { showPopup } = (0, import_popup4.usePopup)();
1325
+ const trackNoun = (0, import_track_noun4.useTrackNoun)();
1326
+ const { uuid } = genrePlaylist;
1327
+ const { fetch } = (0, import_transport3.useFetchWrapper)(getBackendBaseUrl);
1328
+ const { data, isPending, isError, refetch, hasNextPage, isFetchingNextPage, fetchNextPage } = (0, import_react_query.useInfiniteQuery)({
1329
+ queryKey: scope === "me" ? import_genre_tree4.genrePlaylistQueryKeys.me.tracks(uuid) : artist ? [...import_genre_tree5.libraryQueryKeys.reference.songs.byGenre(getBackendBaseUrl(), genreUuid), "artist", artist.uuid] : import_genre_tree5.libraryQueryKeys.reference.songs.byGenre(getBackendBaseUrl(), genreUuid),
1330
+ queryFn: async ({ pageParam }) => {
1331
+ if (scope === "me") return fetchGenrePlaylistTracksPage(uuid, scope, Number(pageParam));
1332
+ const response = await fetch(
1333
+ import_genre_tree5.libraryEndpoints.reference.songs.list(),
1334
+ true,
1335
+ false,
1336
+ {},
1337
+ { genre: genreUuid, ...artist && { artist: artist.uuid }, pageSize: CATALOGUE_PAGE_SIZE, withTotal: false, ...pageParam && { cursor: pageParam } }
1338
+ );
1339
+ const parsed = (0, import_transport5.parseWithLog)(SongPageSchema, response, "GenrePlaylistTracks");
1340
+ return { tracks: parsed.results, total: parsed.overallTotal, nextPage: parsed.next ? nextCursor(parsed.next) : null };
1341
+ },
1342
+ initialPageParam: scope === "me" ? 1 : "",
1343
+ getNextPageParam: (lastPage) => lastPage.nextPage ?? void 0,
1344
+ enabled: scope !== "me" || sessionRestored && !!session?.accessToken
1345
+ });
1346
+ const sentinelRef = (0, import_react10.useRef)(null);
1347
+ (0, import_react10.useEffect)(() => {
1348
+ const sentinel = sentinelRef.current;
1349
+ if (!hasNextPage || isFetchingNextPage || !sentinel) return;
1350
+ const observer = new IntersectionObserver((entries) => {
1351
+ if (entries.some((entry) => entry.isIntersecting)) {
1352
+ void fetchNextPage();
1353
+ }
1354
+ });
1355
+ observer.observe(sentinel);
1356
+ return () => observer.disconnect();
1357
+ }, [hasNextPage, isFetchingNextPage, fetchNextPage]);
1358
+ const pages = data?.pages ?? [];
1359
+ const tracks = pages.flatMap((page) => page.tracks);
1360
+ const total = pages[0]?.total ?? void 0;
1361
+ const play = (startSongUuid) => {
1362
+ const started = artist ? playNewTrackListFromArtist(artist, scope, { startSongUuid, ordering: "title", genreUuid }) : playNewTrackListFromGenrePlaylist(genrePlaylist, scope, { startSongUuid });
1363
+ started.catch((error) => {
1364
+ console.error("Failed to play genre playlist:", error);
1365
+ showPopup(/* @__PURE__ */ (0, import_jsx_runtime18.jsx)(InternalErrorPopup, { errorCode: import_transport6.ErrorCode.CLIENT_UNKNOWN }));
1366
+ });
1367
+ };
1368
+ return /* @__PURE__ */ (0, import_jsx_runtime18.jsxs)("div", { className: "gtv-info-panel-children", children: [
1369
+ /* @__PURE__ */ (0, import_jsx_runtime18.jsxs)("span", { className: "gtv-info-panel-children-title", children: [
1370
+ (0, import_track_noun4.capitalize)(trackNoun.plural),
1371
+ total !== void 0 && ` (${total})`
1372
+ ] }),
1373
+ renderTracksToolbar?.(),
1374
+ isPending ? /* @__PURE__ */ (0, import_jsx_runtime18.jsx)(import_ui4.Skeleton, { className: "h-4 w-3/4", "aria-busy": "true" }) : isError && tracks.length === 0 ? /* @__PURE__ */ (0, import_jsx_runtime18.jsxs)("p", { role: "alert", children: [
1375
+ "Couldn't load ",
1376
+ trackNoun.plural,
1377
+ ".",
1378
+ " ",
1379
+ /* @__PURE__ */ (0, import_jsx_runtime18.jsx)("button", { type: "button", className: "underline", onClick: () => void refetch(), children: "Retry" })
1380
+ ] }) : tracks.length === 0 ? /* @__PURE__ */ (0, import_jsx_runtime18.jsx)("p", { children: "\u2014" }) : /* @__PURE__ */ (0, import_jsx_runtime18.jsxs)("ul", { className: "mt-1 max-h-64 overflow-y-auto text-[0.8125rem] leading-[1.45] [container-type:inline-size]", "aria-label": `${genrePlaylist.name} ${trackNoun.plural}`, children: [
1381
+ tracks.map((track, index) => {
1382
+ const label = track.artists?.length ? `${track.title} \u2014 ${track.artists.map((artist2) => artist2.name).join(", ")}` : track.title;
1383
+ const isCurrent = playerTrackObject?.track.id === track.uuid;
1384
+ const isPlaying = isCurrent && playState === import_player5.PlayStates.PLAYING;
1385
+ const text = /* @__PURE__ */ (0, import_jsx_runtime18.jsxs)("span", { className: TEXT_CELL, children: [
1386
+ /* @__PURE__ */ (0, import_jsx_runtime18.jsx)("span", { className: TITLE_CELL, children: (0, import_genre_tree6.isSong)(track) ? /* @__PURE__ */ (0, import_jsx_runtime18.jsx)(MusicBrainzRecordingLink, { song: track, className: "hover:underline focus-visible:underline", children: track.title }) : track.title }),
1387
+ track.artists?.length ? /* @__PURE__ */ (0, import_jsx_runtime18.jsx)("span", { className: SEPARATOR, children: " \u2014 " }) : null,
1388
+ /* @__PURE__ */ (0, import_jsx_runtime18.jsx)("span", { className: COLUMN_CELL, children: track.artists?.length ? /* @__PURE__ */ (0, import_jsx_runtime18.jsx)(import_genre_tree8.ArtistNames, { artists: track.artists, renderArtistLink }) : null }),
1389
+ track.album && /* @__PURE__ */ (0, import_jsx_runtime18.jsx)("span", { className: ALBUM_CELL, children: track.album.name })
1390
+ ] });
1391
+ const leading = renderSongLeading?.(track, index);
1392
+ const actions = renderSongActions?.(track);
1393
+ const isUnplayable = (0, import_genre_tree6.isSong)(track) && (0, import_genre_tree6.songUnplayableReason)(track) !== null;
1394
+ if (isUnplayable) {
1395
+ return /* @__PURE__ */ (0, import_jsx_runtime18.jsxs)("li", { className: "flex min-h-8 items-center gap-1 px-1 opacity-50", children: [
1396
+ leading,
1397
+ /* @__PURE__ */ (0, import_jsx_runtime18.jsx)(
1398
+ "button",
1399
+ {
1400
+ type: "button",
1401
+ disabled: true,
1402
+ "aria-disabled": "true",
1403
+ "aria-label": label,
1404
+ className: "flex h-6 w-6 shrink-0 items-center justify-center rounded-full opacity-0 cursor-not-allowed",
1405
+ children: /* @__PURE__ */ (0, import_jsx_runtime18.jsx)(import_fa2.FaPlay, { size: 10 })
1406
+ }
1407
+ ),
1408
+ text,
1409
+ actions
1410
+ ] }, track.uuid);
1411
+ }
1412
+ return /* @__PURE__ */ (0, import_jsx_runtime18.jsxs)(
1413
+ "li",
1414
+ {
1415
+ className: `group flex min-h-8 items-center gap-1 rounded px-1 hover:bg-[var(--color-neutral-100)] ${isCurrent ? "font-medium text-[var(--color-primary)]" : ""}`,
1416
+ children: [
1417
+ leading,
1418
+ /* @__PURE__ */ (0, import_jsx_runtime18.jsx)(
1419
+ "button",
1420
+ {
1421
+ type: "button",
1422
+ "aria-label": `${isPlaying ? "Pause" : "Play"} ${label}`,
1423
+ className: `flex h-6 w-6 shrink-0 items-center justify-center rounded-full cursor-pointer group-hover:opacity-100 focus-visible:opacity-100 ${isCurrent && playState !== import_player5.PlayStates.STOPPED ? "opacity-100" : "opacity-0 [@media(pointer:coarse)]:opacity-100"}`,
1424
+ onClick: () => isCurrent ? handlePlayPauseAction() : play(track.uuid),
1425
+ children: isCurrent && playState === import_player5.PlayStates.LOADING ? /* @__PURE__ */ (0, import_jsx_runtime18.jsx)(import_ui4.RingLoader, { size: 10 }) : isPlaying ? /* @__PURE__ */ (0, import_jsx_runtime18.jsx)(import_fa2.FaPause, { size: 10 }) : /* @__PURE__ */ (0, import_jsx_runtime18.jsx)(import_fa2.FaPlay, { size: 10 })
1426
+ }
1427
+ ),
1428
+ text,
1429
+ actions
1430
+ ]
1431
+ },
1432
+ track.uuid
1433
+ );
1434
+ }),
1435
+ hasNextPage && /* @__PURE__ */ (0, import_jsx_runtime18.jsx)("li", { ref: sentinelRef, "aria-hidden": "true", className: "h-4" })
1436
+ ] })
1437
+ ] });
1438
+ }
1439
+
1440
+ // src/genre-tree/GenreDetailExtras.tsx
1441
+ var import_track_noun5 = require("@behindthemusictree/app-kit/track-noun");
1442
+ var import_si2 = require("react-icons/si");
1443
+ var import_jsx_runtime19 = require("react/jsx-runtime");
1444
+ function GenreDetailExtras({
1445
+ genreUuid,
1446
+ genrePlaylist,
1447
+ wikidataId,
1448
+ scope,
1449
+ getBackendBaseUrl,
1450
+ criteriaOverviewSchema,
1451
+ renderGenreDetailExtras,
1452
+ renderArtistLink,
1453
+ renderSummary,
1454
+ renderEssentialTracks,
1455
+ family,
1456
+ ...trackSlots
1457
+ }) {
1458
+ const trackNoun = (0, import_track_noun5.useTrackNoun)();
1459
+ const { data: overview, isPending } = (0, import_genre_tree9.useFetchGenreOverview)(
1460
+ genreUuid,
1461
+ scope,
1462
+ getBackendBaseUrl,
1463
+ criteriaOverviewSchema
1464
+ );
1465
+ if (isPending) {
1466
+ return /* @__PURE__ */ (0, import_jsx_runtime19.jsxs)(import_jsx_runtime19.Fragment, { children: [
1467
+ /* @__PURE__ */ (0, import_jsx_runtime19.jsxs)("div", { className: "gtv-info-panel-children", "aria-busy": "true", "data-testid": "genre-detail-extras-skeleton", children: [
1468
+ /* @__PURE__ */ (0, import_jsx_runtime19.jsx)("span", { className: "gtv-info-panel-children-title", children: "Summary" }),
1469
+ /* @__PURE__ */ (0, import_jsx_runtime19.jsx)(import_ui5.Skeleton, { className: "h-4 w-3/4" })
1470
+ ] }),
1471
+ family
1472
+ ] });
1473
+ }
1474
+ if (!overview) return /* @__PURE__ */ (0, import_jsx_runtime19.jsx)(import_jsx_runtime19.Fragment, { children: family });
1475
+ const { summary, summarySourceUrl, essentialSongs } = overview;
1476
+ const wikidataUrl = wikidataId ? `https://www.wikidata.org/wiki/${encodeURIComponent(wikidataId)}` : null;
1477
+ return /* @__PURE__ */ (0, import_jsx_runtime19.jsxs)(import_jsx_runtime19.Fragment, { children: [
1478
+ (summarySourceUrl || wikidataUrl) && /* @__PURE__ */ (0, import_jsx_runtime19.jsxs)("div", { className: "gtv-info-panel-children text-[0.8125rem] leading-[1.45] text-[var(--color-text)]", children: [
1479
+ /* @__PURE__ */ (0, import_jsx_runtime19.jsx)("span", { className: "gtv-info-panel-children-title", children: "Links" }),
1480
+ /* @__PURE__ */ (0, import_jsx_runtime19.jsxs)("div", { className: "mt-1 flex flex-wrap gap-x-3 gap-y-1 text-xs", children: [
1481
+ summarySourceUrl && /* @__PURE__ */ (0, import_jsx_runtime19.jsxs)(
1482
+ "a",
1483
+ {
1484
+ href: summarySourceUrl,
1485
+ target: "_blank",
1486
+ rel: "noopener noreferrer",
1487
+ className: "inline-flex items-center gap-1.5 text-[var(--color-text-muted)] hover:text-[var(--color-text)]",
1488
+ children: [
1489
+ /* @__PURE__ */ (0, import_jsx_runtime19.jsx)(import_si2.SiWikipedia, { "aria-hidden": "true", className: "h-3.5 w-3.5" }),
1490
+ "Wikipedia"
1491
+ ]
1492
+ }
1493
+ ),
1494
+ wikidataUrl && /* @__PURE__ */ (0, import_jsx_runtime19.jsxs)(
1495
+ "a",
1496
+ {
1497
+ href: wikidataUrl,
1498
+ target: "_blank",
1499
+ rel: "noopener noreferrer",
1500
+ className: "inline-flex items-center gap-1.5 text-[var(--color-text-muted)] hover:text-[var(--color-text)]",
1501
+ children: [
1502
+ /* @__PURE__ */ (0, import_jsx_runtime19.jsx)(import_si2.SiWikidata, { "aria-hidden": "true", className: "h-3.5 w-3.5" }),
1503
+ "Wikidata"
1504
+ ]
1505
+ }
1506
+ )
1507
+ ] })
1508
+ ] }),
1509
+ renderSummary ? renderSummary(overview) : /* @__PURE__ */ (0, import_jsx_runtime19.jsxs)("div", { className: "gtv-info-panel-children text-[0.8125rem] leading-[1.45] text-[var(--color-text)]", children: [
1510
+ /* @__PURE__ */ (0, import_jsx_runtime19.jsx)("span", { className: "gtv-info-panel-children-title", children: "Summary" }),
1511
+ /* @__PURE__ */ (0, import_jsx_runtime19.jsx)(ClampedText, { className: "mt-1", children: summary ?? "\u2014" }, genreUuid)
1512
+ ] }),
1513
+ family,
1514
+ renderGenreDetailExtras?.(overview),
1515
+ renderEssentialTracks ? renderEssentialTracks(overview) : essentialSongs && /* @__PURE__ */ (0, import_jsx_runtime19.jsxs)("div", { className: "gtv-info-panel-children text-[0.8125rem] leading-[1.45] text-[var(--color-text)]", children: [
1516
+ /* @__PURE__ */ (0, import_jsx_runtime19.jsxs)("span", { className: "gtv-info-panel-children-title", children: [
1517
+ "Essential ",
1518
+ trackNoun.plural
1519
+ ] }),
1520
+ essentialSongs.length > 0 ? /* @__PURE__ */ (0, import_jsx_runtime19.jsx)("ul", { className: "mt-1 divide-y divide-[var(--color-border)]", children: essentialSongs.map((track) => /* @__PURE__ */ (0, import_jsx_runtime19.jsx)("li", { className: "py-1", children: track.title }, track.uuid)) }) : /* @__PURE__ */ (0, import_jsx_runtime19.jsx)("p", { className: "mt-1", children: "\u2014" })
1521
+ ] }),
1522
+ /* @__PURE__ */ (0, import_jsx_runtime19.jsx)(
1523
+ GenrePlaylistTracks,
1524
+ {
1525
+ genreUuid,
1526
+ genrePlaylist,
1527
+ scope,
1528
+ getBackendBaseUrl,
1529
+ renderArtistLink,
1530
+ ...trackSlots
1531
+ }
1532
+ )
1533
+ ] });
1534
+ }
1535
+
1536
+ // src/genre-tree/GenreInfoPanelBody.tsx
1537
+ var import_jsx_runtime20 = require("react/jsx-runtime");
1538
+ var import_react11 = require("react");
1539
+ function GenreNodeActionBand({ genrePlaylist, playback, ...bandProps }) {
1540
+ const { isPlaying, setIsPlaying } = (0, import_player6.usePlayer)();
1541
+ const isCurrentTrackList = playback.playingNodeId === genrePlaylist.uuid;
1542
+ const handlePlayPause = () => {
1543
+ if (isCurrentTrackList) {
1544
+ setIsPlaying(!isPlaying);
1545
+ return;
1546
+ }
1547
+ playback.play(genrePlaylist).catch((error) => {
1548
+ console.error("Failed to load genre playlist tracks:", error);
1549
+ });
1414
1550
  };
1415
- return /* @__PURE__ */ (0, import_jsx_runtime18.jsxs)(
1416
- "button",
1551
+ return /* @__PURE__ */ (0, import_jsx_runtime20.jsx)(
1552
+ GenreActionBand,
1417
1553
  {
1418
- type: "button",
1419
- onClick: openSharePopup,
1420
- className: "group flex flex-col items-center gap-1 w-16 text-xs text-center text-[var(--color-text)] focus-visible:outline-none",
1421
- "aria-label": `Share ${target.name}`,
1422
- title: `Share ${target.name}`,
1423
- children: [
1424
- /* @__PURE__ */ (0, import_jsx_runtime18.jsx)("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: /* @__PURE__ */ (0, import_jsx_runtime18.jsx)(import_lucide_react6.Share2, { "aria-hidden": "true", className: "w-5 h-5" }) }),
1425
- "Share"
1426
- ]
1554
+ ...bandProps,
1555
+ onPlayPause: genrePlaylist.tracksCount > 0 ? handlePlayPause : void 0,
1556
+ isPlaying: isCurrentTrackList && isPlaying
1427
1557
  }
1428
1558
  );
1429
1559
  }
1430
-
1431
- // src/genre-tree/GenreActionBand.tsx
1432
- var import_jsx_runtime19 = require("react/jsx-runtime");
1433
- function GenreActionBand({
1434
- node,
1435
- onPlayPause,
1436
- isPlaying = false,
1437
- onZoomIn,
1438
- onAddChild,
1439
- onRename,
1440
- onChangeParent,
1441
- side,
1442
- onToggleSide,
1443
- toggleSideDisabledReason,
1444
- additionalActions,
1445
- shareTarget,
1446
- shareUrl
1560
+ function GenreInfoPanelBody({
1561
+ toolbar,
1562
+ family,
1563
+ genreUuid,
1564
+ genrePlaylist,
1565
+ wikidataId,
1566
+ scope,
1567
+ getBackendBaseUrl,
1568
+ playback,
1569
+ band,
1570
+ extras
1447
1571
  }) {
1448
- const items = [];
1449
- const adminItems = [];
1450
- if (onPlayPause) {
1451
- items.push({
1452
- key: "play-pause",
1453
- label: isPlaying ? "Pause" : "Play",
1454
- icon: isPlaying ? /* @__PURE__ */ (0, import_jsx_runtime19.jsx)(import_lucide_react7.Pause, { "aria-hidden": "true", className: "w-5 h-5" }) : /* @__PURE__ */ (0, import_jsx_runtime19.jsx)(import_lucide_react7.Play, { "aria-hidden": "true", className: "w-5 h-5" }),
1455
- onClick: onPlayPause
1456
- });
1457
- }
1458
- if (onZoomIn) {
1459
- items.push({
1460
- key: "zoom-in",
1461
- label: "Focus",
1462
- icon: /* @__PURE__ */ (0, import_jsx_runtime19.jsx)(import_lucide_react7.ZoomIn, { "aria-hidden": "true", className: "w-5 h-5" }),
1463
- onClick: onZoomIn
1464
- });
1465
- }
1466
- if (onAddChild) {
1467
- adminItems.push({ key: "add-child", label: "Add", icon: /* @__PURE__ */ (0, import_jsx_runtime19.jsx)(import_lucide_react7.Plus, { "aria-hidden": "true", className: "w-5 h-5" }), onClick: onAddChild });
1468
- }
1469
- if (onRename) {
1470
- adminItems.push({ key: "rename", label: "Rename", icon: /* @__PURE__ */ (0, import_jsx_runtime19.jsx)(import_lucide_react7.Pencil, { "aria-hidden": "true", className: "w-5 h-5" }), onClick: onRename });
1471
- }
1472
- if (onChangeParent) {
1473
- adminItems.push({ key: "change-parent", label: "Parent", icon: /* @__PURE__ */ (0, import_jsx_runtime19.jsx)(import_lucide_react7.MoveUpRight, { "aria-hidden": "true", className: "w-5 h-5" }), onClick: onChangeParent });
1474
- }
1475
- if (onToggleSide) {
1476
- adminItems.push({
1477
- key: "toggle-side",
1478
- label: side === "pop" ? "Make core" : "Make pop",
1479
- icon: /* @__PURE__ */ (0, import_jsx_runtime19.jsx)(import_lucide_react7.ArrowLeftRight, { "aria-hidden": "true", className: "w-5 h-5" }),
1480
- onClick: onToggleSide,
1481
- disabled: !!toggleSideDisabledReason,
1482
- title: toggleSideDisabledReason
1483
- });
1484
- }
1485
- for (const action of additionalActions?.(node) ?? []) {
1486
- items.push({
1487
- key: action.key,
1488
- label: action.label(node),
1489
- icon: action.icon(node),
1490
- onClick: (event) => action.onClick(event, node),
1491
- disabled: action.enabled ? !action.enabled(node) : false
1492
- });
1493
- }
1494
- if (items.length === 0 && (!shareTarget || !shareUrl) && adminItems.length === 0) return null;
1495
- const renderItems = (list) => list.map((item) => /* @__PURE__ */ (0, import_jsx_runtime19.jsxs)(
1496
- "button",
1497
- {
1498
- type: "button",
1499
- disabled: item.disabled,
1500
- title: item.title,
1501
- onClick: item.onClick,
1502
- className: "group flex flex-col items-center gap-1 w-16 text-xs text-center text-[var(--color-text)] focus-visible:outline-none disabled:opacity-50",
1503
- children: [
1504
- /* @__PURE__ */ (0, import_jsx_runtime19.jsx)("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)] group-disabled:group-hover:bg-[var(--color-surface-muted)]", children: item.icon }),
1505
- item.label
1506
- ]
1507
- },
1508
- item.key
1509
- ));
1510
- return /* @__PURE__ */ (0, import_jsx_runtime19.jsxs)("div", { className: "genre-action-band border-b border-[var(--color-border)]", children: [
1511
- (items.length > 0 || shareTarget && shareUrl) && /* @__PURE__ */ (0, import_jsx_runtime19.jsxs)("div", { className: "flex flex-wrap gap-1 px-3.5 py-3", children: [
1512
- renderItems(items),
1513
- shareTarget && shareUrl && /* @__PURE__ */ (0, import_jsx_runtime19.jsx)(ShareActionButton, { target: shareTarget, url: shareUrl })
1514
- ] }),
1515
- adminItems.length > 0 && /* @__PURE__ */ (0, import_jsx_runtime19.jsxs)(
1516
- "section",
1572
+ return /* @__PURE__ */ (0, import_jsx_runtime20.jsxs)(import_jsx_runtime20.Fragment, { children: [
1573
+ toolbar,
1574
+ /* @__PURE__ */ (0, import_jsx_runtime20.jsx)(GenreNodeActionBand, { ...band, genrePlaylist, playback }),
1575
+ /* @__PURE__ */ (0, import_react11.createElement)(
1576
+ GenreDetailExtras,
1517
1577
  {
1518
- "aria-label": "Admin actions",
1519
- className: `px-3.5 py-3 ${items.length > 0 ? "border-t border-[var(--color-border)]" : ""}`,
1520
- children: [
1521
- /* @__PURE__ */ (0, import_jsx_runtime19.jsx)("h3", { className: "mb-2 text-xs font-semibold uppercase tracking-wide text-[var(--color-text-muted)]", children: "Admin" }),
1522
- /* @__PURE__ */ (0, import_jsx_runtime19.jsx)("div", { className: "flex flex-wrap gap-1", children: renderItems(adminItems) })
1523
- ]
1578
+ ...extras,
1579
+ key: genreUuid,
1580
+ family,
1581
+ genreUuid,
1582
+ genrePlaylist,
1583
+ wikidataId,
1584
+ scope,
1585
+ getBackendBaseUrl
1524
1586
  }
1525
1587
  )
1526
1588
  ] });
@@ -1529,14 +1591,14 @@ function GenreActionBand({
1529
1591
  // src/genre-tree/PanelEdgeTab.tsx
1530
1592
  var import_lucide_react8 = require("lucide-react");
1531
1593
  var import_tailwind_merge = require("tailwind-merge");
1532
- var import_jsx_runtime20 = require("react/jsx-runtime");
1594
+ var import_jsx_runtime21 = require("react/jsx-runtime");
1533
1595
  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)]";
1534
1596
  function PanelEdgeTab({ side, edge, center, controls, className: className2 }) {
1535
1597
  if (!edge && !center) return null;
1536
1598
  const isLeft = side === "left";
1537
1599
  const EdgeIcon = isLeft ? import_lucide_react8.ChevronLeft : import_lucide_react8.ChevronRight;
1538
1600
  const CenterIcon = isLeft ? import_lucide_react8.ChevronRight : import_lucide_react8.ChevronLeft;
1539
- const renderButton = (action, icon) => /* @__PURE__ */ (0, import_jsx_runtime20.jsx)(
1601
+ const renderButton = (action, icon) => /* @__PURE__ */ (0, import_jsx_runtime21.jsx)(
1540
1602
  "button",
1541
1603
  {
1542
1604
  type: "button",
@@ -1549,10 +1611,10 @@ function PanelEdgeTab({ side, edge, center, controls, className: className2 }) {
1549
1611
  children: icon
1550
1612
  }
1551
1613
  );
1552
- const edgeButton = edge && renderButton(edge, /* @__PURE__ */ (0, import_jsx_runtime20.jsx)(EdgeIcon, { "aria-hidden": "true", className: "h-5 w-5" }));
1553
- const centerButton = center && renderButton(center, /* @__PURE__ */ (0, import_jsx_runtime20.jsx)(CenterIcon, { "aria-hidden": "true", className: "h-5 w-5" }));
1614
+ const edgeButton = edge && renderButton(edge, /* @__PURE__ */ (0, import_jsx_runtime21.jsx)(EdgeIcon, { "aria-hidden": "true", className: "h-5 w-5" }));
1615
+ const centerButton = center && renderButton(center, /* @__PURE__ */ (0, import_jsx_runtime21.jsx)(CenterIcon, { "aria-hidden": "true", className: "h-5 w-5" }));
1554
1616
  const buttons = isLeft ? [edgeButton, centerButton] : [centerButton, edgeButton];
1555
- return /* @__PURE__ */ (0, import_jsx_runtime20.jsxs)(
1617
+ return /* @__PURE__ */ (0, import_jsx_runtime21.jsxs)(
1556
1618
  "div",
1557
1619
  {
1558
1620
  className: (0, import_tailwind_merge.twMerge)(
@@ -1562,17 +1624,30 @@ function PanelEdgeTab({ side, edge, center, controls, className: className2 }) {
1562
1624
  ),
1563
1625
  children: [
1564
1626
  buttons[0],
1565
- buttons[0] && buttons[1] && /* @__PURE__ */ (0, import_jsx_runtime20.jsx)("div", { role: "presentation", className: "h-px shrink-0 bg-[var(--color-border)]" }),
1627
+ buttons[0] && buttons[1] && /* @__PURE__ */ (0, import_jsx_runtime21.jsx)("div", { role: "presentation", className: "h-px shrink-0 bg-[var(--color-border)]" }),
1566
1628
  buttons[1]
1567
1629
  ]
1568
1630
  }
1569
1631
  );
1570
1632
  }
1571
1633
 
1634
+ // src/genre-tree/useLeftDetailPanelLayout.ts
1635
+ var import_react12 = require("react");
1636
+ function useLeftDetailPanelLayout(isPanelShown, enabled = true) {
1637
+ (0, import_react12.useEffect)(() => {
1638
+ if (!enabled || isPanelShown) return void 0;
1639
+ const root = document.documentElement;
1640
+ root.style.setProperty("--layout-left-w", "0px");
1641
+ return () => {
1642
+ root.style.removeProperty("--layout-left-w");
1643
+ };
1644
+ }, [enabled, isPanelShown]);
1645
+ }
1646
+
1572
1647
  // src/genre-tree/ChangeParentPopup.tsx
1573
- var import_react11 = require("react");
1648
+ var import_react13 = require("react");
1574
1649
  var import_genre_tree10 = require("@behindthemusictree/app-kit/genre-tree");
1575
- var import_jsx_runtime21 = require("react/jsx-runtime");
1650
+ var import_jsx_runtime22 = require("react/jsx-runtime");
1576
1651
  function ChangeParentPopup({
1577
1652
  genrePlaylist,
1578
1653
  genrePlaylists,
@@ -1582,7 +1657,7 @@ function ChangeParentPopup({
1582
1657
  onParentChanged
1583
1658
  }) {
1584
1659
  const { mutate, isPending, formErrors } = (0, import_genre_tree10.useUpdateGenre)(scope, getBackendBaseUrl);
1585
- const candidates = (0, import_react11.useMemo)(
1660
+ const candidates = (0, import_react13.useMemo)(
1586
1661
  () => (0, import_genre_tree10.getReparentCandidates)(genrePlaylists, genrePlaylist.uuid),
1587
1662
  [genrePlaylists, genrePlaylist.uuid]
1588
1663
  );
@@ -1600,19 +1675,19 @@ function ChangeParentPopup({
1600
1675
  }
1601
1676
  );
1602
1677
  };
1603
- (0, import_react11.useEffect)(() => {
1678
+ (0, import_react13.useEffect)(() => {
1604
1679
  const handleKeyDown = (event) => {
1605
1680
  if (event.key === "Escape") onClose();
1606
1681
  };
1607
1682
  window.addEventListener("keydown", handleKeyDown);
1608
1683
  return () => window.removeEventListener("keydown", handleKeyDown);
1609
1684
  }, [onClose]);
1610
- return /* @__PURE__ */ (0, import_jsx_runtime21.jsxs)(BasePopup_default, { title: `Change parent of ${genrePlaylist.name}`, onClose, showCancelButton: true, fitContent: true, children: [
1611
- /* @__PURE__ */ (0, import_jsx_runtime21.jsxs)("p", { className: "mb-3 text-sm text-[var(--color-text-muted)]", children: [
1685
+ return /* @__PURE__ */ (0, import_jsx_runtime22.jsxs)(BasePopup_default, { title: `Change parent of ${genrePlaylist.name}`, onClose, showCancelButton: true, fitContent: true, children: [
1686
+ /* @__PURE__ */ (0, import_jsx_runtime22.jsxs)("p", { className: "mb-3 text-sm text-[var(--color-text-muted)]", children: [
1612
1687
  "Current parent: ",
1613
1688
  genrePlaylist.parent?.name ?? "none"
1614
1689
  ] }),
1615
- /* @__PURE__ */ (0, import_jsx_runtime21.jsx)("div", { "aria-busy": isPending, className: isPending ? "pointer-events-none opacity-60" : void 0, children: /* @__PURE__ */ (0, import_jsx_runtime21.jsx)(
1690
+ /* @__PURE__ */ (0, import_jsx_runtime22.jsx)("div", { "aria-busy": isPending, className: isPending ? "pointer-events-none opacity-60" : void 0, children: /* @__PURE__ */ (0, import_jsx_runtime22.jsx)(
1616
1691
  GenreSearch,
1617
1692
  {
1618
1693
  genrePlaylists: candidates,
@@ -1620,7 +1695,7 @@ function ChangeParentPopup({
1620
1695
  placeholder: "Search a new parent\u2026"
1621
1696
  }
1622
1697
  ) }),
1623
- formErrors.length > 0 && /* @__PURE__ */ (0, import_jsx_runtime21.jsx)("p", { role: "alert", className: "mt-3 text-sm text-[var(--color-red-500)]", children: formErrors.map(({ message }) => message).join(" ") })
1698
+ formErrors.length > 0 && /* @__PURE__ */ (0, import_jsx_runtime22.jsx)("p", { role: "alert", className: "mt-3 text-sm text-[var(--color-red-500)]", children: formErrors.map(({ message }) => message).join(" ") })
1624
1699
  ] });
1625
1700
  }
1626
1701
 
@@ -1635,7 +1710,7 @@ var import_ui7 = require("@behindthemusictree/ui");
1635
1710
  var import_lucide_react9 = require("lucide-react");
1636
1711
  var import_si3 = require("react-icons/si");
1637
1712
  var import_ui6 = require("@behindthemusictree/ui");
1638
- var import_jsx_runtime22 = require("react/jsx-runtime");
1713
+ var import_jsx_runtime23 = require("react/jsx-runtime");
1639
1714
  var LINK_KINDS = {
1640
1715
  official: { label: "Official website", Icon: import_lucide_react9.Globe },
1641
1716
  wikipedia: { label: "Wikipedia", Icon: import_si3.SiWikipedia },
@@ -1672,7 +1747,7 @@ function artistLinks(artist) {
1672
1747
  return links;
1673
1748
  }
1674
1749
  function ArtistLinks({ links }) {
1675
- return /* @__PURE__ */ (0, import_jsx_runtime22.jsx)("ul", { className: "flex flex-wrap gap-1.5", children: links.map(({ key, url, Icon, label }) => /* @__PURE__ */ (0, import_jsx_runtime22.jsx)("li", { children: /* @__PURE__ */ (0, import_jsx_runtime22.jsx)(import_ui6.Chip, { href: url, icon: /* @__PURE__ */ (0, import_jsx_runtime22.jsx)(Icon, { className: "h-3.5 w-3.5 shrink-0", "aria-hidden": "true" }), children: label }) }, key)) });
1750
+ return /* @__PURE__ */ (0, import_jsx_runtime23.jsx)("ul", { className: "flex flex-wrap gap-1.5", children: links.map(({ key, url, Icon, label }) => /* @__PURE__ */ (0, import_jsx_runtime23.jsx)("li", { children: /* @__PURE__ */ (0, import_jsx_runtime23.jsx)(import_ui6.Chip, { href: url, icon: /* @__PURE__ */ (0, import_jsx_runtime23.jsx)(Icon, { className: "h-3.5 w-3.5 shrink-0", "aria-hidden": "true" }), children: label }) }, key)) });
1676
1751
  }
1677
1752
 
1678
1753
  // src/genre-tree/artist/formatYears.ts
@@ -1683,7 +1758,7 @@ function formatYears(beginYear, endYear, ended) {
1683
1758
  }
1684
1759
 
1685
1760
  // src/genre-tree/artist/ArtistHeader.tsx
1686
- var import_jsx_runtime23 = require("react/jsx-runtime");
1761
+ var import_jsx_runtime24 = require("react/jsx-runtime");
1687
1762
  var countryNames = new Intl.DisplayNames(["en"], { type: "region" });
1688
1763
  function ArtistHeader({ artist }) {
1689
1764
  const fields = [];
@@ -1693,35 +1768,35 @@ function ArtistHeader({ artist }) {
1693
1768
  const years = formatYears(artist.beginYear, artist.endYear, artist.ended);
1694
1769
  if (years) fields.push({ label: "Active", value: years });
1695
1770
  const links = artistLinks(artist);
1696
- if (links.length > 0) fields.push({ label: "Links", value: /* @__PURE__ */ (0, import_jsx_runtime23.jsx)(ArtistLinks, { links }) });
1771
+ if (links.length > 0) fields.push({ label: "Links", value: /* @__PURE__ */ (0, import_jsx_runtime24.jsx)(ArtistLinks, { links }) });
1697
1772
  if (fields.length === 0) return null;
1698
- return /* @__PURE__ */ (0, import_jsx_runtime23.jsx)(import_ui7.FieldList, { items: fields });
1773
+ return /* @__PURE__ */ (0, import_jsx_runtime24.jsx)(import_ui7.FieldList, { items: fields });
1699
1774
  }
1700
1775
 
1701
1776
  // src/genre-tree/artist/ArtistPhoto.tsx
1702
- var import_jsx_runtime24 = require("react/jsx-runtime");
1777
+ var import_jsx_runtime25 = require("react/jsx-runtime");
1703
1778
  function ExternalLink({ href, children }) {
1704
- return /* @__PURE__ */ (0, import_jsx_runtime24.jsx)("a", { href, target: "_blank", rel: "noopener noreferrer", className: "underline-offset-2 hover:underline", children });
1779
+ return /* @__PURE__ */ (0, import_jsx_runtime25.jsx)("a", { href, target: "_blank", rel: "noopener noreferrer", className: "underline-offset-2 hover:underline", children });
1705
1780
  }
1706
1781
  function ArtistPhoto({ image, name }) {
1707
- return /* @__PURE__ */ (0, import_jsx_runtime24.jsxs)("figure", { className: "flex flex-col gap-1.5 px-6 py-2.5", children: [
1708
- /* @__PURE__ */ (0, import_jsx_runtime24.jsx)("img", { src: image.url, alt: name, className: "h-64 w-full object-contain bg-[var(--color-surface-muted)] rounded" }),
1709
- /* @__PURE__ */ (0, import_jsx_runtime24.jsxs)("figcaption", { className: "text-xs text-[var(--color-text-muted)]", children: [
1782
+ return /* @__PURE__ */ (0, import_jsx_runtime25.jsxs)("figure", { className: "flex flex-col gap-1.5 px-6 py-2.5", children: [
1783
+ /* @__PURE__ */ (0, import_jsx_runtime25.jsx)("img", { src: image.url, alt: name, className: "h-64 w-full object-contain bg-[var(--color-surface-muted)] rounded" }),
1784
+ /* @__PURE__ */ (0, import_jsx_runtime25.jsxs)("figcaption", { className: "text-xs text-[var(--color-text-muted)]", children: [
1710
1785
  "Photo: ",
1711
- /* @__PURE__ */ (0, import_jsx_runtime24.jsx)(ExternalLink, { href: image.pageUrl, children: image.author ?? "Wikimedia Commons" }),
1786
+ /* @__PURE__ */ (0, import_jsx_runtime25.jsx)(ExternalLink, { href: image.pageUrl, children: image.author ?? "Wikimedia Commons" }),
1712
1787
  ",",
1713
1788
  " ",
1714
- image.licenseUrl ? /* @__PURE__ */ (0, import_jsx_runtime24.jsx)(ExternalLink, { href: image.licenseUrl, children: image.license }) : image.license
1789
+ image.licenseUrl ? /* @__PURE__ */ (0, import_jsx_runtime25.jsx)(ExternalLink, { href: image.licenseUrl, children: image.license }) : image.license
1715
1790
  ] })
1716
1791
  ] });
1717
1792
  }
1718
1793
 
1719
1794
  // src/genre-tree/artist/ArtistMembers.tsx
1720
1795
  var import_ui8 = require("@behindthemusictree/ui");
1721
- var import_jsx_runtime25 = require("react/jsx-runtime");
1796
+ var import_jsx_runtime26 = require("react/jsx-runtime");
1722
1797
  function MembershipList({ title, memberships, renderArtistLink }) {
1723
1798
  if (memberships.length === 0) return null;
1724
- return /* @__PURE__ */ (0, import_jsx_runtime25.jsx)(import_ui8.PanelGroup, { title, children: /* @__PURE__ */ (0, import_jsx_runtime25.jsx)("ul", { children: memberships.map((membership, index) => {
1799
+ return /* @__PURE__ */ (0, import_jsx_runtime26.jsx)(import_ui8.PanelGroup, { title, children: /* @__PURE__ */ (0, import_jsx_runtime26.jsx)("ul", { children: memberships.map((membership, index) => {
1725
1800
  const details = [
1726
1801
  formatYears(membership.beginYear, membership.endYear, membership.ended),
1727
1802
  ...membership.roles
@@ -1729,9 +1804,9 @@ function MembershipList({ title, memberships, renderArtistLink }) {
1729
1804
  const detailsText = details.filter(Boolean).join(" \xB7 ");
1730
1805
  return (
1731
1806
  // A member can have several periods in the same group.
1732
- /* @__PURE__ */ (0, import_jsx_runtime25.jsxs)("li", { className: "truncate py-0.5", title: detailsText || void 0, children: [
1733
- /* @__PURE__ */ (0, import_jsx_runtime25.jsx)("span", { className: "font-medium text-[var(--color-text)]", children: renderArtistLink(membership.artist) }),
1734
- detailsText && /* @__PURE__ */ (0, import_jsx_runtime25.jsxs)("span", { className: "text-[var(--color-text-muted)]", children: [
1807
+ /* @__PURE__ */ (0, import_jsx_runtime26.jsxs)("li", { className: "truncate py-0.5", title: detailsText || void 0, children: [
1808
+ /* @__PURE__ */ (0, import_jsx_runtime26.jsx)("span", { className: "font-medium text-[var(--color-text)]", children: renderArtistLink(membership.artist) }),
1809
+ detailsText && /* @__PURE__ */ (0, import_jsx_runtime26.jsxs)("span", { className: "text-[var(--color-text-muted)]", children: [
1735
1810
  " \u2014 ",
1736
1811
  detailsText
1737
1812
  ] })
@@ -1742,9 +1817,9 @@ function MembershipList({ title, memberships, renderArtistLink }) {
1742
1817
  function ArtistMembers({ title, memberships, renderArtistLink }) {
1743
1818
  if (memberships.length === 0) return null;
1744
1819
  const isCurrent = (membership) => !membership.ended && membership.endYear === null;
1745
- return /* @__PURE__ */ (0, import_jsx_runtime25.jsxs)(import_ui8.PanelSection, { title, children: [
1746
- /* @__PURE__ */ (0, import_jsx_runtime25.jsx)(MembershipList, { title: "Current", memberships: memberships.filter(isCurrent), renderArtistLink }),
1747
- /* @__PURE__ */ (0, import_jsx_runtime25.jsx)(
1820
+ return /* @__PURE__ */ (0, import_jsx_runtime26.jsxs)(import_ui8.PanelSection, { title, children: [
1821
+ /* @__PURE__ */ (0, import_jsx_runtime26.jsx)(MembershipList, { title: "Current", memberships: memberships.filter(isCurrent), renderArtistLink }),
1822
+ /* @__PURE__ */ (0, import_jsx_runtime26.jsx)(
1748
1823
  MembershipList,
1749
1824
  {
1750
1825
  title: "Past",
@@ -1757,7 +1832,7 @@ function ArtistMembers({ title, memberships, renderArtistLink }) {
1757
1832
 
1758
1833
  // src/genre-tree/artist/ArtistDiscography.tsx
1759
1834
  var import_ui9 = require("@behindthemusictree/ui");
1760
- var import_jsx_runtime26 = require("react/jsx-runtime");
1835
+ var import_jsx_runtime27 = require("react/jsx-runtime");
1761
1836
  var GROUPS = ["Albums", "EPs", "Singles", "Live", "Compilations", "Other"];
1762
1837
  var PRIMARY_GROUPS = { album: "Albums", ep: "EPs", single: "Singles" };
1763
1838
  function discographyGroup({ primaryType, secondaryTypes }) {
@@ -1777,55 +1852,55 @@ function byYear(a, b) {
1777
1852
  function ArtistDiscography({ albums }) {
1778
1853
  if (albums.length === 0) return null;
1779
1854
  const sorted = [...albums].sort(byYear);
1780
- return /* @__PURE__ */ (0, import_jsx_runtime26.jsx)(import_ui9.PanelSection, { title: "Discography", children: GROUPS.map((group) => {
1855
+ return /* @__PURE__ */ (0, import_jsx_runtime27.jsx)(import_ui9.PanelSection, { title: "Discography", children: GROUPS.map((group) => {
1781
1856
  const groupAlbums = sorted.filter((album) => discographyGroup(album) === group);
1782
1857
  if (groupAlbums.length === 0) return null;
1783
- return /* @__PURE__ */ (0, import_jsx_runtime26.jsx)(import_ui9.PanelGroup, { title: group, children: /* @__PURE__ */ (0, import_jsx_runtime26.jsx)("ul", { children: groupAlbums.map((album) => /* @__PURE__ */ (0, import_jsx_runtime26.jsxs)("li", { className: "grid grid-cols-[2.5rem_minmax(0,1fr)] gap-2 py-0.5", children: [
1784
- /* @__PURE__ */ (0, import_jsx_runtime26.jsx)("span", { className: "tabular-nums text-[var(--color-text-muted)]", children: album.year ?? "\u2014" }),
1785
- /* @__PURE__ */ (0, import_jsx_runtime26.jsx)("span", { className: "truncate text-[var(--color-text)]", title: album.name, children: album.name })
1858
+ return /* @__PURE__ */ (0, import_jsx_runtime27.jsx)(import_ui9.PanelGroup, { title: group, children: /* @__PURE__ */ (0, import_jsx_runtime27.jsx)("ul", { children: groupAlbums.map((album) => /* @__PURE__ */ (0, import_jsx_runtime27.jsxs)("li", { className: "grid grid-cols-[2.5rem_minmax(0,1fr)] gap-2 py-0.5", children: [
1859
+ /* @__PURE__ */ (0, import_jsx_runtime27.jsx)("span", { className: "tabular-nums text-[var(--color-text-muted)]", children: album.year ?? "\u2014" }),
1860
+ /* @__PURE__ */ (0, import_jsx_runtime27.jsx)("span", { className: "truncate text-[var(--color-text)]", title: album.name, children: album.name })
1786
1861
  ] }, album.uuid)) }) }, group);
1787
1862
  }) });
1788
1863
  }
1789
1864
 
1790
1865
  // src/genre-tree/artist/ArtistSongs.tsx
1791
- var import_react12 = require("react");
1866
+ var import_react14 = require("react");
1792
1867
  var import_react_query2 = require("@tanstack/react-query");
1793
1868
  var import_lucide_react10 = require("lucide-react");
1794
1869
  var import_ui10 = require("@behindthemusictree/ui");
1795
- var import_player9 = require("@behindthemusictree/app-kit/player");
1870
+ var import_player10 = require("@behindthemusictree/app-kit/player");
1796
1871
  var import_genre_tree14 = require("@behindthemusictree/app-kit/genre-tree");
1797
1872
  var import_transport7 = require("@behindthemusictree/app-kit/transport");
1798
1873
 
1799
1874
  // src/genre-tree/track-list-sidebar/TrackItem.tsx
1800
- var import_player8 = require("@behindthemusictree/app-kit/player");
1875
+ var import_player9 = require("@behindthemusictree/app-kit/player");
1801
1876
  var import_genre_tree11 = require("@behindthemusictree/app-kit/genre-tree");
1802
1877
 
1803
1878
  // src/genre-tree/TrackPositionPlayPause.tsx
1804
1879
  var import_fa3 = require("react-icons/fa");
1805
- var import_player6 = require("@behindthemusictree/app-kit/player");
1806
1880
  var import_player7 = require("@behindthemusictree/app-kit/player");
1807
- var import_jsx_runtime27 = require("react/jsx-runtime");
1881
+ var import_player8 = require("@behindthemusictree/app-kit/player");
1882
+ var import_jsx_runtime28 = require("react/jsx-runtime");
1808
1883
  function TrackPositionPlayPause({
1809
1884
  position,
1810
1885
  uuid,
1811
1886
  handlePlayPauseClick,
1812
1887
  isSelected = false
1813
1888
  }) {
1814
- const { playerTrackObject, playbackConfirmedTrackId, playState, isLoading: isPlayerLoading } = (0, import_player6.usePlayer)();
1889
+ const { playerTrackObject, playbackConfirmedTrackId, playState, isLoading: isPlayerLoading } = (0, import_player7.usePlayer)();
1815
1890
  const isThisTrack = playerTrackObject?.track.id === uuid;
1816
1891
  const isConfirmedPlayable = playbackConfirmedTrackId === uuid;
1817
- const isAwaitingPlayback = (isSelected || isThisTrack) && !isConfirmedPlayable && !(isThisTrack && playerTrackObject?.loadError) && (!isThisTrack || isPlayerLoading || playState === import_player7.PlayStates.LOADING || playState === import_player7.PlayStates.PLAYING);
1818
- const isPlaying = isThisTrack && isConfirmedPlayable && playState === import_player7.PlayStates.PLAYING;
1819
- const isPaused = isThisTrack && isConfirmedPlayable && playState === import_player7.PlayStates.PAUSED;
1892
+ const isAwaitingPlayback = (isSelected || isThisTrack) && !isConfirmedPlayable && !(isThisTrack && playerTrackObject?.loadError) && (!isThisTrack || isPlayerLoading || playState === import_player8.PlayStates.LOADING || playState === import_player8.PlayStates.PLAYING);
1893
+ const isPlaying = isThisTrack && isConfirmedPlayable && playState === import_player8.PlayStates.PLAYING;
1894
+ const isPaused = isThisTrack && isConfirmedPlayable && playState === import_player8.PlayStates.PAUSED;
1820
1895
  const isCurrentTrackActive = isPlaying || isPaused;
1821
- return /* @__PURE__ */ (0, import_jsx_runtime27.jsxs)(
1896
+ return /* @__PURE__ */ (0, import_jsx_runtime28.jsxs)(
1822
1897
  "div",
1823
1898
  {
1824
1899
  className: "track-position-play-pause relative flex items-center justify-center gap-2 text-lg w-16 cursor-pointer",
1825
1900
  style: { minWidth: "4rem", maxWidth: "4rem" },
1826
1901
  onClick: handlePlayPauseClick,
1827
1902
  children: [
1828
- /* @__PURE__ */ (0, import_jsx_runtime27.jsx)("div", { className: "track-position-play-pause__status group-hover:hidden flex items-center justify-center", children: isAwaitingPlayback ? /* @__PURE__ */ (0, import_jsx_runtime27.jsx)("span", { className: "track-position-play-pause__spinner", role: "status", "aria-label": "Loading track" }) : isCurrentTrackActive ? /* @__PURE__ */ (0, import_jsx_runtime27.jsxs)(
1903
+ /* @__PURE__ */ (0, import_jsx_runtime28.jsx)("div", { className: "track-position-play-pause__status group-hover:hidden flex items-center justify-center", children: isAwaitingPlayback ? /* @__PURE__ */ (0, import_jsx_runtime28.jsx)("span", { className: "track-position-play-pause__spinner", role: "status", "aria-label": "Loading track" }) : isCurrentTrackActive ? /* @__PURE__ */ (0, import_jsx_runtime28.jsxs)(
1829
1904
  "span",
1830
1905
  {
1831
1906
  className: "track-position-play-pause__bars",
@@ -1833,18 +1908,18 @@ function TrackPositionPlayPause({
1833
1908
  role: "img",
1834
1909
  "aria-label": isPlaying ? "Playing" : "Paused",
1835
1910
  children: [
1836
- /* @__PURE__ */ (0, import_jsx_runtime27.jsx)("span", { className: "track-position-play-pause__bar" }),
1837
- /* @__PURE__ */ (0, import_jsx_runtime27.jsx)("span", { className: "track-position-play-pause__bar" }),
1838
- /* @__PURE__ */ (0, import_jsx_runtime27.jsx)("span", { className: "track-position-play-pause__bar" })
1911
+ /* @__PURE__ */ (0, import_jsx_runtime28.jsx)("span", { className: "track-position-play-pause__bar" }),
1912
+ /* @__PURE__ */ (0, import_jsx_runtime28.jsx)("span", { className: "track-position-play-pause__bar" }),
1913
+ /* @__PURE__ */ (0, import_jsx_runtime28.jsx)("span", { className: "track-position-play-pause__bar" })
1839
1914
  ]
1840
1915
  }
1841
1916
  ) : position }),
1842
- /* @__PURE__ */ (0, import_jsx_runtime27.jsx)(
1917
+ /* @__PURE__ */ (0, import_jsx_runtime28.jsx)(
1843
1918
  "div",
1844
1919
  {
1845
1920
  className: `hidden group-hover:flex items-center justify-center ${isCurrentTrackActive ? "" : "absolute inset-0"}`,
1846
1921
  "aria-hidden": "true",
1847
- children: isAwaitingPlayback ? /* @__PURE__ */ (0, import_jsx_runtime27.jsx)("span", { className: "track-position-play-pause__spinner" }) : isPlaying ? /* @__PURE__ */ (0, import_jsx_runtime27.jsx)(import_fa3.FaPause, {}) : /* @__PURE__ */ (0, import_jsx_runtime27.jsx)(import_fa3.FaPlay, {})
1922
+ children: isAwaitingPlayback ? /* @__PURE__ */ (0, import_jsx_runtime28.jsx)("span", { className: "track-position-play-pause__spinner" }) : isPlaying ? /* @__PURE__ */ (0, import_jsx_runtime28.jsx)(import_fa3.FaPause, {}) : /* @__PURE__ */ (0, import_jsx_runtime28.jsx)(import_fa3.FaPlay, {})
1848
1923
  }
1849
1924
  )
1850
1925
  ]
@@ -1855,7 +1930,7 @@ function TrackPositionPlayPause({
1855
1930
  // src/genre-tree/track-list-sidebar/TrackItem.tsx
1856
1931
  var import_genre_tree12 = require("@behindthemusictree/app-kit/genre-tree");
1857
1932
  var import_genre_tree13 = require("@behindthemusictree/app-kit/genre-tree");
1858
- var import_jsx_runtime28 = require("react/jsx-runtime");
1933
+ var import_jsx_runtime29 = require("react/jsx-runtime");
1859
1934
  function TrackItem({
1860
1935
  track,
1861
1936
  position,
@@ -1863,7 +1938,7 @@ function TrackItem({
1863
1938
  renderActions,
1864
1939
  renderArtistLink
1865
1940
  }) {
1866
- const { handlePlayPauseAction, playerTrackObject, loadTrackForPlayer } = (0, import_player8.usePlayer)();
1941
+ const { handlePlayPauseAction, playerTrackObject, loadTrackForPlayer } = (0, import_player9.usePlayer)();
1867
1942
  const { trackList, selectedTrack, toTrackAtPosition, runtimeUnplayableTrackIds } = (0, import_genre_tree11.useTrackList)();
1868
1943
  const scope = trackList?.origin?.scope ?? null;
1869
1944
  const isCurrentTrack = playerTrackObject?.track.id === track.uuid;
@@ -1878,12 +1953,12 @@ function TrackItem({
1878
1953
  loadTrackForPlayer(track.uuid);
1879
1954
  }
1880
1955
  };
1881
- return /* @__PURE__ */ (0, import_jsx_runtime28.jsxs)(
1956
+ return /* @__PURE__ */ (0, import_jsx_runtime29.jsxs)(
1882
1957
  "div",
1883
1958
  {
1884
1959
  className: `track-item flex h-14 text-[var(--color-text)] ${isUnplayable ? "opacity-50" : "hover:bg-[var(--color-surface-muted)] group"}`,
1885
1960
  children: [
1886
- isUnplayable ? /* @__PURE__ */ (0, import_jsx_runtime28.jsx)(
1961
+ isUnplayable ? /* @__PURE__ */ (0, import_jsx_runtime29.jsx)(
1887
1962
  "div",
1888
1963
  {
1889
1964
  className: "flex items-center justify-center text-lg w-16",
@@ -1891,7 +1966,7 @@ function TrackItem({
1891
1966
  "aria-disabled": "true",
1892
1967
  children: position
1893
1968
  }
1894
- ) : /* @__PURE__ */ (0, import_jsx_runtime28.jsx)(
1969
+ ) : /* @__PURE__ */ (0, import_jsx_runtime29.jsx)(
1895
1970
  TrackPositionPlayPause,
1896
1971
  {
1897
1972
  position,
@@ -1900,12 +1975,12 @@ function TrackItem({
1900
1975
  isSelected: selectedTrack?.uuid === track.uuid
1901
1976
  }
1902
1977
  ),
1903
- /* @__PURE__ */ (0, import_jsx_runtime28.jsxs)("div", { className: "title-artist-container flex flex-col items-start justify-center min-w-0 flex-1", children: [
1904
- /* @__PURE__ */ (0, import_jsx_runtime28.jsx)("div", { className: "flex max-w-full items-center gap-2", children: /* @__PURE__ */ (0, import_jsx_runtime28.jsx)("div", { className: "title text-lg font-bold text-[var(--color-text)] max-w-full truncate", children: (0, import_genre_tree12.isSong)(track) ? /* @__PURE__ */ (0, import_jsx_runtime28.jsx)(MusicBrainzRecordingLink, { song: track, className: "hover:underline focus-visible:underline", children: track.title }) : track.title }) }),
1905
- track.artists && track.artists.length > 0 ? /* @__PURE__ */ (0, import_jsx_runtime28.jsx)("div", { className: "artist text-base max-w-full truncate text-[var(--color-text-muted)]", children: /* @__PURE__ */ (0, import_jsx_runtime28.jsx)(import_genre_tree13.ArtistNames, { artists: track.artists, renderArtistLink }) }) : ""
1978
+ /* @__PURE__ */ (0, import_jsx_runtime29.jsxs)("div", { className: "title-artist-container flex flex-col items-start justify-center min-w-0 flex-1", children: [
1979
+ /* @__PURE__ */ (0, import_jsx_runtime29.jsx)("div", { className: "flex max-w-full items-center gap-2", children: /* @__PURE__ */ (0, import_jsx_runtime29.jsx)("div", { className: "title text-lg font-bold text-[var(--color-text)] max-w-full truncate", children: (0, import_genre_tree12.isSong)(track) ? /* @__PURE__ */ (0, import_jsx_runtime29.jsx)(MusicBrainzRecordingLink, { song: track, className: "hover:underline focus-visible:underline", children: track.title }) : track.title }) }),
1980
+ track.artists && track.artists.length > 0 ? /* @__PURE__ */ (0, import_jsx_runtime29.jsx)("div", { className: "artist text-base max-w-full truncate text-[var(--color-text-muted)]", children: /* @__PURE__ */ (0, import_jsx_runtime29.jsx)(import_genre_tree13.ArtistNames, { artists: track.artists, renderArtistLink }) }) : ""
1906
1981
  ] }),
1907
- /* @__PURE__ */ (0, import_jsx_runtime28.jsx)("div", { className: "genre-name-container flex items-center justify-start w-[21.6%] min-w-0 shrink-0", children: track.genre ? /* @__PURE__ */ (0, import_jsx_runtime28.jsx)("div", { className: "genre-name max-w-full whitespace-normal break-words p-1 text-xs font-bold", children: track.genre.name }) : "" }),
1908
- renderDuration && /* @__PURE__ */ (0, import_jsx_runtime28.jsx)(
1982
+ /* @__PURE__ */ (0, import_jsx_runtime29.jsx)("div", { className: "genre-name-container flex items-center justify-start w-[21.6%] min-w-0 shrink-0", children: track.genre ? /* @__PURE__ */ (0, import_jsx_runtime29.jsx)("div", { className: "genre-name max-w-full whitespace-normal break-words p-1 text-xs font-bold", children: track.genre.name }) : "" }),
1983
+ renderDuration && /* @__PURE__ */ (0, import_jsx_runtime29.jsx)(
1909
1984
  "div",
1910
1985
  {
1911
1986
  className: "duration flex text-base w-16 items-center justify-center",
@@ -1913,25 +1988,25 @@ function TrackItem({
1913
1988
  children: renderDuration(track)
1914
1989
  }
1915
1990
  ),
1916
- renderActions && /* @__PURE__ */ (0, import_jsx_runtime28.jsx)("div", { className: "edit flex text-base w-6 items-center justify-center mr-2 cursor-pointer", children: renderActions(track) })
1991
+ renderActions && /* @__PURE__ */ (0, import_jsx_runtime29.jsx)("div", { className: "edit flex text-base w-6 items-center justify-center mr-2 cursor-pointer", children: renderActions(track) })
1917
1992
  ]
1918
1993
  }
1919
1994
  );
1920
1995
  }
1921
1996
 
1922
1997
  // src/genre-tree/artist/ArtistSongs.tsx
1923
- var import_jsx_runtime29 = require("react/jsx-runtime");
1998
+ var import_jsx_runtime30 = require("react/jsx-runtime");
1924
1999
  var SongPageSchema2 = (0, import_transport7.PaginatedResponseSchema)(import_genre_tree14.SongDetailedSchema);
1925
2000
  var SORTS = [
1926
2001
  { ordering: "title", label: "Title" },
1927
2002
  { ordering: "-albumYear", label: "Year" }
1928
2003
  ];
1929
2004
  function ArtistSongs({ artist, getBackendBaseUrl, renderArtistLink }) {
1930
- const [page, setPage] = (0, import_react12.useState)(1);
1931
- const [ordering, setOrdering] = (0, import_react12.useState)("title");
2005
+ const [page, setPage] = (0, import_react14.useState)(1);
2006
+ const [ordering, setOrdering] = (0, import_react14.useState)("title");
1932
2007
  const { fetch } = (0, import_transport7.useFetchWrapper)(getBackendBaseUrl);
1933
2008
  const { playNewTrackListFromArtist } = (0, import_genre_tree14.useTrackList)();
1934
- const { playerTrackObject } = (0, import_player9.usePlayer)();
2009
+ const { playerTrackObject } = (0, import_player10.usePlayer)();
1935
2010
  const { data, isPending, isError } = (0, import_transport7.useQueryWithParse)({
1936
2011
  queryKey: ["referenceSongs", "artist", artist.uuid, ordering, page],
1937
2012
  queryFn: () => fetch(import_genre_tree14.libraryEndpoints.reference.songs.list(), true, false, void 0, {
@@ -1953,21 +2028,21 @@ function ArtistSongs({ artist, getBackendBaseUrl, renderArtistLink }) {
1953
2028
  event.stopPropagation();
1954
2029
  void play(song.uuid);
1955
2030
  };
1956
- return /* @__PURE__ */ (0, import_jsx_runtime29.jsx)(import_ui10.PanelSection, { title: "Songs", children: isPending ? /* @__PURE__ */ (0, import_jsx_runtime29.jsx)(import_ui10.Skeleton, { className: "h-14", "aria-busy": "true" }) : isError ? /* @__PURE__ */ (0, import_jsx_runtime29.jsx)("p", { className: "text-red-600", children: "Could not load songs." }) : data.results.length === 0 ? /* @__PURE__ */ (0, import_jsx_runtime29.jsx)("p", { className: "text-[var(--color-text-muted)]", children: "No songs yet." }) : /* @__PURE__ */ (0, import_jsx_runtime29.jsxs)(import_jsx_runtime29.Fragment, { children: [
1957
- /* @__PURE__ */ (0, import_jsx_runtime29.jsxs)("div", { className: "flex flex-wrap items-center justify-between gap-2 mb-2", children: [
1958
- /* @__PURE__ */ (0, import_jsx_runtime29.jsxs)(
2031
+ return /* @__PURE__ */ (0, import_jsx_runtime30.jsx)(import_ui10.PanelSection, { title: "Songs", children: isPending ? /* @__PURE__ */ (0, import_jsx_runtime30.jsx)(import_ui10.Skeleton, { className: "h-14", "aria-busy": "true" }) : isError ? /* @__PURE__ */ (0, import_jsx_runtime30.jsx)("p", { className: "text-red-600", children: "Could not load songs." }) : data.results.length === 0 ? /* @__PURE__ */ (0, import_jsx_runtime30.jsx)("p", { className: "text-[var(--color-text-muted)]", children: "No songs yet." }) : /* @__PURE__ */ (0, import_jsx_runtime30.jsxs)(import_jsx_runtime30.Fragment, { children: [
2032
+ /* @__PURE__ */ (0, import_jsx_runtime30.jsxs)("div", { className: "flex flex-wrap items-center justify-between gap-2 mb-2", children: [
2033
+ /* @__PURE__ */ (0, import_jsx_runtime30.jsxs)(
1959
2034
  "button",
1960
2035
  {
1961
2036
  type: "button",
1962
2037
  onClick: () => void play(),
1963
2038
  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",
1964
2039
  children: [
1965
- /* @__PURE__ */ (0, import_jsx_runtime29.jsx)(import_lucide_react10.Play, { className: "h-3.5 w-3.5", "aria-hidden": "true" }),
2040
+ /* @__PURE__ */ (0, import_jsx_runtime30.jsx)(import_lucide_react10.Play, { className: "h-3.5 w-3.5", "aria-hidden": "true" }),
1966
2041
  "Play all"
1967
2042
  ]
1968
2043
  }
1969
2044
  ),
1970
- /* @__PURE__ */ (0, import_jsx_runtime29.jsx)("div", { role: "group", "aria-label": "Sort songs by", className: "flex gap-1 text-xs", children: SORTS.map((sort) => /* @__PURE__ */ (0, import_jsx_runtime29.jsx)(
2045
+ /* @__PURE__ */ (0, import_jsx_runtime30.jsx)("div", { role: "group", "aria-label": "Sort songs by", className: "flex gap-1 text-xs", children: SORTS.map((sort) => /* @__PURE__ */ (0, import_jsx_runtime30.jsx)(
1971
2046
  "button",
1972
2047
  {
1973
2048
  type: "button",
@@ -1982,7 +2057,7 @@ function ArtistSongs({ artist, getBackendBaseUrl, renderArtistLink }) {
1982
2057
  sort.ordering
1983
2058
  )) })
1984
2059
  ] }),
1985
- /* @__PURE__ */ (0, import_jsx_runtime29.jsx)("ul", { className: "overflow-hidden mb-2 bg-zinc-950 rounded-lg", children: data.results.map((song, index) => /* @__PURE__ */ (0, import_jsx_runtime29.jsx)("li", { onClickCapture: (event) => handleRowClickCapture(event, song), children: /* @__PURE__ */ (0, import_jsx_runtime29.jsx)(
2060
+ /* @__PURE__ */ (0, import_jsx_runtime30.jsx)("ul", { className: "overflow-hidden mb-2 bg-zinc-950 rounded-lg", children: data.results.map((song, index) => /* @__PURE__ */ (0, import_jsx_runtime30.jsx)("li", { onClickCapture: (event) => handleRowClickCapture(event, song), children: /* @__PURE__ */ (0, import_jsx_runtime30.jsx)(
1986
2061
  TrackItem,
1987
2062
  {
1988
2063
  track: song,
@@ -1990,12 +2065,12 @@ function ArtistSongs({ artist, getBackendBaseUrl, renderArtistLink }) {
1990
2065
  renderArtistLink
1991
2066
  }
1992
2067
  ) }, song.uuid)) }),
1993
- /* @__PURE__ */ (0, import_jsx_runtime29.jsx)(import_ui10.Pagination, { currentPage: page, totalPages: data.totalPages, onPageChange: setPage })
2068
+ /* @__PURE__ */ (0, import_jsx_runtime30.jsx)(import_ui10.Pagination, { currentPage: page, totalPages: data.totalPages, onPageChange: setPage })
1994
2069
  ] }) });
1995
2070
  }
1996
2071
 
1997
2072
  // src/genre-tree/artist/useArtistPanel.tsx
1998
- var import_jsx_runtime30 = require("react/jsx-runtime");
2073
+ var import_jsx_runtime31 = require("react/jsx-runtime");
1999
2074
  function useArtistPanel({
2000
2075
  uuid,
2001
2076
  scope,
@@ -2015,26 +2090,26 @@ function useArtistPanel({
2015
2090
  return {
2016
2091
  ...panel,
2017
2092
  title: "Artist",
2018
- content: /* @__PURE__ */ (0, import_jsx_runtime30.jsxs)("div", { "aria-busy": "true", className: "flex flex-col gap-2.5 px-6 py-2.5", children: [
2019
- /* @__PURE__ */ (0, import_jsx_runtime30.jsx)(import_ui11.Skeleton, { className: "h-3 w-16" }),
2020
- /* @__PURE__ */ (0, import_jsx_runtime30.jsx)(import_ui11.Skeleton, { className: "h-4 w-32" }),
2021
- /* @__PURE__ */ (0, import_jsx_runtime30.jsx)(import_ui11.Skeleton, { className: "h-3 w-16" }),
2022
- /* @__PURE__ */ (0, import_jsx_runtime30.jsx)(import_ui11.Skeleton, { className: "h-4 w-24" }),
2023
- /* @__PURE__ */ (0, import_jsx_runtime30.jsx)(import_ui11.Skeleton, { className: "h-16 mt-2.5" }),
2024
- /* @__PURE__ */ (0, import_jsx_runtime30.jsx)(import_ui11.Skeleton, { className: "h-16" })
2093
+ content: /* @__PURE__ */ (0, import_jsx_runtime31.jsxs)("div", { "aria-busy": "true", className: "flex flex-col gap-2.5 px-6 py-2.5", children: [
2094
+ /* @__PURE__ */ (0, import_jsx_runtime31.jsx)(import_ui11.Skeleton, { className: "h-3 w-16" }),
2095
+ /* @__PURE__ */ (0, import_jsx_runtime31.jsx)(import_ui11.Skeleton, { className: "h-4 w-32" }),
2096
+ /* @__PURE__ */ (0, import_jsx_runtime31.jsx)(import_ui11.Skeleton, { className: "h-3 w-16" }),
2097
+ /* @__PURE__ */ (0, import_jsx_runtime31.jsx)(import_ui11.Skeleton, { className: "h-4 w-24" }),
2098
+ /* @__PURE__ */ (0, import_jsx_runtime31.jsx)(import_ui11.Skeleton, { className: "h-16 mt-2.5" }),
2099
+ /* @__PURE__ */ (0, import_jsx_runtime31.jsx)(import_ui11.Skeleton, { className: "h-16" })
2025
2100
  ] })
2026
2101
  };
2027
2102
  case "not-found":
2028
2103
  return {
2029
2104
  ...panel,
2030
2105
  title: "Artist not found",
2031
- content: /* @__PURE__ */ (0, import_jsx_runtime30.jsx)("p", { className: "px-6 py-2.5 text-[var(--color-text-muted)]", children: "This artist does not exist." })
2106
+ content: /* @__PURE__ */ (0, import_jsx_runtime31.jsx)("p", { className: "px-6 py-2.5 text-[var(--color-text-muted)]", children: "This artist does not exist." })
2032
2107
  };
2033
2108
  case "error":
2034
2109
  return {
2035
2110
  ...panel,
2036
2111
  title: "Artist",
2037
- content: /* @__PURE__ */ (0, import_jsx_runtime30.jsx)("p", { className: "px-6 py-2.5 text-red-600", children: "Could not load this artist." })
2112
+ content: /* @__PURE__ */ (0, import_jsx_runtime31.jsx)("p", { className: "px-6 py-2.5 text-red-600", children: "Could not load this artist." })
2038
2113
  };
2039
2114
  case "success": {
2040
2115
  const { artist } = result;
@@ -2043,13 +2118,13 @@ function useArtistPanel({
2043
2118
  return {
2044
2119
  ...panel,
2045
2120
  title: artist.name,
2046
- content: /* @__PURE__ */ (0, import_jsx_runtime30.jsxs)(import_jsx_runtime30.Fragment, { children: [
2047
- shareUrl && /* @__PURE__ */ (0, import_jsx_runtime30.jsx)("div", { className: "genre-action-band border-b border-[var(--color-border)]", children: /* @__PURE__ */ (0, import_jsx_runtime30.jsx)("div", { className: "flex flex-wrap gap-1 px-3.5 py-3", children: shareTarget && /* @__PURE__ */ (0, import_jsx_runtime30.jsx)(ShareActionButton, { target: shareTarget, url: shareUrl }) }) }),
2048
- artist.image && /* @__PURE__ */ (0, import_jsx_runtime30.jsx)(ArtistPhoto, { image: artist.image, name: artist.name }),
2049
- /* @__PURE__ */ (0, import_jsx_runtime30.jsx)(ArtistHeader, { artist }),
2050
- /* @__PURE__ */ (0, import_jsx_runtime30.jsx)(ArtistMembers, { title: "Members", memberships: artist.members, renderArtistLink }),
2051
- /* @__PURE__ */ (0, import_jsx_runtime30.jsx)(ArtistMembers, { title: "Member of", memberships: artist.memberOf, renderArtistLink }),
2052
- artist.tracksCount > 0 && /* @__PURE__ */ (0, import_jsx_runtime30.jsx)(
2121
+ content: /* @__PURE__ */ (0, import_jsx_runtime31.jsxs)(import_jsx_runtime31.Fragment, { children: [
2122
+ shareUrl && /* @__PURE__ */ (0, import_jsx_runtime31.jsx)("div", { className: "genre-action-band border-b border-[var(--color-border)]", children: /* @__PURE__ */ (0, import_jsx_runtime31.jsx)("div", { className: "flex flex-wrap gap-1 px-3.5 py-3", children: shareTarget && /* @__PURE__ */ (0, import_jsx_runtime31.jsx)(ShareActionButton, { target: shareTarget, url: shareUrl }) }) }),
2123
+ artist.image && /* @__PURE__ */ (0, import_jsx_runtime31.jsx)(ArtistPhoto, { image: artist.image, name: artist.name }),
2124
+ /* @__PURE__ */ (0, import_jsx_runtime31.jsx)(ArtistHeader, { artist }),
2125
+ /* @__PURE__ */ (0, import_jsx_runtime31.jsx)(ArtistMembers, { title: "Members", memberships: artist.members, renderArtistLink }),
2126
+ /* @__PURE__ */ (0, import_jsx_runtime31.jsx)(ArtistMembers, { title: "Member of", memberships: artist.memberOf, renderArtistLink }),
2127
+ artist.tracksCount > 0 && /* @__PURE__ */ (0, import_jsx_runtime31.jsx)(
2053
2128
  ArtistSongs,
2054
2129
  {
2055
2130
  artist: { uuid: artist.uuid, name: artist.name },
@@ -2058,7 +2133,7 @@ function useArtistPanel({
2058
2133
  }
2059
2134
  )
2060
2135
  ] }),
2061
- extraContent: artist.albums.length > 0 ? /* @__PURE__ */ (0, import_jsx_runtime30.jsx)(ArtistDiscography, { albums: artist.albums }) : void 0
2136
+ extraContent: artist.albums.length > 0 ? /* @__PURE__ */ (0, import_jsx_runtime31.jsx)(ArtistDiscography, { albums: artist.albums }) : void 0
2062
2137
  };
2063
2138
  }
2064
2139
  }
@@ -2067,27 +2142,27 @@ function useArtistPanel({
2067
2142
  // src/genre-tree/SizeMetricToggle.tsx
2068
2143
  var import_md = require("react-icons/md");
2069
2144
  var import_ui12 = require("@behindthemusictree/ui");
2070
- var import_jsx_runtime31 = require("react/jsx-runtime");
2145
+ var import_jsx_runtime32 = require("react/jsx-runtime");
2071
2146
  function SizeMetricToggle({
2072
2147
  value,
2073
2148
  onChange
2074
2149
  }) {
2075
- return /* @__PURE__ */ (0, import_jsx_runtime31.jsx)(
2150
+ return /* @__PURE__ */ (0, import_jsx_runtime32.jsx)(
2076
2151
  import_ui12.SegmentedControl,
2077
2152
  {
2078
2153
  "aria-label": "Size nodes by",
2079
2154
  value,
2080
2155
  onChange,
2081
2156
  options: [
2082
- { value: "songs", label: /* @__PURE__ */ (0, import_jsx_runtime31.jsx)(import_md.MdMusicNote, { size: 18, "aria-hidden": true }), "aria-label": "Size by songs", title: "Node size by song count" },
2083
- { value: "views", label: /* @__PURE__ */ (0, import_jsx_runtime31.jsx)(import_md.MdVisibility, { size: 18, "aria-hidden": true }), "aria-label": "Size by views", title: "Node size by song views" }
2157
+ { value: "songs", label: /* @__PURE__ */ (0, import_jsx_runtime32.jsx)(import_md.MdMusicNote, { size: 18, "aria-hidden": true }), "aria-label": "Size by songs", title: "Node size by song count" },
2158
+ { value: "views", label: /* @__PURE__ */ (0, import_jsx_runtime32.jsx)(import_md.MdVisibility, { size: 18, "aria-hidden": true }), "aria-label": "Size by views", title: "Node size by song views" }
2084
2159
  ]
2085
2160
  }
2086
2161
  );
2087
2162
  }
2088
2163
 
2089
2164
  // src/genre-tree/GenreTreeView.tsx
2090
- var import_jsx_runtime32 = require("react/jsx-runtime");
2165
+ var import_jsx_runtime33 = require("react/jsx-runtime");
2091
2166
  var PREVIOUS_BUTTON_CSS = `
2092
2167
  .genre-tree-view .search-overlay .gtv-info-panel-previous { display: none; }
2093
2168
  .genre-tree-view:has(.search-overlay .gtv-info-panel-previous) .genre-search input { padding-left: 2.75rem; }
@@ -2097,27 +2172,6 @@ var PREVIOUS_BUTTON_CSS = `
2097
2172
  .genre-search-previous { animation: genre-tree-view-previous-in 200ms ease-out; }
2098
2173
  }
2099
2174
  `;
2100
- function GenreNodeActionBand({ genrePlaylist, scope, playback, ...bandProps }) {
2101
- const { isPlaying, setIsPlaying } = (0, import_player10.usePlayer)();
2102
- const isCurrentTrackList = playback.playingNodeId === genrePlaylist.uuid;
2103
- const handlePlayPause = () => {
2104
- if (isCurrentTrackList) {
2105
- setIsPlaying(!isPlaying);
2106
- return;
2107
- }
2108
- playback.play(genrePlaylist).catch((error) => {
2109
- console.error("Failed to load genre playlist tracks:", error);
2110
- });
2111
- };
2112
- return /* @__PURE__ */ (0, import_jsx_runtime32.jsx)(
2113
- GenreActionBand,
2114
- {
2115
- ...bandProps,
2116
- onPlayPause: genrePlaylist.tracksCount > 0 ? handlePlayPause : void 0,
2117
- isPlaying: isCurrentTrackList && isPlaying
2118
- }
2119
- );
2120
- }
2121
2175
  var NO_POP_ROOT_TITLE = "This genre tree has no 'Mainstream Pop' root yet";
2122
2176
  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]";
2123
2177
  function GenreTreeView({
@@ -2131,6 +2185,7 @@ function GenreTreeView({
2131
2185
  selectedNodeId: controlledSelectedNodeId,
2132
2186
  onSelectedNodeIdChange,
2133
2187
  defaultViewport,
2188
+ viewportRestore,
2134
2189
  onViewportChange,
2135
2190
  readOnly = false,
2136
2191
  className: className2,
@@ -2139,7 +2194,6 @@ function GenreTreeView({
2139
2194
  allowWheelRotation: controlledAllowWheelRotation,
2140
2195
  criteriaOverviewSchema,
2141
2196
  renderGenreDetailExtras,
2142
- controlsOrientation = "horizontal",
2143
2197
  floatingControlsLeading,
2144
2198
  sizeMetric: controlledSizeMetric,
2145
2199
  onSizeMetricChange,
@@ -2166,14 +2220,14 @@ function GenreTreeView({
2166
2220
  placement = "default",
2167
2221
  ...focus
2168
2222
  }) {
2169
- const getShareUrlRef = (0, import_react13.useRef)(getShareUrl);
2223
+ const getShareUrlRef = (0, import_react15.useRef)(getShareUrl);
2170
2224
  getShareUrlRef.current = getShareUrl;
2171
- const [internalArtistUuid, setInternalArtistUuid] = (0, import_react13.useState)(null);
2225
+ const [internalArtistUuid, setInternalArtistUuid] = (0, import_react15.useState)(null);
2172
2226
  const isArtistControlled = controlledArtistUuid !== void 0;
2173
2227
  const artistUuid = isArtistControlled ? controlledArtistUuid : internalArtistUuid;
2174
- const [internalSizeMetric, setInternalSizeMetric] = (0, import_react13.useState)("songs");
2228
+ const [internalSizeMetric, setInternalSizeMetric] = (0, import_react15.useState)("songs");
2175
2229
  const sizeMetric = controlledSizeMetric ?? internalSizeMetric;
2176
- const deferredSizeMetric = (0, import_react13.useDeferredValue)(sizeMetric);
2230
+ const deferredSizeMetric = (0, import_react15.useDeferredValue)(sizeMetric);
2177
2231
  const changeSizeMetric = (metric) => {
2178
2232
  if (controlledSizeMetric === void 0) setInternalSizeMetric(metric);
2179
2233
  onSizeMetricChange?.(metric);
@@ -2209,6 +2263,16 @@ function GenreTreeView({
2209
2263
  criteriaOverviewSchema,
2210
2264
  artistUuid
2211
2265
  });
2266
+ const restoreRevision = viewportRestore?.revision;
2267
+ const [stackedRestoreOwner, setStackedRestoreOwner] = (0, import_react15.useState)({ revision: restoreRevision, rootUuid: selectedGenrePlaylist?.root.uuid });
2268
+ let restoreRootUuid = stackedRestoreOwner.rootUuid;
2269
+ if (stackedRestoreOwner.revision !== restoreRevision) {
2270
+ restoreRootUuid = selectedGenrePlaylist?.root.uuid;
2271
+ setStackedRestoreOwner({ revision: restoreRevision, rootUuid: restoreRootUuid });
2272
+ } else if (restoreRootUuid === void 0 && selectedGenrePlaylist) {
2273
+ restoreRootUuid = selectedGenrePlaylist.root.uuid;
2274
+ setStackedRestoreOwner({ revision: restoreRevision, rootUuid: restoreRootUuid });
2275
+ }
2212
2276
  const isGenreUpdating = (0, import_react_query3.useIsMutating)({ mutationKey: import_genre_tree17.genreUpdateMutationKey }) > 0;
2213
2277
  const { mutate: updateGenre } = (0, import_genre_tree17.useUpdateGenre)(scope, getBackendBaseUrl);
2214
2278
  const busy = deferredSizeMetric !== sizeMetric || isTreeFetching || isGenreUpdating;
@@ -2216,7 +2280,7 @@ function GenreTreeView({
2216
2280
  const canSizeByViews = !isArtistFiltered && hasViewsCount(visibleGenrePlaylists);
2217
2281
  const artistResult = (0, import_genre_tree17.useArtist)(artistUuid, artistSearchBaseUrl ?? getBackendBaseUrl);
2218
2282
  const filterArtistName = artistResult.status === "success" ? artistResult.artist.name : null;
2219
- const filterArtist = (0, import_react13.useMemo)(
2283
+ const filterArtist = (0, import_react15.useMemo)(
2220
2284
  () => artistUuid !== null && filterArtistName !== null ? { uuid: artistUuid, name: filterArtistName } : null,
2221
2285
  [artistUuid, filterArtistName]
2222
2286
  );
@@ -2226,14 +2290,14 @@ function GenreTreeView({
2226
2290
  if (selectedNodeId !== null) clearSelection();
2227
2291
  changeArtist(uuid);
2228
2292
  };
2229
- const [expandedArtistUuid, setExpandedArtistUuid] = (0, import_react13.useState)(null);
2293
+ const [expandedArtistUuid, setExpandedArtistUuid] = (0, import_react15.useState)(null);
2230
2294
  const artistBaseUrl = artistSearchBaseUrl ?? getBackendBaseUrl;
2231
2295
  const artistPanel = useArtistPanel({
2232
2296
  uuid: artistUuid,
2233
2297
  scope,
2234
2298
  getBackendBaseUrl: artistBaseUrl,
2235
2299
  getShareUrl: getShareUrlRef.current,
2236
- renderArtistLink: renderArtistLink ?? ((artist) => artist.isSpecialPurpose ? artist.name : /* @__PURE__ */ (0, import_jsx_runtime32.jsx)("button", { type: "button", onClick: () => openArtist(artist.uuid), className: "hover:underline", children: artist.name })),
2300
+ renderArtistLink: renderArtistLink ?? ((artist) => artist.isSpecialPurpose ? artist.name : /* @__PURE__ */ (0, import_jsx_runtime33.jsx)("button", { type: "button", onClick: () => openArtist(artist.uuid), className: "hover:underline", children: artist.name })),
2237
2301
  expanded: controlledArtistExpanded ?? (artistUuid !== null && expandedArtistUuid === artistUuid),
2238
2302
  onExpandedChange: (on) => {
2239
2303
  if (controlledArtistExpanded === void 0) setExpandedArtistUuid(on ? artistUuid : null);
@@ -2243,8 +2307,8 @@ function GenreTreeView({
2243
2307
  });
2244
2308
  const builtInArtistPanel = overlayPanel || hasSelectedGenreNode ? void 0 : artistPanel;
2245
2309
  const activeOverlayPanel = overlayPanel ?? (builtInArtistPanel && { ...builtInArtistPanel, hideTitle: true, hideClose: true });
2246
- const [uncontrolledCollapsed, setUncontrolledCollapsed] = (0, import_react13.useState)(false);
2247
- const [uncontrolledExpanded, setUncontrolledExpanded] = (0, import_react13.useState)(false);
2310
+ const [uncontrolledCollapsed, setUncontrolledCollapsed] = (0, import_react15.useState)(false);
2311
+ const [uncontrolledExpanded, setUncontrolledExpanded] = (0, import_react15.useState)(false);
2248
2312
  const isCollapsed = collapsed ?? uncontrolledCollapsed;
2249
2313
  const isExpanded = expanded ?? uncontrolledExpanded;
2250
2314
  const changeCollapsed = (next) => {
@@ -2255,18 +2319,11 @@ function GenreTreeView({
2255
2319
  setUncontrolledExpanded(next);
2256
2320
  onExpandedChange?.(next);
2257
2321
  };
2258
- (0, import_react13.useEffect)(() => {
2322
+ (0, import_react15.useEffect)(() => {
2259
2323
  setUncontrolledCollapsed(false);
2260
2324
  }, [selectedNodeId, artistUuid]);
2261
2325
  const isDetailHidden = isCollapsed || !hasSelectedGenreNode && !activeOverlayPanel;
2262
- (0, import_react13.useEffect)(() => {
2263
- if (placement !== "regions" || !isDetailHidden) return void 0;
2264
- const root = document.documentElement;
2265
- root.style.setProperty("--layout-left-w", "0px");
2266
- return () => {
2267
- root.style.removeProperty("--layout-left-w");
2268
- };
2269
- }, [placement, isDetailHidden]);
2326
+ useLeftDetailPanelLayout(!isDetailHidden, placement === "regions");
2270
2327
  const detailOverlay = placement === "regions" && activeOverlayPanel?.onExpandedChange ? activeOverlayPanel : void 0;
2271
2328
  const detailExpanded = detailOverlay ? !!detailOverlay.expanded : isExpanded;
2272
2329
  const onDetailExpandedChange = detailOverlay ? detailOverlay.onExpandedChange : changeExpanded;
@@ -2278,14 +2335,14 @@ function GenreTreeView({
2278
2335
  hideExpandButton: placement === "regions" ? true : void 0,
2279
2336
  overlayPanel: detailOverlay ? { ...detailOverlay, collapsed: isCollapsed, hideExpandButton: true } : activeOverlayPanel
2280
2337
  };
2281
- const [isWheelRevealed, setIsWheelRevealed] = (0, import_react13.useState)(false);
2338
+ const [isWheelRevealed, setIsWheelRevealed] = (0, import_react15.useState)(false);
2282
2339
  const usesHandoff = viewMode === "wheel" || viewMode === "pop-core";
2283
2340
  const isTreeReady = !isLoading && (!usesHandoff || isWheelRevealed);
2284
- const rootRef = (0, import_react13.useRef)(null);
2341
+ const rootRef = (0, import_react15.useRef)(null);
2285
2342
  const handlePrevious = () => {
2286
2343
  rootRef.current?.querySelector(".search-overlay .gtv-info-panel-previous")?.click();
2287
2344
  };
2288
- (0, import_react13.useEffect)(() => {
2345
+ (0, import_react15.useEffect)(() => {
2289
2346
  if (!isTreeReady || viewMode !== "pop-core" || !hasSelectedGenreNode || !selectedGenrePlaylist) return;
2290
2347
  const root = rootRef.current;
2291
2348
  const centerChip = root?.querySelector(".gtv-wheel-center-node .gtv-wheel-chip");
@@ -2295,21 +2352,21 @@ function GenreTreeView({
2295
2352
  centerChip.click();
2296
2353
  }, [hasSelectedGenreNode, isTreeReady, selectedGenrePlaylist, viewMode]);
2297
2354
  const { showPopup, hidePopup } = (0, import_popup5.usePopup)();
2298
- const [searchText, setSearchText] = (0, import_react13.useState)("");
2355
+ const [searchText, setSearchText] = (0, import_react15.useState)("");
2299
2356
  const artistSearchResult = (0, import_genre_tree16.useArtistSearch)(artistBaseUrl, artistSearch ? searchText : "");
2300
2357
  const artistExtraResults = artistSearch ? {
2301
2358
  label: "Artists",
2302
2359
  items: artistSearchResult.artists.map((artist) => ({
2303
2360
  key: artist.uuid,
2304
- content: /* @__PURE__ */ (0, import_jsx_runtime32.jsxs)(import_jsx_runtime32.Fragment, { children: [
2305
- /* @__PURE__ */ (0, import_jsx_runtime32.jsxs)("span", { className: "block truncate font-medium", children: [
2361
+ content: /* @__PURE__ */ (0, import_jsx_runtime33.jsxs)(import_jsx_runtime33.Fragment, { children: [
2362
+ /* @__PURE__ */ (0, import_jsx_runtime33.jsxs)("span", { className: "block truncate font-medium", children: [
2306
2363
  artist.name,
2307
- artist.disambiguation && /* @__PURE__ */ (0, import_jsx_runtime32.jsxs)("span", { className: "font-normal text-[var(--color-neutral-500)]", children: [
2364
+ artist.disambiguation && /* @__PURE__ */ (0, import_jsx_runtime33.jsxs)("span", { className: "font-normal text-[var(--color-neutral-500)]", children: [
2308
2365
  " \xB7 ",
2309
2366
  artist.disambiguation
2310
2367
  ] })
2311
2368
  ] }),
2312
- artist.matchedAlias && /* @__PURE__ */ (0, import_jsx_runtime32.jsxs)("span", { className: "block truncate text-xs text-[var(--color-neutral-500)]", children: [
2369
+ artist.matchedAlias && /* @__PURE__ */ (0, import_jsx_runtime33.jsxs)("span", { className: "block truncate text-xs text-[var(--color-neutral-500)]", children: [
2313
2370
  "also known as ",
2314
2371
  artist.matchedAlias
2315
2372
  ] })
@@ -2335,13 +2392,13 @@ function GenreTreeView({
2335
2392
  const handleGenreSearchZoomIn = () => {
2336
2393
  if (selectedUuid) focus.onFocusGenreChange?.(selectedUuid, "zoom-in");
2337
2394
  };
2338
- const zoomInRef = (0, import_react13.useRef)(void 0);
2395
+ const zoomInRef = (0, import_react15.useRef)(void 0);
2339
2396
  zoomInRef.current = canZoomIn ? handleGenreSearchZoomIn : void 0;
2340
- const renderInfoPanelBody = (0, import_react13.useCallback)(
2397
+ const renderInfoPanelBody = (0, import_react15.useCallback)(
2341
2398
  (node, { toolbar, ancestors, family }) => {
2342
2399
  const genrePlaylist = genrePlaylistByNodeId.get(node.id);
2343
2400
  const criteria = genrePlaylist?.criteria;
2344
- if (!genrePlaylist || !criteria) return { primary: /* @__PURE__ */ (0, import_jsx_runtime32.jsxs)(import_jsx_runtime32.Fragment, { children: [
2401
+ if (!genrePlaylist || !criteria) return { primary: /* @__PURE__ */ (0, import_jsx_runtime33.jsxs)(import_jsx_runtime33.Fragment, { children: [
2345
2402
  toolbar,
2346
2403
  family
2347
2404
  ] }), aside: ancestors };
@@ -2361,21 +2418,26 @@ function GenreTreeView({
2361
2418
  (sibling) => sibling.parent?.uuid === parentPlaylist.uuid && sibling.uuid !== genrePlaylist.uuid && sibling.criteria?.side === "pop"
2362
2419
  ) : void 0;
2363
2420
  return {
2364
- primary: /* @__PURE__ */ (0, import_jsx_runtime32.jsxs)(import_jsx_runtime32.Fragment, { children: [
2365
- toolbar,
2366
- /* @__PURE__ */ (0, import_jsx_runtime32.jsx)(
2367
- GenreNodeActionBand,
2368
- {
2421
+ primary: /* @__PURE__ */ (0, import_jsx_runtime33.jsx)(
2422
+ GenreInfoPanelBody,
2423
+ {
2424
+ toolbar,
2425
+ family,
2426
+ genreUuid,
2427
+ genrePlaylist,
2428
+ wikidataId: criteria.wikidataId,
2429
+ scope,
2430
+ getBackendBaseUrl,
2431
+ playback,
2432
+ band: {
2369
2433
  node,
2370
- genrePlaylist,
2371
- scope,
2372
2434
  shareTarget: shareUrl ? shareTarget : void 0,
2373
2435
  shareUrl,
2374
2436
  onZoomIn: zoomInRef.current,
2375
2437
  onAddChild: readOnly ? void 0 : () => handleGenreCreationAction(criteria),
2376
2438
  onRename: readOnly ? void 0 : () => handleGenreRenameAction(criteria),
2377
2439
  onChangeParent: readOnly ? void 0 : () => showPopup(
2378
- /* @__PURE__ */ (0, import_jsx_runtime32.jsx)(
2440
+ /* @__PURE__ */ (0, import_jsx_runtime33.jsx)(
2379
2441
  ChangeParentPopup,
2380
2442
  {
2381
2443
  genrePlaylist: { ...genrePlaylist, criteria },
@@ -2390,19 +2452,9 @@ function GenreTreeView({
2390
2452
  side,
2391
2453
  onToggleSide: canToggleSide ? () => updateGenre({ uuid: genreUuid, data: { side: side === "pop" ? "core" : "pop" } }) : void 0,
2392
2454
  toggleSideDisabledReason: popSibling ? `${popSibling.name} is already the pop side of ${parentPlaylist?.name}` : void 0,
2393
- additionalActions,
2394
- playback
2395
- }
2396
- ),
2397
- /* @__PURE__ */ (0, import_jsx_runtime32.jsx)(
2398
- GenreDetailExtras,
2399
- {
2400
- family,
2401
- genreUuid,
2402
- genrePlaylist,
2403
- wikidataId: criteria.wikidataId,
2404
- scope,
2405
- getBackendBaseUrl,
2455
+ additionalActions
2456
+ },
2457
+ extras: {
2406
2458
  criteriaOverviewSchema,
2407
2459
  renderGenreDetailExtras,
2408
2460
  renderArtistLink,
@@ -2412,10 +2464,9 @@ function GenreTreeView({
2412
2464
  renderSongLeading,
2413
2465
  renderSongActions,
2414
2466
  renderTracksToolbar
2415
- },
2416
- genreUuid
2417
- )
2418
- ] }),
2467
+ }
2468
+ }
2469
+ ),
2419
2470
  aside: ancestors
2420
2471
  };
2421
2472
  },
@@ -2446,7 +2497,7 @@ function GenreTreeView({
2446
2497
  );
2447
2498
  const isRotationControlled = controlledAllowWheelRotation !== void 0;
2448
2499
  const allowWheelRotation = controlledAllowWheelRotation ?? internalAllowWheelRotation;
2449
- const detailTab = placement === "regions" && (hasSelectedGenreNode || detailOverlay) ? /* @__PURE__ */ (0, import_jsx_runtime32.jsx)(
2500
+ const detailTab = placement === "regions" && (hasSelectedGenreNode || detailOverlay) ? /* @__PURE__ */ (0, import_jsx_runtime33.jsx)(
2450
2501
  PanelEdgeTab,
2451
2502
  {
2452
2503
  side: "left",
@@ -2456,8 +2507,8 @@ function GenreTreeView({
2456
2507
  ) : null;
2457
2508
  const renderArtistFilter = () => {
2458
2509
  if (isLoading || !filterArtist) return null;
2459
- return /* @__PURE__ */ (0, import_jsx_runtime32.jsxs)("span", { className: "inline-flex items-center overflow-hidden rounded-full bg-[var(--color-neutral-100)] text-sm", children: [
2460
- /* @__PURE__ */ (0, import_jsx_runtime32.jsx)(
2510
+ return /* @__PURE__ */ (0, import_jsx_runtime33.jsxs)("span", { className: "inline-flex items-center overflow-hidden rounded-full bg-[var(--color-neutral-100)] text-sm", children: [
2511
+ /* @__PURE__ */ (0, import_jsx_runtime33.jsx)(
2461
2512
  "button",
2462
2513
  {
2463
2514
  type: "button",
@@ -2469,7 +2520,7 @@ function GenreTreeView({
2469
2520
  children: filterArtist.name
2470
2521
  }
2471
2522
  ),
2472
- /* @__PURE__ */ (0, import_jsx_runtime32.jsx)(
2523
+ /* @__PURE__ */ (0, import_jsx_runtime33.jsx)(
2473
2524
  "button",
2474
2525
  {
2475
2526
  type: "button",
@@ -2481,8 +2532,8 @@ function GenreTreeView({
2481
2532
  )
2482
2533
  ] });
2483
2534
  };
2484
- const actions = /* @__PURE__ */ (0, import_jsx_runtime32.jsxs)(import_jsx_runtime32.Fragment, { children: [
2485
- !isLoading && /* @__PURE__ */ (0, import_jsx_runtime32.jsx)(
2535
+ const actions = /* @__PURE__ */ (0, import_jsx_runtime33.jsxs)(import_jsx_runtime33.Fragment, { children: [
2536
+ !isLoading && /* @__PURE__ */ (0, import_jsx_runtime33.jsx)(
2486
2537
  GenreSearch,
2487
2538
  {
2488
2539
  genrePlaylists: visibleGenrePlaylists,
@@ -2494,9 +2545,9 @@ function GenreTreeView({
2494
2545
  extraResults: artistExtraResults
2495
2546
  }
2496
2547
  ),
2497
- !isLoading && filterArtist && /* @__PURE__ */ (0, import_jsx_runtime32.jsx)("div", { className: "hidden w-full justify-center max-md:flex", children: renderArtistFilter() }),
2498
- /* @__PURE__ */ (0, import_jsx_runtime32.jsxs)("div", { className: "contents max-md:flex max-md:w-full max-md:items-center max-md:gap-2 max-md:overflow-x-auto max-md:[scrollbar-width:none] max-md:[&>*]:shrink-0", children: [
2499
- !isLoading && !isControlled && /* @__PURE__ */ (0, import_jsx_runtime32.jsx)(
2548
+ !isLoading && filterArtist && /* @__PURE__ */ (0, import_jsx_runtime33.jsx)("div", { className: "hidden w-full justify-center max-md:flex", children: renderArtistFilter() }),
2549
+ /* @__PURE__ */ (0, import_jsx_runtime33.jsxs)("div", { className: "contents max-md:flex max-md:w-full max-md:items-center max-md:gap-2 max-md:overflow-x-auto max-md:[scrollbar-width:none] max-md:[&>*]:shrink-0", children: [
2550
+ !isLoading && !isControlled && /* @__PURE__ */ (0, import_jsx_runtime33.jsx)(
2500
2551
  import_ui13.SegmentedControl,
2501
2552
  {
2502
2553
  "aria-label": "Tree view mode",
@@ -2510,13 +2561,13 @@ function GenreTreeView({
2510
2561
  ]
2511
2562
  }
2512
2563
  ),
2513
- !isLoading && showDisplayOptions && /* @__PURE__ */ (0, import_jsx_runtime32.jsx)(
2564
+ !isLoading && showDisplayOptions && /* @__PURE__ */ (0, import_jsx_runtime33.jsx)(
2514
2565
  "div",
2515
2566
  {
2516
2567
  className: "flex items-center gap-1",
2517
2568
  role: "group",
2518
2569
  "aria-label": "Tree display options",
2519
- children: !isRotationControlled && viewMode !== "stacked" && viewMode !== "outline" && /* @__PURE__ */ (0, import_jsx_runtime32.jsx)(
2570
+ children: !isRotationControlled && viewMode !== "stacked" && viewMode !== "outline" && /* @__PURE__ */ (0, import_jsx_runtime33.jsx)(
2520
2571
  import_ui13.Button,
2521
2572
  {
2522
2573
  variant: allowWheelRotation ? "default" : "outline",
@@ -2527,7 +2578,7 @@ function GenreTreeView({
2527
2578
  )
2528
2579
  }
2529
2580
  ),
2530
- !isLoading && !readOnly && showAddRoot && /* @__PURE__ */ (0, import_jsx_runtime32.jsx)(
2581
+ !isLoading && !readOnly && showAddRoot && /* @__PURE__ */ (0, import_jsx_runtime33.jsx)(
2531
2582
  import_ui13.IconTextButton,
2532
2583
  {
2533
2584
  icon: import_lucide_react11.Plus,
@@ -2537,42 +2588,42 @@ function GenreTreeView({
2537
2588
  )
2538
2589
  ] })
2539
2590
  ] });
2540
- return /* @__PURE__ */ (0, import_jsx_runtime32.jsxs)(
2591
+ return /* @__PURE__ */ (0, import_jsx_runtime33.jsxs)(
2541
2592
  "div",
2542
2593
  {
2543
2594
  ref: rootRef,
2544
2595
  className: ["genre-tree-view relative flex flex-col h-full", placement === "regions" && REGIONS_CLASS, className2].filter(Boolean).join(" "),
2545
2596
  children: [
2546
- /* @__PURE__ */ (0, import_jsx_runtime32.jsx)("style", { children: PREVIOUS_BUTTON_CSS }),
2547
- /* @__PURE__ */ (0, import_jsx_runtime32.jsx)(
2597
+ /* @__PURE__ */ (0, import_jsx_runtime33.jsx)("style", { children: PREVIOUS_BUTTON_CSS }),
2598
+ /* @__PURE__ */ (0, import_jsx_runtime33.jsx)(
2548
2599
  "div",
2549
2600
  {
2550
2601
  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"}`,
2551
2602
  children: actions
2552
2603
  }
2553
2604
  ),
2554
- !isLoading && filterArtist && /* @__PURE__ */ (0, import_jsx_runtime32.jsx)(
2605
+ !isLoading && filterArtist && /* @__PURE__ */ (0, import_jsx_runtime33.jsx)(
2555
2606
  "div",
2556
2607
  {
2557
2608
  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"}`,
2558
2609
  children: renderArtistFilter()
2559
2610
  }
2560
2611
  ),
2561
- detailTab && /* @__PURE__ */ (0, import_jsx_runtime32.jsx)(
2612
+ detailTab && /* @__PURE__ */ (0, import_jsx_runtime33.jsx)(
2562
2613
  "div",
2563
2614
  {
2564
2615
  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))]"}`,
2565
2616
  children: detailTab
2566
2617
  }
2567
2618
  ),
2568
- /* @__PURE__ */ (0, import_jsx_runtime32.jsx)("div", { className: "content-container flex-1 min-h-0 flex flex-row gap-4", children: /* @__PURE__ */ (0, import_jsx_runtime32.jsxs)("div", { className: "tree-view-container relative flex-1 min-w-0 flex flex-col h-full", children: [
2569
- !isLoading && viewMode === "outline" && floatingControlsLeading && /* @__PURE__ */ (0, import_jsx_runtime32.jsx)("div", { className: "absolute bottom-3 right-3 z-10", children: floatingControlsLeading }),
2570
- isLoading ? /* @__PURE__ */ (0, import_jsx_runtime32.jsx)(import_genre_tree_view4.GenreTreeViewSkeleton, { viewMode }) : viewMode === "wheel" ? (
2619
+ /* @__PURE__ */ (0, import_jsx_runtime33.jsx)("div", { className: "content-container flex-1 min-h-0 flex flex-row gap-4", children: /* @__PURE__ */ (0, import_jsx_runtime33.jsxs)("div", { className: "tree-view-container relative flex-1 min-w-0 flex flex-col h-full", children: [
2620
+ !isLoading && viewMode === "outline" && floatingControlsLeading && /* @__PURE__ */ (0, import_jsx_runtime33.jsx)("div", { className: "absolute bottom-3 right-3 z-10", children: floatingControlsLeading }),
2621
+ isLoading ? /* @__PURE__ */ (0, import_jsx_runtime33.jsx)(import_genre_tree_view4.GenreTreeViewSkeleton, { viewMode }) : viewMode === "wheel" ? (
2571
2622
  // The info panel's top edge aligns with the tree's; the search bar floats `--layout-panel-inset` below it,
2572
2623
  // and the header inset (the bar's height plus that inset) extends the header's fill behind the bar,
2573
2624
  // Google Maps style; the 8px bottom padding mirrors the inset above the bar, and the hidden title's height is zeroed.
2574
2625
  // The panel's own Zoom in button is hidden: the search bar carries it.
2575
- /* @__PURE__ */ (0, import_jsx_runtime32.jsx)("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__ */ (0, import_jsx_runtime32.jsx)(GenreTreeWheelHandoff, { skeleton: /* @__PURE__ */ (0, import_jsx_runtime32.jsx)(import_genre_tree_view4.GenreTreeWheelSkeleton, {}), onRevealedChange: setIsWheelRevealed, children: /* @__PURE__ */ (0, import_jsx_runtime32.jsx)(
2626
+ /* @__PURE__ */ (0, import_jsx_runtime33.jsx)("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__ */ (0, import_jsx_runtime33.jsx)(GenreTreeWheelHandoff, { skeleton: /* @__PURE__ */ (0, import_jsx_runtime33.jsx)(import_genre_tree_view4.GenreTreeWheelSkeleton, {}), onRevealedChange: setIsWheelRevealed, children: /* @__PURE__ */ (0, import_jsx_runtime33.jsx)(
2576
2627
  GenrePlaylistTreeWheel,
2577
2628
  {
2578
2629
  scope,
@@ -2589,21 +2640,21 @@ function GenreTreeView({
2589
2640
  selectedNodeId,
2590
2641
  readOnly,
2591
2642
  allowWheelRotation,
2592
- controlsOrientation,
2593
2643
  sizeMetric: isArtistFiltered || !canSizeByViews ? "songs" : deferredSizeMetric,
2594
2644
  busy,
2595
- floatingControlsLeading: floatingControlsLeading || canSizeByViews ? /* @__PURE__ */ (0, import_jsx_runtime32.jsxs)("div", { className: "flex items-center gap-2", children: [
2645
+ floatingControlsLeading: floatingControlsLeading || canSizeByViews ? /* @__PURE__ */ (0, import_jsx_runtime33.jsxs)("div", { className: "flex items-center gap-2", children: [
2596
2646
  floatingControlsLeading,
2597
- canSizeByViews && /* @__PURE__ */ (0, import_jsx_runtime32.jsx)(SizeMetricToggle, { value: sizeMetric, onChange: changeSizeMetric })
2647
+ canSizeByViews && /* @__PURE__ */ (0, import_jsx_runtime33.jsx)(SizeMetricToggle, { value: sizeMetric, onChange: changeSizeMetric })
2598
2648
  ] }) : void 0,
2599
2649
  defaultViewport,
2650
+ viewportRestore,
2600
2651
  onViewportChange,
2601
2652
  ...infoPanelCuration
2602
2653
  }
2603
2654
  ) }) })
2604
2655
  ) : viewMode === "pop-core" ? (
2605
2656
  // Same as the wheel: the header inset extends its fill behind the floating search bar, Google Maps style.
2606
- /* @__PURE__ */ (0, import_jsx_runtime32.jsx)("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__ */ (0, import_jsx_runtime32.jsx)(GenreTreeWheelHandoff, { skeleton: /* @__PURE__ */ (0, import_jsx_runtime32.jsx)(import_genre_tree_view4.GenreTreeWheelSkeleton, {}), onRevealedChange: setIsWheelRevealed, children: /* @__PURE__ */ (0, import_jsx_runtime32.jsx)(
2657
+ /* @__PURE__ */ (0, import_jsx_runtime33.jsx)("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__ */ (0, import_jsx_runtime33.jsx)(GenreTreeWheelHandoff, { skeleton: /* @__PURE__ */ (0, import_jsx_runtime33.jsx)(import_genre_tree_view4.GenreTreeWheelSkeleton, {}), onRevealedChange: setIsWheelRevealed, children: /* @__PURE__ */ (0, import_jsx_runtime33.jsx)(
2607
2658
  GenrePlaylistTreeWheelRadialPopCore,
2608
2659
  {
2609
2660
  scope,
@@ -2620,20 +2671,20 @@ function GenreTreeView({
2620
2671
  selectedNodeId,
2621
2672
  readOnly,
2622
2673
  allowWheelRotation,
2623
- controlsOrientation,
2624
2674
  sizeMetric: isArtistFiltered || !canSizeByViews ? "songs" : deferredSizeMetric,
2625
2675
  busy,
2626
- floatingControlsLeading: floatingControlsLeading || canSizeByViews ? /* @__PURE__ */ (0, import_jsx_runtime32.jsxs)("div", { className: "flex items-center gap-2", children: [
2676
+ floatingControlsLeading: floatingControlsLeading || canSizeByViews ? /* @__PURE__ */ (0, import_jsx_runtime33.jsxs)("div", { className: "flex items-center gap-2", children: [
2627
2677
  floatingControlsLeading,
2628
- canSizeByViews && /* @__PURE__ */ (0, import_jsx_runtime32.jsx)(SizeMetricToggle, { value: sizeMetric, onChange: changeSizeMetric })
2678
+ canSizeByViews && /* @__PURE__ */ (0, import_jsx_runtime33.jsx)(SizeMetricToggle, { value: sizeMetric, onChange: changeSizeMetric })
2629
2679
  ] }) : void 0,
2630
2680
  defaultViewport,
2681
+ viewportRestore,
2631
2682
  onViewportChange,
2632
2683
  ...infoPanelCuration,
2633
2684
  ...focus
2634
2685
  }
2635
2686
  ) }) })
2636
- ) : viewMode === "outline" ? /* @__PURE__ */ (0, import_jsx_runtime32.jsx)("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__ */ (0, import_jsx_runtime32.jsx)(
2687
+ ) : viewMode === "outline" ? /* @__PURE__ */ (0, import_jsx_runtime33.jsx)("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__ */ (0, import_jsx_runtime33.jsx)(
2637
2688
  GenrePlaylistTreeWheelRadialPopCore,
2638
2689
  {
2639
2690
  outline: true,
@@ -2652,13 +2703,13 @@ function GenreTreeView({
2652
2703
  readOnly,
2653
2704
  ...infoPanelCuration
2654
2705
  }
2655
- ) }) : /* @__PURE__ */ (0, import_jsx_runtime32.jsx)("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(
2706
+ ) }) : /* @__PURE__ */ (0, import_jsx_runtime33.jsx)("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(
2656
2707
  ([uuid, genrePlaylistTreePerRoot], rootIndex) => {
2657
- return /* @__PURE__ */ (0, import_jsx_runtime32.jsx)(
2708
+ return /* @__PURE__ */ (0, import_jsx_runtime33.jsx)(
2658
2709
  "div",
2659
2710
  {
2660
2711
  className: "tree-per-root-container relative shrink-0 h-[28rem] mt-2 mr-16 p-2 bg-[var(--color-neutral-100)] rounded-lg",
2661
- children: /* @__PURE__ */ (0, import_jsx_runtime32.jsx)("div", { className: "graph-container relative z-10 h-full", children: /* @__PURE__ */ (0, import_jsx_runtime32.jsx)(
2712
+ children: /* @__PURE__ */ (0, import_jsx_runtime33.jsx)("div", { className: "graph-container relative z-10 h-full", children: /* @__PURE__ */ (0, import_jsx_runtime33.jsx)(
2662
2713
  GenrePlaylistTreePerRoot,
2663
2714
  {
2664
2715
  scope,
@@ -2676,8 +2727,8 @@ function GenreTreeView({
2676
2727
  selectedNodeId: uuid === selectedGenrePlaylist?.root.uuid ? selectedNodeId : null,
2677
2728
  readOnly,
2678
2729
  wheelZoom: "modifier",
2679
- controlsOrientation,
2680
2730
  defaultViewport: uuid === selectedGenrePlaylist?.root.uuid ? defaultViewport : void 0,
2731
+ viewportRestore: uuid === selectedGenrePlaylist?.root.uuid && uuid === restoreRootUuid ? viewportRestore : void 0,
2681
2732
  onViewportChange: uuid === selectedGenrePlaylist?.root.uuid ? onViewportChange : void 0,
2682
2733
  ...infoPanelCuration,
2683
2734
  overlayPanel: rootIndex === 0 ? activeOverlayPanel : void 0
@@ -2694,17 +2745,126 @@ function GenreTreeView({
2694
2745
  );
2695
2746
  }
2696
2747
 
2697
- // src/genre-tree/index.ts
2748
+ // src/genre-tree/GenreDetailPanel.tsx
2749
+ var import_react16 = require("react");
2698
2750
  var import_genre_tree_view5 = require("@behindthemusictree/genre-tree-view");
2751
+ var import_ui14 = require("@behindthemusictree/ui");
2752
+ var import_genre_tree18 = require("@behindthemusictree/app-kit/genre-tree");
2753
+ var import_jsx_runtime34 = require("react/jsx-runtime");
2754
+ var ITEM_COUNT_LABEL = "Songs";
2755
+ var RELATIVE_ITEM_COUNT = 1;
2756
+ function toNode(genre, parentId) {
2757
+ return {
2758
+ id: genre.uuid,
2759
+ parentId,
2760
+ name: genre.name,
2761
+ itemCount: RELATIVE_ITEM_COUNT,
2762
+ side: genre.side ?? void 0,
2763
+ wikidataId: genre.wikidataId
2764
+ };
2765
+ }
2766
+ function GenreDetailPanel({
2767
+ genreUuid,
2768
+ scope,
2769
+ getBackendBaseUrl,
2770
+ onClose,
2771
+ onSelectGenre,
2772
+ onZoomIn,
2773
+ shareUrl,
2774
+ shareTarget,
2775
+ collapsed,
2776
+ expanded,
2777
+ onExpandedChange,
2778
+ extras
2779
+ }) {
2780
+ const { data: detail, isPending, isError } = (0, import_genre_tree18.useFetchGenreDetail)(genreUuid, scope, getBackendBaseUrl);
2781
+ const playback = useGenrePlaylistPlayback({ scope, genrePlaylists: [] });
2782
+ const view = (0, import_react16.useMemo)(() => {
2783
+ if (!detail) return null;
2784
+ const rootColor = (0, import_genre_tree_view5.getGenreTreeColor)(detail.root.wikidataId);
2785
+ const relative = (genre, parentId) => ({
2786
+ node: toNode(genre, parentId),
2787
+ fill: (0, import_genre_tree_view5.tintSurface)(rootColor),
2788
+ textColor: import_genre_tree_view5.TEXT_COLOR
2789
+ });
2790
+ const ancestors = [...detail.ascendants].sort((a, b) => b.degree - a.degree);
2791
+ return {
2792
+ node: {
2793
+ id: detail.uuid,
2794
+ parentId: detail.parent?.uuid ?? null,
2795
+ name: detail.name,
2796
+ itemCount: detail.tracksCount,
2797
+ wikidataId: detail.wikidataId
2798
+ },
2799
+ fill: rootColor,
2800
+ parentNode: detail.parent ? relative(detail.parent, null) : null,
2801
+ childNodes: detail.children.map((child) => relative(child, detail.uuid)),
2802
+ ancestorNodes: ancestors.map(
2803
+ ({ ascendant }, index) => relative(ascendant, index === 0 ? null : ancestors[index - 1].ascendant.uuid)
2804
+ )
2805
+ };
2806
+ }, [detail]);
2807
+ if (isError) {
2808
+ return /* @__PURE__ */ (0, import_jsx_runtime34.jsx)("div", { role: "alert", className: "p-4 text-sm text-[var(--color-text)]", children: "This genre could not be loaded." });
2809
+ }
2810
+ if (isPending || !detail || !view) {
2811
+ return /* @__PURE__ */ (0, import_jsx_runtime34.jsxs)("div", { className: "flex flex-col gap-3 p-4", "aria-busy": "true", "data-testid": "genre-detail-panel-skeleton", children: [
2812
+ /* @__PURE__ */ (0, import_jsx_runtime34.jsx)(import_ui14.Skeleton, { className: "h-6 w-2/3" }),
2813
+ /* @__PURE__ */ (0, import_jsx_runtime34.jsx)(import_ui14.Skeleton, { className: "h-4 w-full" }),
2814
+ /* @__PURE__ */ (0, import_jsx_runtime34.jsx)(import_ui14.Skeleton, { className: "h-4 w-3/4" })
2815
+ ] });
2816
+ }
2817
+ const renderBody = (node, sections) => ({
2818
+ primary: /* @__PURE__ */ (0, import_jsx_runtime34.jsx)(
2819
+ GenreInfoPanelBody,
2820
+ {
2821
+ toolbar: sections.toolbar,
2822
+ family: sections.family,
2823
+ genreUuid: detail.uuid,
2824
+ genrePlaylist: { ...detail.criteriaPlaylist, tracksCount: detail.tracksCount },
2825
+ wikidataId: detail.wikidataId,
2826
+ scope,
2827
+ getBackendBaseUrl,
2828
+ playback,
2829
+ band: { node, onZoomIn, shareUrl, shareTarget: shareUrl ? shareTarget : void 0 },
2830
+ extras
2831
+ }
2832
+ ),
2833
+ aside: null
2834
+ });
2835
+ return /* @__PURE__ */ (0, import_jsx_runtime34.jsx)(
2836
+ import_genre_tree_view5.InfoPanel,
2837
+ {
2838
+ node: view.node,
2839
+ fill: view.fill,
2840
+ textColor: import_genre_tree_view5.ACCENT_TEXT_COLOR,
2841
+ parentNode: view.parentNode,
2842
+ childNodes: view.childNodes,
2843
+ ancestorNodes: view.ancestorNodes,
2844
+ side: "left",
2845
+ onClose,
2846
+ onSelectNode: (nodeId) => onSelectGenre(nodeId),
2847
+ itemCountLabel: ITEM_COUNT_LABEL,
2848
+ renderInfoPanelBody: renderBody,
2849
+ expanded,
2850
+ onExpandedChange,
2851
+ collapsed,
2852
+ hideExpandButton: onExpandedChange === void 0
2853
+ }
2854
+ );
2855
+ }
2856
+
2857
+ // src/genre-tree/index.ts
2858
+ var import_genre_tree_view6 = require("@behindthemusictree/genre-tree-view");
2699
2859
 
2700
2860
  // src/genre-tree/track-list-sidebar/TrackListSidebar.tsx
2701
- var import_react14 = require("react");
2861
+ var import_react17 = require("react");
2702
2862
  var import_lucide_react12 = require("lucide-react");
2703
- var import_genre_tree18 = require("@behindthemusictree/app-kit/genre-tree");
2704
2863
  var import_genre_tree19 = require("@behindthemusictree/app-kit/genre-tree");
2864
+ var import_genre_tree20 = require("@behindthemusictree/app-kit/genre-tree");
2705
2865
  var import_track_noun6 = require("@behindthemusictree/app-kit/track-noun");
2706
- var import_jsx_runtime33 = require("react/jsx-runtime");
2707
- var useBrowserLayoutEffect = typeof window === "undefined" ? import_react14.useEffect : import_react14.useLayoutEffect;
2866
+ var import_jsx_runtime35 = require("react/jsx-runtime");
2867
+ var useBrowserLayoutEffect = typeof window === "undefined" ? import_react17.useEffect : import_react17.useLayoutEffect;
2708
2868
  function TrackListSidebar({
2709
2869
  className: className2 = "",
2710
2870
  renderDuration,
@@ -2715,18 +2875,18 @@ function TrackListSidebar({
2715
2875
  onMinimize,
2716
2876
  renderArtistLink
2717
2877
  }) {
2718
- const { trackList, loadMore, autoAdvanceScrollRequest } = (0, import_genre_tree18.useTrackList)();
2878
+ const { trackList, loadMore, autoAdvanceScrollRequest } = (0, import_genre_tree19.useTrackList)();
2719
2879
  const trackNoun = (0, import_track_noun6.useTrackNoun)();
2720
- const trackListRef = (0, import_react14.useRef)(null);
2721
- const previousOriginRef = (0, import_react14.useRef)(trackList?.origin ?? null);
2722
- const handledAutoAdvanceRequestIdRef = (0, import_react14.useRef)(null);
2723
- const shouldFollowPlaybackRef = (0, import_react14.useRef)(true);
2724
- const programmaticScrollTopRef = (0, import_react14.useRef)(null);
2725
- const userScrollIntentRef = (0, import_react14.useRef)(false);
2726
- const userScrollIntentTimerRef = (0, import_react14.useRef)(null);
2727
- const loadMoreSentinelRef = (0, import_react14.useRef)(null);
2880
+ const trackListRef = (0, import_react17.useRef)(null);
2881
+ const previousOriginRef = (0, import_react17.useRef)(trackList?.origin ?? null);
2882
+ const handledAutoAdvanceRequestIdRef = (0, import_react17.useRef)(null);
2883
+ const shouldFollowPlaybackRef = (0, import_react17.useRef)(true);
2884
+ const programmaticScrollTopRef = (0, import_react17.useRef)(null);
2885
+ const userScrollIntentRef = (0, import_react17.useRef)(false);
2886
+ const userScrollIntentTimerRef = (0, import_react17.useRef)(null);
2887
+ const loadMoreSentinelRef = (0, import_react17.useRef)(null);
2728
2888
  const hasMore = !!trackList && trackList.nextPage !== null;
2729
- (0, import_react14.useEffect)(() => {
2889
+ (0, import_react17.useEffect)(() => {
2730
2890
  const sentinel = loadMoreSentinelRef.current;
2731
2891
  if (!hasMore || !sentinel) return;
2732
2892
  const observer = new IntersectionObserver((entries) => {
@@ -2746,11 +2906,11 @@ function TrackListSidebar({
2746
2906
  programmaticScrollTopRef.current = null;
2747
2907
  if (trackListRef.current) trackListRef.current.scrollTop = 0;
2748
2908
  }, [trackList?.origin]);
2749
- (0, import_react14.useEffect)(() => {
2909
+ (0, import_react17.useEffect)(() => {
2750
2910
  const list = trackListRef.current;
2751
2911
  if (!isCollapsed && list?.scrollTop === 0) shouldFollowPlaybackRef.current = true;
2752
2912
  }, [isCollapsed]);
2753
- (0, import_react14.useEffect)(
2913
+ (0, import_react17.useEffect)(
2754
2914
  () => () => {
2755
2915
  if (userScrollIntentTimerRef.current !== null) clearTimeout(userScrollIntentTimerRef.current);
2756
2916
  },
@@ -2804,25 +2964,25 @@ function TrackListSidebar({
2804
2964
  }
2805
2965
  };
2806
2966
  const positionClasses = layout === "inline" ? "relative w-full flex h-full flex-col" : "fixed right-0 w-[50rem] flex flex-col";
2807
- return trackList ? /* @__PURE__ */ (0, import_jsx_runtime33.jsxs)(
2967
+ return trackList ? /* @__PURE__ */ (0, import_jsx_runtime35.jsxs)(
2808
2968
  "div",
2809
2969
  {
2810
2970
  "data-theme": "dark",
2811
2971
  className: `track-list-sidebar ${positionClasses} rounded-2xl bg-[var(--color-surface)] pb-1 ${className2}`,
2812
2972
  style: layout === "fixed" ? { top: "63.5px", maxHeight: "calc(100vh - 63.5px - 79px)" } : void 0,
2813
2973
  children: [
2814
- /* @__PURE__ */ (0, import_jsx_runtime33.jsxs)("div", { className: "header flex h-16 items-center px-4 py-2 text-[var(--color-text)]", children: [
2815
- /* @__PURE__ */ (0, import_jsx_runtime33.jsxs)("div", { className: "min-w-0 flex flex-1 flex-col justify-center", children: [
2816
- /* @__PURE__ */ (0, import_jsx_runtime33.jsxs)("div", { className: "origin flex text-xl", children: [
2817
- /* @__PURE__ */ (0, import_jsx_runtime33.jsx)("div", { className: "from h-auto flex flex-col justify-center items-center mr-2", children: "From" }),
2818
- /* @__PURE__ */ (0, import_jsx_runtime33.jsx)("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__ */ (0, import_jsx_runtime33.jsx)("div", { className: "name max-w-full truncate", children: trackList.origin.label }) })
2974
+ /* @__PURE__ */ (0, import_jsx_runtime35.jsxs)("div", { className: "header flex h-16 items-center px-4 py-2 text-[var(--color-text)]", children: [
2975
+ /* @__PURE__ */ (0, import_jsx_runtime35.jsxs)("div", { className: "min-w-0 flex flex-1 flex-col justify-center", children: [
2976
+ /* @__PURE__ */ (0, import_jsx_runtime35.jsxs)("div", { className: "origin flex text-xl", children: [
2977
+ /* @__PURE__ */ (0, import_jsx_runtime35.jsx)("div", { className: "from h-auto flex flex-col justify-center items-center mr-2", children: "From" }),
2978
+ /* @__PURE__ */ (0, import_jsx_runtime35.jsx)("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__ */ (0, import_jsx_runtime35.jsx)("div", { className: "name max-w-full truncate", children: trackList.origin.label }) })
2819
2979
  ] }),
2820
- /* @__PURE__ */ (0, import_jsx_runtime33.jsxs)("div", { className: "info flex items-center text-sm text-[var(--color-text-muted)]", children: [
2821
- trackList.origin.type === import_genre_tree19.TrackListOriginType.GENRE_PLAYLIST ? "Genre \u2022 " : trackList.origin.type === import_genre_tree19.TrackListOriginType.ARTIST ? "Artist \u2022 " : `${trackNoun.singular} playlist \u2022 `,
2980
+ /* @__PURE__ */ (0, import_jsx_runtime35.jsxs)("div", { className: "info flex items-center text-sm text-[var(--color-text-muted)]", children: [
2981
+ trackList.origin.type === import_genre_tree20.TrackListOriginType.GENRE_PLAYLIST ? "Genre \u2022 " : trackList.origin.type === import_genre_tree20.TrackListOriginType.ARTIST ? "Artist \u2022 " : `${trackNoun.singular} playlist \u2022 `,
2822
2982
  `${trackList.total} ${trackList.total > 1 ? trackNoun.plural : trackNoun.singular}`
2823
2983
  ] })
2824
2984
  ] }),
2825
- (isCollapsed && onExpand || !isCollapsed && onMinimize) && /* @__PURE__ */ (0, import_jsx_runtime33.jsx)(
2985
+ (isCollapsed && onExpand || !isCollapsed && onMinimize) && /* @__PURE__ */ (0, import_jsx_runtime35.jsx)(
2826
2986
  "button",
2827
2987
  {
2828
2988
  type: "button",
@@ -2831,11 +2991,11 @@ function TrackListSidebar({
2831
2991
  "aria-expanded": !isCollapsed,
2832
2992
  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"}`,
2833
2993
  onClick: isCollapsed ? onExpand : onMinimize,
2834
- children: /* @__PURE__ */ (0, import_jsx_runtime33.jsx)(import_lucide_react12.ListMusic, { "aria-hidden": "true", className: "h-5 w-5" })
2994
+ children: /* @__PURE__ */ (0, import_jsx_runtime35.jsx)(import_lucide_react12.ListMusic, { "aria-hidden": "true", className: "h-5 w-5" })
2835
2995
  }
2836
2996
  )
2837
2997
  ] }),
2838
- !isCollapsed && /* @__PURE__ */ (0, import_jsx_runtime33.jsxs)(
2998
+ !isCollapsed && /* @__PURE__ */ (0, import_jsx_runtime35.jsxs)(
2839
2999
  "ul",
2840
3000
  {
2841
3001
  ref: trackListRef,
@@ -2845,7 +3005,7 @@ function TrackListSidebar({
2845
3005
  onPointerDown: markUserScrollIntent,
2846
3006
  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",
2847
3007
  children: [
2848
- trackList.tracks.map((track, index) => /* @__PURE__ */ (0, import_jsx_runtime33.jsx)("li", { "data-track-uuid": track.uuid, children: /* @__PURE__ */ (0, import_jsx_runtime33.jsx)(
3008
+ trackList.tracks.map((track, index) => /* @__PURE__ */ (0, import_jsx_runtime35.jsx)("li", { "data-track-uuid": track.uuid, children: /* @__PURE__ */ (0, import_jsx_runtime35.jsx)(
2849
3009
  TrackItem,
2850
3010
  {
2851
3011
  track,
@@ -2855,7 +3015,7 @@ function TrackListSidebar({
2855
3015
  renderArtistLink
2856
3016
  }
2857
3017
  ) }, track.uuid)),
2858
- hasMore && /* @__PURE__ */ (0, import_jsx_runtime33.jsx)("li", { ref: loadMoreSentinelRef, "aria-hidden": "true", className: "h-px" })
3018
+ hasMore && /* @__PURE__ */ (0, import_jsx_runtime35.jsx)("li", { ref: loadMoreSentinelRef, "aria-hidden": "true", className: "h-px" })
2859
3019
  ]
2860
3020
  }
2861
3021
  )
@@ -2865,16 +3025,16 @@ function TrackListSidebar({
2865
3025
  }
2866
3026
 
2867
3027
  // src/genre-tree/PlayerPanel.tsx
2868
- var import_react15 = require("react");
3028
+ var import_react18 = require("react");
2869
3029
  var import_tailwind_merge2 = require("tailwind-merge");
2870
3030
  var import_player12 = require("@behindthemusictree/app-kit/player");
2871
- var import_genre_tree21 = require("@behindthemusictree/app-kit/genre-tree");
3031
+ var import_genre_tree22 = require("@behindthemusictree/app-kit/genre-tree");
2872
3032
 
2873
3033
  // src/genre-tree/PlayerControls.tsx
2874
3034
  var import_lucide_react13 = require("lucide-react");
2875
- var import_genre_tree20 = require("@behindthemusictree/app-kit/genre-tree");
3035
+ var import_genre_tree21 = require("@behindthemusictree/app-kit/genre-tree");
2876
3036
  var import_player11 = require("@behindthemusictree/app-kit/player");
2877
- var import_jsx_runtime34 = require("react/jsx-runtime");
3037
+ var import_jsx_runtime36 = require("react/jsx-runtime");
2878
3038
  function PlayerControls() {
2879
3039
  const {
2880
3040
  duration,
@@ -2886,22 +3046,22 @@ function PlayerControls() {
2886
3046
  setVolume,
2887
3047
  volume
2888
3048
  } = (0, import_player11.usePlayer)();
2889
- const { hasNextTrack, hasPreviousTrack, playNextTrack, playPreviousTrack } = (0, import_genre_tree20.useTrackList)();
3049
+ const { hasNextTrack, hasPreviousTrack, playNextTrack, playPreviousTrack } = (0, import_genre_tree21.useTrackList)();
2890
3050
  const currentTime = (0, import_player11.useCurrentTime)();
2891
3051
  const isBusy = isLoading || playState === import_player11.PlayStates.LOADING;
2892
3052
  const isReady = !!playerTrackObject?.isReady && !isBusy && !playerTrackObject.loadError;
2893
3053
  const safeDuration = Number.isFinite(duration) && duration > 0 ? duration : 0;
2894
3054
  const safeCurrentTime = Number.isFinite(currentTime) ? Math.max(0, Math.min(currentTime, safeDuration)) : 0;
2895
3055
  const isPlaying = playState === import_player11.PlayStates.PLAYING;
2896
- return /* @__PURE__ */ (0, import_jsx_runtime34.jsxs)(
3056
+ return /* @__PURE__ */ (0, import_jsx_runtime36.jsxs)(
2897
3057
  "section",
2898
3058
  {
2899
3059
  "aria-label": "Player controls",
2900
3060
  className: "player-controls flex shrink-0 flex-col gap-2 border-b border-zinc-800 px-3 py-2 text-zinc-200",
2901
3061
  children: [
2902
- /* @__PURE__ */ (0, import_jsx_runtime34.jsxs)("div", { className: "flex min-w-0 items-center gap-2 text-xs tabular-nums text-zinc-400", children: [
2903
- /* @__PURE__ */ (0, import_jsx_runtime34.jsx)("span", { "aria-label": "Elapsed time", children: (0, import_genre_tree20.formatTime)(safeCurrentTime) }),
2904
- /* @__PURE__ */ (0, import_jsx_runtime34.jsx)(
3062
+ /* @__PURE__ */ (0, import_jsx_runtime36.jsxs)("div", { className: "flex min-w-0 items-center gap-2 text-xs tabular-nums text-zinc-400", children: [
3063
+ /* @__PURE__ */ (0, import_jsx_runtime36.jsx)("span", { "aria-label": "Elapsed time", children: (0, import_genre_tree21.formatTime)(safeCurrentTime) }),
3064
+ /* @__PURE__ */ (0, import_jsx_runtime36.jsx)(
2905
3065
  "input",
2906
3066
  {
2907
3067
  "aria-label": "Seek",
@@ -2915,11 +3075,11 @@ function PlayerControls() {
2915
3075
  onChange: (event) => seekTo(Number(event.currentTarget.value))
2916
3076
  }
2917
3077
  ),
2918
- /* @__PURE__ */ (0, import_jsx_runtime34.jsx)("span", { "aria-label": "Duration", children: (0, import_genre_tree20.formatTime)(safeDuration) })
3078
+ /* @__PURE__ */ (0, import_jsx_runtime36.jsx)("span", { "aria-label": "Duration", children: (0, import_genre_tree21.formatTime)(safeDuration) })
2919
3079
  ] }),
2920
- /* @__PURE__ */ (0, import_jsx_runtime34.jsxs)("div", { className: "grid min-h-9 grid-cols-[1fr_auto_1fr] items-center gap-3", children: [
2921
- /* @__PURE__ */ (0, import_jsx_runtime34.jsxs)("div", { className: "col-start-2 flex items-center justify-center gap-2", children: [
2922
- /* @__PURE__ */ (0, import_jsx_runtime34.jsx)(
3080
+ /* @__PURE__ */ (0, import_jsx_runtime36.jsxs)("div", { className: "grid min-h-9 grid-cols-[1fr_auto_1fr] items-center gap-3", children: [
3081
+ /* @__PURE__ */ (0, import_jsx_runtime36.jsxs)("div", { className: "col-start-2 flex items-center justify-center gap-2", children: [
3082
+ /* @__PURE__ */ (0, import_jsx_runtime36.jsx)(
2923
3083
  "button",
2924
3084
  {
2925
3085
  type: "button",
@@ -2928,10 +3088,10 @@ function PlayerControls() {
2928
3088
  disabled: !isReady || !hasPreviousTrack,
2929
3089
  onClick: playPreviousTrack,
2930
3090
  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",
2931
- children: /* @__PURE__ */ (0, import_jsx_runtime34.jsx)(import_lucide_react13.SkipBack, { "aria-hidden": "true", className: "h-4 w-4" })
3091
+ children: /* @__PURE__ */ (0, import_jsx_runtime36.jsx)(import_lucide_react13.SkipBack, { "aria-hidden": "true", className: "h-4 w-4" })
2932
3092
  }
2933
3093
  ),
2934
- /* @__PURE__ */ (0, import_jsx_runtime34.jsx)(
3094
+ /* @__PURE__ */ (0, import_jsx_runtime36.jsx)(
2935
3095
  "button",
2936
3096
  {
2937
3097
  type: "button",
@@ -2940,10 +3100,10 @@ function PlayerControls() {
2940
3100
  disabled: !isReady,
2941
3101
  onClick: handlePlayPauseAction,
2942
3102
  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",
2943
- children: isPlaying ? /* @__PURE__ */ (0, import_jsx_runtime34.jsx)(import_lucide_react13.Pause, { "aria-hidden": "true", className: "h-4 w-4", fill: "currentColor" }) : /* @__PURE__ */ (0, import_jsx_runtime34.jsx)(import_lucide_react13.Play, { "aria-hidden": "true", className: "h-4 w-4", fill: "currentColor" })
3103
+ children: isPlaying ? /* @__PURE__ */ (0, import_jsx_runtime36.jsx)(import_lucide_react13.Pause, { "aria-hidden": "true", className: "h-4 w-4", fill: "currentColor" }) : /* @__PURE__ */ (0, import_jsx_runtime36.jsx)(import_lucide_react13.Play, { "aria-hidden": "true", className: "h-4 w-4", fill: "currentColor" })
2944
3104
  }
2945
3105
  ),
2946
- /* @__PURE__ */ (0, import_jsx_runtime34.jsx)(
3106
+ /* @__PURE__ */ (0, import_jsx_runtime36.jsx)(
2947
3107
  "button",
2948
3108
  {
2949
3109
  type: "button",
@@ -2952,14 +3112,14 @@ function PlayerControls() {
2952
3112
  disabled: !isReady || !hasNextTrack,
2953
3113
  onClick: playNextTrack,
2954
3114
  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",
2955
- children: /* @__PURE__ */ (0, import_jsx_runtime34.jsx)(import_lucide_react13.SkipForward, { "aria-hidden": "true", className: "h-4 w-4" })
3115
+ children: /* @__PURE__ */ (0, import_jsx_runtime36.jsx)(import_lucide_react13.SkipForward, { "aria-hidden": "true", className: "h-4 w-4" })
2956
3116
  }
2957
3117
  )
2958
3118
  ] }),
2959
- /* @__PURE__ */ (0, import_jsx_runtime34.jsxs)("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: [
2960
- volume === 0 ? /* @__PURE__ */ (0, import_jsx_runtime34.jsx)(import_lucide_react13.VolumeX, { "aria-hidden": "true", className: "h-[0.7rem] w-[0.7rem] shrink-0" }) : /* @__PURE__ */ (0, import_jsx_runtime34.jsx)(import_lucide_react13.Volume2, { "aria-hidden": "true", className: "h-[0.7rem] w-[0.7rem] shrink-0" }),
2961
- /* @__PURE__ */ (0, import_jsx_runtime34.jsx)("span", { className: "sr-only", children: "Volume" }),
2962
- /* @__PURE__ */ (0, import_jsx_runtime34.jsx)(
3119
+ /* @__PURE__ */ (0, import_jsx_runtime36.jsxs)("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: [
3120
+ volume === 0 ? /* @__PURE__ */ (0, import_jsx_runtime36.jsx)(import_lucide_react13.VolumeX, { "aria-hidden": "true", className: "h-[0.7rem] w-[0.7rem] shrink-0" }) : /* @__PURE__ */ (0, import_jsx_runtime36.jsx)(import_lucide_react13.Volume2, { "aria-hidden": "true", className: "h-[0.7rem] w-[0.7rem] shrink-0" }),
3121
+ /* @__PURE__ */ (0, import_jsx_runtime36.jsx)("span", { className: "sr-only", children: "Volume" }),
3122
+ /* @__PURE__ */ (0, import_jsx_runtime36.jsx)(
2963
3123
  "input",
2964
3124
  {
2965
3125
  "aria-label": "Volume",
@@ -2981,7 +3141,7 @@ function PlayerControls() {
2981
3141
  }
2982
3142
 
2983
3143
  // src/genre-tree/PlayerPanel.tsx
2984
- var import_jsx_runtime35 = require("react/jsx-runtime");
3144
+ var import_jsx_runtime37 = require("react/jsx-runtime");
2985
3145
  var PANEL_ID = "player-panel";
2986
3146
  var DEFAULT_EXTENDED_WIDTH = "calc(100vw - var(--layout-center-left) - var(--layout-gutter))";
2987
3147
  function PlayerPanel({
@@ -2993,13 +3153,13 @@ function PlayerPanel({
2993
3153
  renderArtistLink
2994
3154
  }) {
2995
3155
  const { playerTrackObject } = (0, import_player12.usePlayer)();
2996
- const { toggleTrackListSidebar, isTrackListSidebarVisible } = (0, import_genre_tree21.useTrackListSidebarVisibility)();
2997
- const [lastLoadedTrackTitle, setLastLoadedTrackTitle] = (0, import_react15.useState)("");
3156
+ const { toggleTrackListSidebar, isTrackListSidebarVisible } = (0, import_genre_tree22.useTrackListSidebarVisibility)();
3157
+ const [lastLoadedTrackTitle, setLastLoadedTrackTitle] = (0, import_react18.useState)("");
2998
3158
  const loadedTrackTitle = playerTrackObject?.track.title;
2999
- (0, import_react15.useEffect)(() => {
3159
+ (0, import_react18.useEffect)(() => {
3000
3160
  if (loadedTrackTitle) setLastLoadedTrackTitle(loadedTrackTitle);
3001
3161
  }, [loadedTrackTitle]);
3002
- const [uncontrolledExtended, setUncontrolledExtended] = (0, import_react15.useState)(false);
3162
+ const [uncontrolledExtended, setUncontrolledExtended] = (0, import_react18.useState)(false);
3003
3163
  const isExtendRequested = expanded ?? uncontrolledExtended;
3004
3164
  const setIsExtendRequested = (next) => {
3005
3165
  setUncontrolledExtended(next);
@@ -3020,7 +3180,7 @@ function PlayerPanel({
3020
3180
  onClick: () => setIsExtendRequested(true),
3021
3181
  expanded: false
3022
3182
  } : null;
3023
- return /* @__PURE__ */ (0, import_jsx_runtime35.jsxs)(
3183
+ return /* @__PURE__ */ (0, import_jsx_runtime37.jsxs)(
3024
3184
  "div",
3025
3185
  {
3026
3186
  id: PANEL_ID,
@@ -3033,7 +3193,7 @@ function PlayerPanel({
3033
3193
  ),
3034
3194
  style: isExtended ? { width: extendedWidth } : void 0,
3035
3195
  children: [
3036
- /* @__PURE__ */ (0, import_jsx_runtime35.jsx)(
3196
+ /* @__PURE__ */ (0, import_jsx_runtime37.jsx)(
3037
3197
  PanelEdgeTab,
3038
3198
  {
3039
3199
  side: "right",
@@ -3043,7 +3203,7 @@ function PlayerPanel({
3043
3203
  className: "absolute right-full top-1/2 -translate-y-1/2"
3044
3204
  }
3045
3205
  ),
3046
- /* @__PURE__ */ (0, import_jsx_runtime35.jsxs)(
3206
+ /* @__PURE__ */ (0, import_jsx_runtime37.jsxs)(
3047
3207
  "div",
3048
3208
  {
3049
3209
  className: (0, import_tailwind_merge2.twMerge)(
@@ -3051,15 +3211,15 @@ function PlayerPanel({
3051
3211
  isExtended ? "flex-row" : "flex-col"
3052
3212
  ),
3053
3213
  children: [
3054
- /* @__PURE__ */ (0, import_jsx_runtime35.jsxs)("div", { className: "flex w-[var(--layout-right-w)] shrink-0 flex-col", children: [
3055
- /* @__PURE__ */ (0, import_jsx_runtime35.jsx)("div", { className: "flex shrink-0 items-center justify-between gap-2 border-b border-zinc-800 p-2", children: /* @__PURE__ */ (0, import_jsx_runtime35.jsx)("span", { className: "min-w-0 truncate text-sm font-semibold", children: loadedTrackTitle || lastLoadedTrackTitle }) }),
3056
- /* @__PURE__ */ (0, import_jsx_runtime35.jsxs)("div", { className: "relative w-full shrink-0", children: [
3057
- /* @__PURE__ */ (0, import_jsx_runtime35.jsx)(import_player12.PlayerVideoSurface, { className: "aspect-video w-full bg-black" }),
3058
- playerTrackObject?.loadError && /* @__PURE__ */ (0, import_jsx_runtime35.jsx)("span", { className: "absolute bottom-2 left-2 right-2 text-xs text-red-400", children: playerTrackObject.loadError })
3214
+ /* @__PURE__ */ (0, import_jsx_runtime37.jsxs)("div", { className: "flex w-[var(--layout-right-w)] shrink-0 flex-col", children: [
3215
+ /* @__PURE__ */ (0, import_jsx_runtime37.jsx)("div", { className: "flex shrink-0 items-center justify-between gap-2 border-b border-zinc-800 p-2", children: /* @__PURE__ */ (0, import_jsx_runtime37.jsx)("span", { className: "min-w-0 truncate text-sm font-semibold", children: loadedTrackTitle || lastLoadedTrackTitle }) }),
3216
+ /* @__PURE__ */ (0, import_jsx_runtime37.jsxs)("div", { className: "relative w-full shrink-0", children: [
3217
+ /* @__PURE__ */ (0, import_jsx_runtime37.jsx)(import_player12.PlayerVideoSurface, { className: "aspect-video w-full bg-black" }),
3218
+ playerTrackObject?.loadError && /* @__PURE__ */ (0, import_jsx_runtime37.jsx)("span", { className: "absolute bottom-2 left-2 right-2 text-xs text-red-400", children: playerTrackObject.loadError })
3059
3219
  ] }),
3060
- /* @__PURE__ */ (0, import_jsx_runtime35.jsx)(PlayerControls, {})
3220
+ /* @__PURE__ */ (0, import_jsx_runtime37.jsx)(PlayerControls, {})
3061
3221
  ] }),
3062
- /* @__PURE__ */ (0, import_jsx_runtime35.jsx)(
3222
+ /* @__PURE__ */ (0, import_jsx_runtime37.jsx)(
3063
3223
  TrackListSidebar,
3064
3224
  {
3065
3225
  layout: "inline",
@@ -3084,33 +3244,33 @@ function PlayerPanel({
3084
3244
  // src/genre-tree/Rating.tsx
3085
3245
  var import_uuid = require("uuid");
3086
3246
  var import_ai = require("react-icons/ai");
3087
- var import_genre_tree22 = require("@behindthemusictree/app-kit/genre-tree");
3088
- var import_jsx_runtime36 = require("react/jsx-runtime");
3247
+ var import_genre_tree23 = require("@behindthemusictree/app-kit/genre-tree");
3248
+ var import_jsx_runtime38 = require("react/jsx-runtime");
3089
3249
  function Rating({ rating, handleChange }) {
3090
3250
  const instanceId = (0, import_uuid.v4)();
3091
- return /* @__PURE__ */ (0, import_jsx_runtime36.jsx)("div", { className: "flex items-center", children: [...Array(6)].map((_, i) => /* @__PURE__ */ (0, import_jsx_runtime36.jsxs)("div", { className: `relative ${i === 0 ? "mr-4" : "mr-1"}`, children: [
3092
- /* @__PURE__ */ (0, import_jsx_runtime36.jsx)(
3251
+ return /* @__PURE__ */ (0, import_jsx_runtime38.jsx)("div", { className: "flex items-center", children: [...Array(6)].map((_, i) => /* @__PURE__ */ (0, import_jsx_runtime38.jsxs)("div", { className: `relative ${i === 0 ? "mr-4" : "mr-1"}`, children: [
3252
+ /* @__PURE__ */ (0, import_jsx_runtime38.jsx)(
3093
3253
  "input",
3094
3254
  {
3095
3255
  id: `star-${instanceId}-${i}`,
3096
3256
  className: "w-6 h-6 opacity-0",
3097
3257
  type: "radio",
3098
3258
  name: "rating",
3099
- value: i === 0 ? import_genre_tree22.FORM_RATING_NULL_VALUE : i,
3259
+ value: i === 0 ? import_genre_tree23.FORM_RATING_NULL_VALUE : i,
3100
3260
  checked: rating === (i === 0 ? null : i),
3101
3261
  onChange: handleChange
3102
3262
  }
3103
3263
  ),
3104
- /* @__PURE__ */ (0, import_jsx_runtime36.jsx)("label", { htmlFor: `star-${instanceId}-${i}`, className: "cursor-pointer", children: i === 0 ? /* @__PURE__ */ (0, import_jsx_runtime36.jsxs)("div", { className: "flex", children: [
3105
- /* @__PURE__ */ (0, import_jsx_runtime36.jsx)(
3264
+ /* @__PURE__ */ (0, import_jsx_runtime38.jsx)("label", { htmlFor: `star-${instanceId}-${i}`, className: "cursor-pointer", children: i === 0 ? /* @__PURE__ */ (0, import_jsx_runtime38.jsxs)("div", { className: "flex", children: [
3265
+ /* @__PURE__ */ (0, import_jsx_runtime38.jsx)(
3106
3266
  import_ai.AiOutlineStar,
3107
3267
  {
3108
- className: `w-7 h-7 absolute top-0 left-0 ${rating === import_genre_tree22.FORM_RATING_NULL_VALUE ? "text-[var(--color-accent-rating)]" : "text-[var(--color-text-muted)]"}`
3268
+ className: `w-7 h-7 absolute top-0 left-0 ${rating === import_genre_tree23.FORM_RATING_NULL_VALUE ? "text-[var(--color-accent-rating)]" : "text-[var(--color-text-muted)]"}`
3109
3269
  }
3110
3270
  ),
3111
- /* @__PURE__ */ (0, import_jsx_runtime36.jsx)("span", { className: "flex-grow w-8" }),
3271
+ /* @__PURE__ */ (0, import_jsx_runtime38.jsx)("span", { className: "flex-grow w-8" }),
3112
3272
  " "
3113
- ] }) : /* @__PURE__ */ (0, import_jsx_runtime36.jsx)(
3273
+ ] }) : /* @__PURE__ */ (0, import_jsx_runtime38.jsx)(
3114
3274
  import_ai.AiFillStar,
3115
3275
  {
3116
3276
  className: `w-7 h-7 absolute top-0 left-0 ${(rating ?? -1) >= i ? "text-[var(--color-accent-rating)]" : "text-[var(--color-text-muted)]"}`
@@ -3120,6 +3280,9 @@ function Rating({ rating, handleChange }) {
3120
3280
  }
3121
3281
  // Annotate the CommonJS export names for ESM import in node:
3122
3282
  0 && (module.exports = {
3283
+ GenreDetailPanel,
3284
+ GenreInfoPanelBody,
3285
+ GenreNodeActionBand,
3123
3286
  GenrePlaylistTreePerRoot,
3124
3287
  GenreSearch,
3125
3288
  GenreTreeOutlineSkeleton,
@@ -3132,6 +3295,8 @@ function Rating({ rating, handleChange }) {
3132
3295
  Rating,
3133
3296
  TrackItem,
3134
3297
  TrackListSidebar,
3135
- TrackPositionPlayPause
3298
+ TrackPositionPlayPause,
3299
+ useGenrePlaylistPlayback,
3300
+ useLeftDetailPanelLayout
3136
3301
  });
3137
3302
  //# sourceMappingURL=index.cjs.map