@behindthemusictree/app-kit-ui 28.0.2 → 29.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -5,7 +5,7 @@ import {
5
5
  } from "./chunk-DKQQLLMY.js";
6
6
 
7
7
  // src/genre-tree/GenreTreeView.tsx
8
- import { useCallback as useCallback6, useDeferredValue, useEffect as useEffect5, useMemo as useMemo7, useRef as useRef4, useState as useState5 } from "react";
8
+ import { useCallback as useCallback6, useDeferredValue, useEffect as useEffect6, useMemo as useMemo7, useRef as useRef4, useState as useState5 } from "react";
9
9
  import { useIsMutating } from "@tanstack/react-query";
10
10
  import { Plus as Plus2 } from "lucide-react";
11
11
  import { IconTextButton, Button, SegmentedControl as SegmentedControl2 } from "@behindthemusictree/ui";
@@ -17,429 +17,108 @@ import {
17
17
  useArtistSearch
18
18
  } from "@behindthemusictree/app-kit/genre-tree";
19
19
  import { useGenreTreeViewState, useArtist as useArtist2, useUpdateGenre as useUpdateGenre2, genreUpdateMutationKey } from "@behindthemusictree/app-kit/genre-tree";
20
- import { usePlayer as usePlayer8 } from "@behindthemusictree/app-kit/player";
21
20
  import { usePopup as usePopup5 } from "@behindthemusictree/app-kit/popup";
22
21
 
23
- // src/genre-tree/GenreDetailExtras.tsx
24
- import { Skeleton as Skeleton2 } from "@behindthemusictree/ui";
25
- import { useFetchGenreOverview } from "@behindthemusictree/app-kit/genre-tree";
26
-
27
- // src/genre-tree/ClampedText.tsx
28
- import { useLayoutEffect, useRef, useState } from "react";
29
- import { Fragment, jsx, jsxs } from "react/jsx-runtime";
30
- function ClampedText({ children, className: className2 = "" }) {
31
- const ref = useRef(null);
32
- const [expanded, setExpanded] = useState(false);
33
- const [overflows, setOverflows] = useState(false);
34
- useLayoutEffect(() => {
35
- const element = ref.current;
36
- if (!element || expanded) return;
37
- setOverflows(element.scrollHeight > element.clientHeight);
38
- }, [children, expanded]);
39
- return /* @__PURE__ */ jsxs(Fragment, { children: [
40
- /* @__PURE__ */ jsx("p", { ref, className: `${className2} ${expanded ? "" : "line-clamp-3"}`, children }),
41
- (overflows || expanded) && /* @__PURE__ */ jsx(
42
- "button",
43
- {
44
- type: "button",
45
- "aria-expanded": expanded,
46
- onClick: () => setExpanded((value) => !value),
47
- className: "mt-1 text-xs text-[var(--color-text-muted)] underline hover:text-[var(--color-text)]",
48
- children: expanded ? "Show less" : "Show more"
49
- }
50
- )
51
- ] });
52
- }
53
-
54
- // src/genre-tree/GenrePlaylistTracks.tsx
55
- import { useEffect, useRef as useRef2 } from "react";
56
- import { useInfiniteQuery } from "@tanstack/react-query";
57
- import { z } from "zod";
58
- import { FaPlay, FaPause } from "react-icons/fa";
59
- import { RingLoader, Skeleton } from "@behindthemusictree/ui";
60
- import { useSession } from "@behindthemusictree/app-kit/auth";
61
- import { useFetchWrapper } from "@behindthemusictree/app-kit/transport";
62
- import { PaginatedResponseSchema } from "@behindthemusictree/app-kit/transport";
63
- import { parseWithLog } from "@behindthemusictree/app-kit/transport";
22
+ // src/genre-tree/playlist-tree/TreePerRoot.tsx
23
+ import { useCallback as useCallback2, useMemo as useMemo2 } from "react";
24
+ import {
25
+ GenreTree,
26
+ getGenreTreeColor
27
+ } from "@behindthemusictree/genre-tree-view";
64
28
  import { usePlayer } from "@behindthemusictree/app-kit/player";
65
- import { PlayStates } from "@behindthemusictree/app-kit/player";
66
29
  import { usePopup } from "@behindthemusictree/app-kit/popup";
67
30
  import { ErrorCode } from "@behindthemusictree/app-kit/transport";
68
- import { genrePlaylistQueryKeys } from "@behindthemusictree/app-kit/genre-tree";
69
- import { libraryEndpoints, libraryQueryKeys } from "@behindthemusictree/app-kit/genre-tree";
70
- import { isSong, songUnplayableReason, SongDetailedSchema } from "@behindthemusictree/app-kit/genre-tree";
71
- import { useTrackList } from "@behindthemusictree/app-kit/genre-tree";
72
31
 
73
- // src/genre-tree/MusicBrainzRecordingLink.tsx
74
- import { musicBrainzRecordingUrl } from "@behindthemusictree/app-kit/genre-tree";
75
- import { Fragment as Fragment2, jsx as jsx2 } from "react/jsx-runtime";
76
- function MusicBrainzRecordingLink({
77
- song,
78
- children,
79
- className: className2 = ""
32
+ // src/genre-tree/playlist-tree/useGenrePlaylistPlayback.ts
33
+ import { useCallback, useMemo } from "react";
34
+ import {
35
+ TrackListOriginType,
36
+ useTrackList
37
+ } from "@behindthemusictree/app-kit/genre-tree";
38
+ function useGenrePlaylistPlayback({
39
+ scope,
40
+ genrePlaylists,
41
+ artist = null
80
42
  }) {
81
- const url = musicBrainzRecordingUrl(song);
82
- if (!url) return /* @__PURE__ */ jsx2(Fragment2, { children });
83
- return /* @__PURE__ */ jsx2(
84
- "a",
85
- {
86
- href: url,
87
- target: "_blank",
88
- rel: "noopener noreferrer",
89
- "aria-label": `View ${song.title} on MusicBrainz`,
90
- className: className2,
91
- onClick: (event) => event.stopPropagation(),
92
- children
93
- }
43
+ const { trackList, playNewTrackListFromGenrePlaylist, playNewTrackListFromArtist } = useTrackList();
44
+ const origin = trackList?.origin;
45
+ const playingNodeId = useMemo(() => {
46
+ if (!origin) return null;
47
+ if (!artist) return origin.type === TrackListOriginType.GENRE_PLAYLIST ? origin.uuid : null;
48
+ if (origin.type !== TrackListOriginType.ARTIST || origin.uuid !== artist.uuid) return null;
49
+ const { genreUuid } = origin;
50
+ return genreUuid ? genrePlaylists.find((gp) => gp.criteria?.uuid === genreUuid)?.uuid ?? null : null;
51
+ }, [origin, artist, genrePlaylists]);
52
+ const play = useCallback(
53
+ async (genrePlaylist) => {
54
+ if (!artist) return playNewTrackListFromGenrePlaylist(genrePlaylist, scope);
55
+ if (!genrePlaylist.criteria) return;
56
+ return playNewTrackListFromArtist(artist, scope, { ordering: "title", genreUuid: genrePlaylist.criteria.uuid });
57
+ },
58
+ [artist, scope, playNewTrackListFromGenrePlaylist, playNewTrackListFromArtist]
94
59
  );
60
+ return useMemo(() => ({ playingNodeId, play }), [playingNodeId, play]);
95
61
  }
96
62
 
97
- // src/genre-tree/GenrePlaylistTracks.tsx
98
- import { ArtistNames } from "@behindthemusictree/app-kit/genre-tree";
63
+ // src/genre-tree/playlist-tree/TreePerRoot.tsx
99
64
  import { capitalize, useTrackNoun } from "@behindthemusictree/app-kit/track-noun";
100
- import { jsx as jsx3, jsxs as jsxs2 } from "react/jsx-runtime";
101
- var CATALOGUE_PAGE_SIZE = 100;
102
- var SongPageSchema = PaginatedResponseSchema(SongDetailedSchema).extend({
103
- overallTotal: z.number().nullable(),
104
- page: z.number().nullable(),
105
- totalPages: z.number().nullable()
106
- });
107
- var TEXT_CELL = "min-w-0 flex-1 truncate [@container(min-width:40rem)]:flex [@container(min-width:40rem)]:gap-2";
108
- var TITLE_CELL = "[@container(min-width:40rem)]:flex-[2] [@container(min-width:40rem)]:truncate";
109
- var COLUMN_CELL = "[@container(min-width:40rem)]:flex-1 [@container(min-width:40rem)]:truncate";
110
- var ALBUM_CELL = `hidden [@container(min-width:40rem)]:block ${COLUMN_CELL}`;
111
- var SEPARATOR = "[@container(min-width:40rem)]:hidden";
112
- var nextCursor = (next) => {
113
- const cursor = new URL(next).searchParams.get("cursor");
114
- if (!cursor) throw new Error(`Songs next link has no cursor: ${next}`);
115
- return cursor;
116
- };
117
- function GenrePlaylistTracks({
118
- genreUuid,
119
- genrePlaylist,
65
+ import { jsx } from "react/jsx-runtime";
66
+ function GenrePlaylistTreePerRoot({
120
67
  scope,
121
- getBackendBaseUrl,
122
- renderArtistLink,
123
- renderSongLeading,
124
- renderSongActions,
125
- renderTracksToolbar,
126
- artist = null
68
+ className: className2,
69
+ rootUuid,
70
+ genrePlaylistTreePerRoot,
71
+ handleGenreCreationAction,
72
+ handleGenreRenameAction,
73
+ additionalActions,
74
+ onNodeClick,
75
+ onNodeHover,
76
+ renderInfoPanelBody,
77
+ selectedNodeId,
78
+ onSelectedNodeChange,
79
+ hideInfoPanelClose,
80
+ hideInfoPanelTitle,
81
+ readOnly = false,
82
+ showToolbar,
83
+ wheelZoom,
84
+ defaultViewport,
85
+ viewportRestore,
86
+ onViewportChange,
87
+ playback,
88
+ ...infoPanelCuration
127
89
  }) {
128
- const { fetchGenrePlaylistTracksPage, playNewTrackListFromGenrePlaylist, playNewTrackListFromArtist } = useTrackList();
129
- const { session, sessionRestored } = useSession();
130
- const { playerTrackObject, playState, handlePlayPauseAction } = usePlayer();
131
- const { showPopup } = usePopup();
90
+ const { isPlaying, setIsPlaying } = usePlayer();
91
+ const defaultPlayback = useGenrePlaylistPlayback({ scope, genrePlaylists: genrePlaylistTreePerRoot });
92
+ const { playingNodeId, play } = playback ?? defaultPlayback;
132
93
  const trackNoun = useTrackNoun();
133
- const { uuid } = genrePlaylist;
134
- const { fetch } = useFetchWrapper(getBackendBaseUrl);
135
- const { data, isPending, isError, refetch, hasNextPage, isFetchingNextPage, fetchNextPage } = useInfiniteQuery({
136
- queryKey: scope === "me" ? genrePlaylistQueryKeys.me.tracks(uuid) : artist ? [...libraryQueryKeys.reference.songs.byGenre(getBackendBaseUrl(), genreUuid), "artist", artist.uuid] : libraryQueryKeys.reference.songs.byGenre(getBackendBaseUrl(), genreUuid),
137
- queryFn: async ({ pageParam }) => {
138
- if (scope === "me") return fetchGenrePlaylistTracksPage(uuid, scope, Number(pageParam));
139
- const response = await fetch(
140
- libraryEndpoints.reference.songs.list(),
141
- true,
142
- false,
143
- {},
144
- { genre: genreUuid, ...artist && { artist: artist.uuid }, pageSize: CATALOGUE_PAGE_SIZE, withTotal: false, ...pageParam && { cursor: pageParam } }
145
- );
146
- const parsed = parseWithLog(SongPageSchema, response, "GenrePlaylistTracks");
147
- return { tracks: parsed.results, total: parsed.overallTotal, nextPage: parsed.next ? nextCursor(parsed.next) : null };
148
- },
149
- initialPageParam: scope === "me" ? 1 : "",
150
- getNextPageParam: (lastPage) => lastPage.nextPage ?? void 0,
151
- enabled: scope !== "me" || sessionRestored && !!session?.accessToken
152
- });
153
- const sentinelRef = useRef2(null);
154
- useEffect(() => {
155
- const sentinel = sentinelRef.current;
156
- if (!hasNextPage || isFetchingNextPage || !sentinel) return;
157
- const observer = new IntersectionObserver((entries) => {
158
- if (entries.some((entry) => entry.isIntersecting)) {
159
- void fetchNextPage();
160
- }
161
- });
162
- observer.observe(sentinel);
163
- return () => observer.disconnect();
164
- }, [hasNextPage, isFetchingNextPage, fetchNextPage]);
165
- const pages = data?.pages ?? [];
166
- const tracks = pages.flatMap((page) => page.tracks);
167
- const total = pages[0]?.total ?? void 0;
168
- const play = (startSongUuid) => {
169
- const started = artist ? playNewTrackListFromArtist(artist, scope, { startSongUuid, ordering: "title", genreUuid }) : playNewTrackListFromGenrePlaylist(genrePlaylist, scope, { startSongUuid });
170
- started.catch((error) => {
171
- console.error("Failed to play genre playlist:", error);
172
- showPopup(/* @__PURE__ */ jsx3(InternalErrorPopup, { errorCode: ErrorCode.CLIENT_UNKNOWN }));
173
- });
174
- };
175
- return /* @__PURE__ */ jsxs2("div", { className: "gtv-info-panel-children", children: [
176
- /* @__PURE__ */ jsxs2("span", { className: "gtv-info-panel-children-title", children: [
177
- capitalize(trackNoun.plural),
178
- total !== void 0 && ` (${total})`
179
- ] }),
180
- renderTracksToolbar?.(),
181
- isPending ? /* @__PURE__ */ jsx3(Skeleton, { className: "h-4 w-3/4", "aria-busy": "true" }) : isError && tracks.length === 0 ? /* @__PURE__ */ jsxs2("p", { role: "alert", children: [
182
- "Couldn't load ",
183
- trackNoun.plural,
184
- ".",
185
- " ",
186
- /* @__PURE__ */ jsx3("button", { type: "button", className: "underline", onClick: () => void refetch(), children: "Retry" })
187
- ] }) : tracks.length === 0 ? /* @__PURE__ */ jsx3("p", { children: "\u2014" }) : /* @__PURE__ */ jsxs2("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: [
188
- tracks.map((track, index) => {
189
- const label = track.artists?.length ? `${track.title} \u2014 ${track.artists.map((artist2) => artist2.name).join(", ")}` : track.title;
190
- const isCurrent = playerTrackObject?.track.id === track.uuid;
191
- const isPlaying = isCurrent && playState === PlayStates.PLAYING;
192
- const text = /* @__PURE__ */ jsxs2("span", { className: TEXT_CELL, children: [
193
- /* @__PURE__ */ jsx3("span", { className: TITLE_CELL, children: isSong(track) ? /* @__PURE__ */ jsx3(MusicBrainzRecordingLink, { song: track, className: "hover:underline focus-visible:underline", children: track.title }) : track.title }),
194
- track.artists?.length ? /* @__PURE__ */ jsx3("span", { className: SEPARATOR, children: " \u2014 " }) : null,
195
- /* @__PURE__ */ jsx3("span", { className: COLUMN_CELL, children: track.artists?.length ? /* @__PURE__ */ jsx3(ArtistNames, { artists: track.artists, renderArtistLink }) : null }),
196
- track.album && /* @__PURE__ */ jsx3("span", { className: ALBUM_CELL, children: track.album.name })
197
- ] });
198
- const leading = renderSongLeading?.(track, index);
199
- const actions = renderSongActions?.(track);
200
- const isUnplayable = isSong(track) && songUnplayableReason(track) !== null;
201
- if (isUnplayable) {
202
- return /* @__PURE__ */ jsxs2("li", { className: "flex min-h-8 items-center gap-1 px-1 opacity-50", children: [
203
- leading,
204
- /* @__PURE__ */ jsx3(
205
- "button",
206
- {
207
- type: "button",
208
- disabled: true,
209
- "aria-disabled": "true",
210
- "aria-label": label,
211
- className: "flex h-6 w-6 shrink-0 items-center justify-center rounded-full opacity-0 cursor-not-allowed",
212
- children: /* @__PURE__ */ jsx3(FaPlay, { size: 10 })
213
- }
214
- ),
215
- text,
216
- actions
217
- ] }, track.uuid);
218
- }
219
- return /* @__PURE__ */ jsxs2(
220
- "li",
221
- {
222
- 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)]" : ""}`,
223
- children: [
224
- leading,
225
- /* @__PURE__ */ jsx3(
226
- "button",
227
- {
228
- type: "button",
229
- "aria-label": `${isPlaying ? "Pause" : "Play"} ${label}`,
230
- 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 !== PlayStates.STOPPED ? "opacity-100" : "opacity-0 [@media(pointer:coarse)]:opacity-100"}`,
231
- onClick: () => isCurrent ? handlePlayPauseAction() : play(track.uuid),
232
- children: isCurrent && playState === PlayStates.LOADING ? /* @__PURE__ */ jsx3(RingLoader, { size: 10 }) : isPlaying ? /* @__PURE__ */ jsx3(FaPause, { size: 10 }) : /* @__PURE__ */ jsx3(FaPlay, { size: 10 })
233
- }
234
- ),
235
- text,
236
- actions
237
- ]
238
- },
239
- track.uuid
240
- );
241
- }),
242
- hasNextPage && /* @__PURE__ */ jsx3("li", { ref: sentinelRef, "aria-hidden": "true", className: "h-4" })
243
- ] })
244
- ] });
245
- }
246
-
247
- // src/genre-tree/GenreDetailExtras.tsx
248
- import { useTrackNoun as useTrackNoun2 } from "@behindthemusictree/app-kit/track-noun";
249
- import { SiWikidata, SiWikipedia } from "react-icons/si";
250
- import { Fragment as Fragment3, jsx as jsx4, jsxs as jsxs3 } from "react/jsx-runtime";
251
- function GenreDetailExtras({
252
- genreUuid,
253
- genrePlaylist,
254
- wikidataId,
255
- scope,
256
- getBackendBaseUrl,
257
- criteriaOverviewSchema,
258
- renderGenreDetailExtras,
259
- renderArtistLink,
260
- renderSummary,
261
- renderEssentialTracks,
262
- family,
263
- ...trackSlots
264
- }) {
265
- const trackNoun = useTrackNoun2();
266
- const { data: overview, isPending } = useFetchGenreOverview(
267
- genreUuid,
268
- scope,
269
- getBackendBaseUrl,
270
- criteriaOverviewSchema
271
- );
272
- if (isPending) {
273
- return /* @__PURE__ */ jsxs3(Fragment3, { children: [
274
- /* @__PURE__ */ jsxs3("div", { className: "gtv-info-panel-children", "aria-busy": "true", "data-testid": "genre-detail-extras-skeleton", children: [
275
- /* @__PURE__ */ jsx4("span", { className: "gtv-info-panel-children-title", children: "Summary" }),
276
- /* @__PURE__ */ jsx4(Skeleton2, { className: "h-4 w-3/4" })
277
- ] }),
278
- family
279
- ] });
280
- }
281
- if (!overview) return /* @__PURE__ */ jsx4(Fragment3, { children: family });
282
- const { summary, summarySourceUrl, essentialSongs } = overview;
283
- const wikidataUrl = wikidataId ? `https://www.wikidata.org/wiki/${encodeURIComponent(wikidataId)}` : null;
284
- return /* @__PURE__ */ jsxs3(Fragment3, { children: [
285
- (summarySourceUrl || wikidataUrl) && /* @__PURE__ */ jsxs3("div", { className: "gtv-info-panel-children text-[0.8125rem] leading-[1.45] text-[var(--color-text)]", children: [
286
- /* @__PURE__ */ jsx4("span", { className: "gtv-info-panel-children-title", children: "Links" }),
287
- /* @__PURE__ */ jsxs3("div", { className: "mt-1 flex flex-wrap gap-x-3 gap-y-1 text-xs", children: [
288
- summarySourceUrl && /* @__PURE__ */ jsxs3(
289
- "a",
290
- {
291
- href: summarySourceUrl,
292
- target: "_blank",
293
- rel: "noopener noreferrer",
294
- className: "inline-flex items-center gap-1.5 text-[var(--color-text-muted)] hover:text-[var(--color-text)]",
295
- children: [
296
- /* @__PURE__ */ jsx4(SiWikipedia, { "aria-hidden": "true", className: "h-3.5 w-3.5" }),
297
- "Wikipedia"
298
- ]
299
- }
300
- ),
301
- wikidataUrl && /* @__PURE__ */ jsxs3(
302
- "a",
303
- {
304
- href: wikidataUrl,
305
- target: "_blank",
306
- rel: "noopener noreferrer",
307
- className: "inline-flex items-center gap-1.5 text-[var(--color-text-muted)] hover:text-[var(--color-text)]",
308
- children: [
309
- /* @__PURE__ */ jsx4(SiWikidata, { "aria-hidden": "true", className: "h-3.5 w-3.5" }),
310
- "Wikidata"
311
- ]
312
- }
313
- )
314
- ] })
315
- ] }),
316
- renderSummary ? renderSummary(overview) : /* @__PURE__ */ jsxs3("div", { className: "gtv-info-panel-children text-[0.8125rem] leading-[1.45] text-[var(--color-text)]", children: [
317
- /* @__PURE__ */ jsx4("span", { className: "gtv-info-panel-children-title", children: "Summary" }),
318
- /* @__PURE__ */ jsx4(ClampedText, { className: "mt-1", children: summary ?? "\u2014" }, genreUuid)
319
- ] }),
320
- family,
321
- renderGenreDetailExtras?.(overview),
322
- renderEssentialTracks ? renderEssentialTracks(overview) : essentialSongs && /* @__PURE__ */ jsxs3("div", { className: "gtv-info-panel-children text-[0.8125rem] leading-[1.45] text-[var(--color-text)]", children: [
323
- /* @__PURE__ */ jsxs3("span", { className: "gtv-info-panel-children-title", children: [
324
- "Essential ",
325
- trackNoun.plural
326
- ] }),
327
- essentialSongs.length > 0 ? /* @__PURE__ */ jsx4("ul", { className: "mt-1 divide-y divide-[var(--color-border)]", children: essentialSongs.map((track) => /* @__PURE__ */ jsx4("li", { className: "py-1", children: track.title }, track.uuid)) }) : /* @__PURE__ */ jsx4("p", { className: "mt-1", children: "\u2014" })
328
- ] }),
329
- /* @__PURE__ */ jsx4(
330
- GenrePlaylistTracks,
331
- {
332
- genreUuid,
333
- genrePlaylist,
334
- scope,
335
- getBackendBaseUrl,
336
- renderArtistLink,
337
- ...trackSlots
338
- }
339
- )
340
- ] });
341
- }
342
-
343
- // src/genre-tree/playlist-tree/TreePerRoot.tsx
344
- import { useCallback as useCallback2, useMemo as useMemo2 } from "react";
345
- import {
346
- GenreTree,
347
- getGenreTreeColor
348
- } from "@behindthemusictree/genre-tree-view";
349
- import { usePlayer as usePlayer2 } from "@behindthemusictree/app-kit/player";
350
- import { usePopup as usePopup2 } from "@behindthemusictree/app-kit/popup";
351
- import { ErrorCode as ErrorCode2 } from "@behindthemusictree/app-kit/transport";
352
-
353
- // src/genre-tree/playlist-tree/useGenrePlaylistPlayback.ts
354
- import { useCallback, useMemo } from "react";
355
- import {
356
- TrackListOriginType,
357
- useTrackList as useTrackList2
358
- } from "@behindthemusictree/app-kit/genre-tree";
359
- function useGenrePlaylistPlayback({
360
- scope,
361
- genrePlaylists,
362
- artist = null
363
- }) {
364
- const { trackList, playNewTrackListFromGenrePlaylist, playNewTrackListFromArtist } = useTrackList2();
365
- const origin = trackList?.origin;
366
- const playingNodeId = useMemo(() => {
367
- if (!origin) return null;
368
- if (!artist) return origin.type === TrackListOriginType.GENRE_PLAYLIST ? origin.uuid : null;
369
- if (origin.type !== TrackListOriginType.ARTIST || origin.uuid !== artist.uuid) return null;
370
- const { genreUuid } = origin;
371
- return genreUuid ? genrePlaylists.find((gp) => gp.criteria?.uuid === genreUuid)?.uuid ?? null : null;
372
- }, [origin, artist, genrePlaylists]);
373
- const play = useCallback(
374
- async (genrePlaylist) => {
375
- if (!artist) return playNewTrackListFromGenrePlaylist(genrePlaylist, scope);
376
- if (!genrePlaylist.criteria) return;
377
- return playNewTrackListFromArtist(artist, scope, { ordering: "title", genreUuid: genrePlaylist.criteria.uuid });
378
- },
379
- [artist, scope, playNewTrackListFromGenrePlaylist, playNewTrackListFromArtist]
380
- );
381
- return useMemo(() => ({ playingNodeId, play }), [playingNodeId, play]);
382
- }
383
-
384
- // src/genre-tree/playlist-tree/TreePerRoot.tsx
385
- import { capitalize as capitalize2, useTrackNoun as useTrackNoun3 } from "@behindthemusictree/app-kit/track-noun";
386
- import { jsx as jsx5 } from "react/jsx-runtime";
387
- function GenrePlaylistTreePerRoot({
388
- scope,
389
- className: className2,
390
- rootUuid,
391
- genrePlaylistTreePerRoot,
392
- handleGenreCreationAction,
393
- handleGenreRenameAction,
394
- additionalActions,
395
- onNodeClick,
396
- onNodeHover,
397
- renderInfoPanelBody,
398
- selectedNodeId,
399
- onSelectedNodeChange,
400
- hideInfoPanelClose,
401
- hideInfoPanelTitle,
402
- readOnly = false,
403
- showToolbar,
404
- wheelZoom,
405
- controlsOrientation,
406
- defaultViewport,
407
- onViewportChange,
408
- playback,
409
- ...infoPanelCuration
410
- }) {
411
- const { isPlaying, setIsPlaying } = usePlayer2();
412
- const defaultPlayback = useGenrePlaylistPlayback({ scope, genrePlaylists: genrePlaylistTreePerRoot });
413
- const { playingNodeId, play } = playback ?? defaultPlayback;
414
- const trackNoun = useTrackNoun3();
415
- const { showPopup } = usePopup2();
416
- const nodes = useMemo2(
417
- () => genrePlaylistTreePerRoot.map((genrePlaylist) => ({
418
- id: genrePlaylist.uuid,
419
- parentId: genrePlaylist.parent?.uuid ?? null,
420
- name: genrePlaylist.name,
421
- itemCount: genrePlaylist.tracksCount,
422
- actionable: Boolean(genrePlaylist.criteria),
423
- side: genrePlaylist.criteria?.side ?? void 0,
424
- wikidataId: genrePlaylist.criteria?.wikidataId ?? null,
425
- umbrella: genrePlaylist.isUmbrella
426
- })),
427
- [genrePlaylistTreePerRoot]
428
- );
429
- const handlePlayPause = useCallback2(
430
- (nodeId) => {
431
- const genrePlaylist = genrePlaylistTreePerRoot.find((g) => g.uuid === nodeId);
432
- if (!genrePlaylist) return;
433
- if (nodeId === playingNodeId) {
434
- setIsPlaying(!isPlaying);
435
- return;
94
+ const { showPopup } = usePopup();
95
+ const nodes = useMemo2(
96
+ () => genrePlaylistTreePerRoot.map((genrePlaylist) => ({
97
+ id: genrePlaylist.uuid,
98
+ parentId: genrePlaylist.parent?.uuid ?? null,
99
+ name: genrePlaylist.name,
100
+ itemCount: genrePlaylist.tracksCount,
101
+ actionable: Boolean(genrePlaylist.criteria),
102
+ side: genrePlaylist.criteria?.side ?? void 0,
103
+ wikidataId: genrePlaylist.criteria?.wikidataId ?? null,
104
+ umbrella: genrePlaylist.isUmbrella
105
+ })),
106
+ [genrePlaylistTreePerRoot]
107
+ );
108
+ const handlePlayPause = useCallback2(
109
+ (nodeId) => {
110
+ const genrePlaylist = genrePlaylistTreePerRoot.find((g) => g.uuid === nodeId);
111
+ if (!genrePlaylist) return;
112
+ if (nodeId === playingNodeId) {
113
+ setIsPlaying(!isPlaying);
114
+ return;
436
115
  }
437
116
  if (genrePlaylist.tracksCount === 0) {
438
117
  return;
439
118
  }
440
119
  play(genrePlaylist).catch((error) => {
441
120
  console.error("Failed to load genre playlist tracks:", error);
442
- showPopup(/* @__PURE__ */ jsx5(InternalErrorPopup, { errorCode: ErrorCode2.CLIENT_UNKNOWN }));
121
+ showPopup(/* @__PURE__ */ jsx(InternalErrorPopup, { errorCode: ErrorCode.CLIENT_UNKNOWN }));
443
122
  });
444
123
  },
445
124
  [
@@ -471,12 +150,12 @@ function GenrePlaylistTreePerRoot({
471
150
  if (confirm(`Are you sure you want to delete "${node.name}"?`)) {
472
151
  }
473
152
  }, []);
474
- return /* @__PURE__ */ jsx5(
153
+ return /* @__PURE__ */ jsx(
475
154
  GenreTree,
476
155
  {
477
156
  className: className2,
478
157
  nodes,
479
- itemCountLabel: `${capitalize2(trackNoun.singular)} count`,
158
+ itemCountLabel: `${capitalize(trackNoun.singular)} count`,
480
159
  rootColor: getGenreTreeColor(rootUuid),
481
160
  playingNodeId,
482
161
  playState: isPlaying ? "playing" : "paused",
@@ -494,8 +173,8 @@ function GenrePlaylistTreePerRoot({
494
173
  hideInfoPanelTitle,
495
174
  showToolbar,
496
175
  wheelZoom,
497
- controlsOrientation,
498
176
  defaultViewport,
177
+ viewportRestore,
499
178
  onViewportChange,
500
179
  ...infoPanelCuration,
501
180
  readOnly
@@ -508,9 +187,9 @@ import { useCallback as useCallback3, useMemo as useMemo3 } from "react";
508
187
  import {
509
188
  GenreTreeWheel
510
189
  } from "@behindthemusictree/genre-tree-view";
511
- import { usePlayer as usePlayer3 } from "@behindthemusictree/app-kit/player";
512
- import { usePopup as usePopup3 } from "@behindthemusictree/app-kit/popup";
513
- import { ErrorCode as ErrorCode3 } from "@behindthemusictree/app-kit/transport";
190
+ import { usePlayer as usePlayer2 } from "@behindthemusictree/app-kit/player";
191
+ import { usePopup as usePopup2 } from "@behindthemusictree/app-kit/popup";
192
+ import { ErrorCode as ErrorCode2 } from "@behindthemusictree/app-kit/transport";
514
193
 
515
194
  // src/genre-tree/playlist-tree/size-metric.ts
516
195
  var hasViewsCount = (genrePlaylists) => genrePlaylists.every((genrePlaylist) => genrePlaylist.viewsCount !== void 0);
@@ -523,8 +202,8 @@ var genrePlaylistSize = (genrePlaylist, metric) => {
523
202
  };
524
203
 
525
204
  // src/genre-tree/playlist-tree/TreeWheel.tsx
526
- import { capitalize as capitalize3, useTrackNoun as useTrackNoun4 } from "@behindthemusictree/app-kit/track-noun";
527
- import { jsx as jsx6 } from "react/jsx-runtime";
205
+ import { capitalize as capitalize2, useTrackNoun as useTrackNoun2 } from "@behindthemusictree/app-kit/track-noun";
206
+ import { jsx as jsx2 } from "react/jsx-runtime";
528
207
  function GenrePlaylistTreeWheel({
529
208
  scope,
530
209
  className: className2,
@@ -542,20 +221,20 @@ function GenrePlaylistTreeWheel({
542
221
  readOnly = false,
543
222
  allowWheelRotation,
544
223
  showToolbar,
545
- controlsOrientation,
546
224
  sizeMetric = "songs",
547
225
  floatingControlsLeading,
548
226
  busy,
549
227
  defaultViewport,
228
+ viewportRestore,
550
229
  onViewportChange,
551
230
  playback,
552
231
  ...infoPanelCuration
553
232
  }) {
554
- const { isPlaying, setIsPlaying } = usePlayer3();
233
+ const { isPlaying, setIsPlaying } = usePlayer2();
555
234
  const defaultPlayback = useGenrePlaylistPlayback({ scope, genrePlaylists });
556
235
  const { playingNodeId, play } = playback ?? defaultPlayback;
557
- const trackNoun = useTrackNoun4();
558
- const { showPopup } = usePopup3();
236
+ const trackNoun = useTrackNoun2();
237
+ const { showPopup } = usePopup2();
559
238
  const nodes = useMemo3(
560
239
  () => genrePlaylists.map((genrePlaylist) => ({
561
240
  id: genrePlaylist.uuid,
@@ -581,7 +260,7 @@ function GenrePlaylistTreeWheel({
581
260
  }
582
261
  play(genrePlaylist).catch((error) => {
583
262
  console.error("Failed to load genre playlist tracks:", error);
584
- showPopup(/* @__PURE__ */ jsx6(InternalErrorPopup, { errorCode: ErrorCode3.CLIENT_UNKNOWN }));
263
+ showPopup(/* @__PURE__ */ jsx2(InternalErrorPopup, { errorCode: ErrorCode2.CLIENT_UNKNOWN }));
585
264
  });
586
265
  },
587
266
  [
@@ -613,12 +292,12 @@ function GenrePlaylistTreeWheel({
613
292
  if (confirm(`Are you sure you want to delete "${node.name}"?`)) {
614
293
  }
615
294
  }, []);
616
- return /* @__PURE__ */ jsx6(
295
+ return /* @__PURE__ */ jsx2(
617
296
  GenreTreeWheel,
618
297
  {
619
298
  className: className2,
620
299
  nodes,
621
- itemCountLabel: sizeMetric === "views" ? "Views" : `${capitalize3(trackNoun.singular)} count`,
300
+ itemCountLabel: sizeMetric === "views" ? "Views" : `${capitalize2(trackNoun.singular)} count`,
622
301
  playingNodeId,
623
302
  playState: isPlaying ? "playing" : "paused",
624
303
  onPlayPause: handlePlayPause,
@@ -635,10 +314,10 @@ function GenrePlaylistTreeWheel({
635
314
  hideInfoPanelTitle,
636
315
  allowWheelRotation,
637
316
  showToolbar,
638
- controlsOrientation,
639
317
  floatingControlsLeading,
640
318
  busy,
641
319
  defaultViewport,
320
+ viewportRestore,
642
321
  onViewportChange,
643
322
  ...infoPanelCuration,
644
323
  readOnly
@@ -652,9 +331,9 @@ import {
652
331
  GenreTreeOutline,
653
332
  GenreTreeWheelRadialPopCore
654
333
  } from "@behindthemusictree/genre-tree-view";
655
- import { usePlayer as usePlayer4 } from "@behindthemusictree/app-kit/player";
656
- import { capitalize as capitalize4, useTrackNoun as useTrackNoun5 } from "@behindthemusictree/app-kit/track-noun";
657
- import { jsx as jsx7 } from "react/jsx-runtime";
334
+ import { usePlayer as usePlayer3 } from "@behindthemusictree/app-kit/player";
335
+ import { capitalize as capitalize3, useTrackNoun as useTrackNoun3 } from "@behindthemusictree/app-kit/track-noun";
336
+ import { jsx as jsx3 } from "react/jsx-runtime";
658
337
  function GenrePlaylistTreeWheelRadialPopCore(props) {
659
338
  const {
660
339
  scope,
@@ -673,12 +352,12 @@ function GenrePlaylistTreeWheelRadialPopCore(props) {
673
352
  readOnly = false,
674
353
  allowWheelRotation,
675
354
  showToolbar,
676
- controlsOrientation,
677
355
  sizeMetric = "songs",
678
356
  floatingControlsLeading,
679
357
  busy,
680
358
  outline = false,
681
359
  defaultViewport,
360
+ viewportRestore,
682
361
  onViewportChange,
683
362
  playback
684
363
  } = props;
@@ -691,10 +370,10 @@ function GenrePlaylistTreeWheelRadialPopCore(props) {
691
370
  hideExpandButton: props.hideExpandButton,
692
371
  overlayPanel: props.overlayPanel
693
372
  };
694
- const { isPlaying, setIsPlaying } = usePlayer4();
373
+ const { isPlaying, setIsPlaying } = usePlayer3();
695
374
  const defaultPlayback = useGenrePlaylistPlayback({ scope, genrePlaylists });
696
375
  const { playingNodeId, play } = playback ?? defaultPlayback;
697
- const trackNoun = useTrackNoun5();
376
+ const trackNoun = useTrackNoun3();
698
377
  const nodes = useMemo4(
699
378
  () => genrePlaylists.map((genrePlaylist) => ({
700
379
  id: genrePlaylist.uuid,
@@ -750,7 +429,7 @@ function GenrePlaylistTreeWheelRadialPopCore(props) {
750
429
  const treeProps = {
751
430
  className: className2,
752
431
  nodes,
753
- itemCountLabel: sizeMetric === "views" ? "Views" : `${capitalize4(trackNoun.singular)} count`,
432
+ itemCountLabel: sizeMetric === "views" ? "Views" : `${capitalize3(trackNoun.singular)} count`,
754
433
  playingNodeId,
755
434
  playState: isPlaying ? "playing" : "paused",
756
435
  onPlayPause: handlePlayPause,
@@ -769,15 +448,15 @@ function GenrePlaylistTreeWheelRadialPopCore(props) {
769
448
  ...infoPanelCuration,
770
449
  readOnly
771
450
  };
772
- return outline ? /* @__PURE__ */ jsx7(GenreTreeOutline, { ...treeProps }) : /* @__PURE__ */ jsx7(
451
+ return outline ? /* @__PURE__ */ jsx3(GenreTreeOutline, { ...treeProps }) : /* @__PURE__ */ jsx3(
773
452
  GenreTreeWheelRadialPopCore,
774
453
  {
775
454
  ...treeProps,
776
455
  allowWheelRotation,
777
- controlsOrientation,
778
456
  floatingControlsLeading,
779
457
  busy,
780
458
  defaultViewport,
459
+ viewportRestore,
781
460
  onViewportChange,
782
461
  ...focus.onFocusGenreChange ? { focusNodeId: focus.focusGenreUuid, onFocusChange: focus.onFocusGenreChange } : { focusNodeId: void 0, onFocusChange: void 0 }
783
462
  }
@@ -785,13 +464,13 @@ function GenrePlaylistTreeWheelRadialPopCore(props) {
785
464
  }
786
465
 
787
466
  // src/genre-tree/GenreTreeWheelHandoff.tsx
788
- import { useEffect as useEffect2, useRef as useRef3, useState as useState2 } from "react";
789
- import { jsx as jsx8, jsxs as jsxs4 } from "react/jsx-runtime";
467
+ import { useEffect, useRef, useState } from "react";
468
+ import { jsx as jsx4, jsxs } from "react/jsx-runtime";
790
469
  function GenreTreeWheelHandoff({ skeleton, children, onRevealedChange }) {
791
- const [revealed, setRevealed] = useState2(false);
792
- const onRevealedChangeRef = useRef3(onRevealedChange);
470
+ const [revealed, setRevealed] = useState(false);
471
+ const onRevealedChangeRef = useRef(onRevealedChange);
793
472
  onRevealedChangeRef.current = onRevealedChange;
794
- useEffect2(() => {
473
+ useEffect(() => {
795
474
  let innerFrame = 0;
796
475
  const outerFrame = requestAnimationFrame(() => {
797
476
  innerFrame = requestAnimationFrame(() => {
@@ -805,8 +484,8 @@ function GenreTreeWheelHandoff({ skeleton, children, onRevealedChange }) {
805
484
  cancelAnimationFrame(innerFrame);
806
485
  };
807
486
  }, []);
808
- return /* @__PURE__ */ jsxs4("div", { className: "relative w-full h-full", children: [
809
- /* @__PURE__ */ jsx8(
487
+ return /* @__PURE__ */ jsxs("div", { className: "relative w-full h-full", children: [
488
+ /* @__PURE__ */ jsx4(
810
489
  "div",
811
490
  {
812
491
  className: "absolute inset-0 transition-opacity duration-150 ease-out",
@@ -815,7 +494,7 @@ function GenreTreeWheelHandoff({ skeleton, children, onRevealedChange }) {
815
494
  children
816
495
  }
817
496
  ),
818
- !revealed && /* @__PURE__ */ jsx8("div", { className: "absolute inset-0", "aria-hidden": "true", children: skeleton })
497
+ !revealed && /* @__PURE__ */ jsx4("div", { className: "absolute inset-0", "aria-hidden": "true", children: skeleton })
819
498
  ] });
820
499
  }
821
500
 
@@ -823,7 +502,7 @@ function GenreTreeWheelHandoff({ skeleton, children, onRevealedChange }) {
823
502
  import { ArrowLeft, LoaderCircle, Search, X } from "lucide-react";
824
503
  import { Input } from "@behindthemusictree/ui";
825
504
  import { useGenreSearch } from "@behindthemusictree/app-kit/genre-tree";
826
- import { jsx as jsx9, jsxs as jsxs5 } from "react/jsx-runtime";
505
+ import { jsx as jsx5, jsxs as jsxs2 } from "react/jsx-runtime";
827
506
  function GenreSearch({
828
507
  genrePlaylists,
829
508
  onSelect,
@@ -845,8 +524,8 @@ function GenreSearch({
845
524
  const loading = query !== "" && query !== selectedName && (extraResults?.loading ?? false);
846
525
  const showList = results.length > 0 || extraItems.length > 0;
847
526
  const heading = "px-3 pt-2 pb-1 text-xs font-medium uppercase text-[var(--color-text-muted)]";
848
- return /* @__PURE__ */ jsxs5("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: [
849
- onPrevious && /* @__PURE__ */ jsx9(
527
+ return /* @__PURE__ */ jsxs2("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: [
528
+ onPrevious && /* @__PURE__ */ jsx5(
850
529
  "button",
851
530
  {
852
531
  type: "button",
@@ -854,10 +533,10 @@ function GenreSearch({
854
533
  title: "Previous",
855
534
  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)]",
856
535
  onClick: onPrevious,
857
- children: /* @__PURE__ */ jsx9(ArrowLeft, { "aria-hidden": "true", className: "w-4 h-4" })
536
+ children: /* @__PURE__ */ jsx5(ArrowLeft, { "aria-hidden": "true", className: "w-4 h-4" })
858
537
  }
859
538
  ),
860
- /* @__PURE__ */ jsx9(
539
+ /* @__PURE__ */ jsx5(
861
540
  Input,
862
541
  {
863
542
  type: "text",
@@ -875,21 +554,21 @@ function GenreSearch({
875
554
  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"
876
555
  }
877
556
  ),
878
- /* @__PURE__ */ jsxs5("div", { className: "absolute right-3 top-1/2 flex items-center -translate-y-1/2", children: [
879
- /* @__PURE__ */ jsxs5(
557
+ /* @__PURE__ */ jsxs2("div", { className: "absolute right-3 top-1/2 flex items-center -translate-y-1/2", children: [
558
+ /* @__PURE__ */ jsxs2(
880
559
  "span",
881
560
  {
882
561
  "data-loading": loading,
883
562
  className: "relative p-2 text-[var(--color-text-muted)] pointer-events-none",
884
563
  children: [
885
- /* @__PURE__ */ jsx9(
564
+ /* @__PURE__ */ jsx5(
886
565
  Search,
887
566
  {
888
567
  "aria-hidden": "true",
889
568
  className: `w-6 h-6 transition-opacity duration-200 motion-reduce:transition-none ${loading ? "opacity-0" : ""}`
890
569
  }
891
570
  ),
892
- /* @__PURE__ */ jsx9(
571
+ /* @__PURE__ */ jsx5(
893
572
  LoaderCircle,
894
573
  {
895
574
  "aria-hidden": "true",
@@ -899,7 +578,7 @@ function GenreSearch({
899
578
  ]
900
579
  }
901
580
  ),
902
- canClear && /* @__PURE__ */ jsx9(
581
+ canClear && /* @__PURE__ */ jsx5(
903
582
  "button",
904
583
  {
905
584
  type: "button",
@@ -909,14 +588,14 @@ function GenreSearch({
909
588
  clear();
910
589
  onQueryChange?.("");
911
590
  },
912
- children: /* @__PURE__ */ jsx9(X, { "aria-hidden": "true", className: "w-6 h-6" })
591
+ children: /* @__PURE__ */ jsx5(X, { "aria-hidden": "true", className: "w-6 h-6" })
913
592
  }
914
593
  )
915
594
  ] }),
916
- extraResults?.status && /* @__PURE__ */ jsx9("p", { role: "status", "aria-live": "polite", className: "sr-only", children: extraResults.status }),
917
- showList && /* @__PURE__ */ jsxs5("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: [
918
- extraResults && results.length > 0 && /* @__PURE__ */ jsx9("li", { className: heading, children: "Genres" }),
919
- results.map((genrePlaylist) => /* @__PURE__ */ jsx9("li", { children: /* @__PURE__ */ jsxs5(
595
+ extraResults?.status && /* @__PURE__ */ jsx5("p", { role: "status", "aria-live": "polite", className: "sr-only", children: extraResults.status }),
596
+ showList && /* @__PURE__ */ jsxs2("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: [
597
+ extraResults && results.length > 0 && /* @__PURE__ */ jsx5("li", { className: heading, children: "Genres" }),
598
+ results.map((genrePlaylist) => /* @__PURE__ */ jsx5("li", { children: /* @__PURE__ */ jsxs2(
920
599
  "button",
921
600
  {
922
601
  type: "button",
@@ -927,12 +606,12 @@ function GenreSearch({
927
606
  },
928
607
  children: [
929
608
  genrePlaylist.name,
930
- genrePlaylist.isUmbrella && /* @__PURE__ */ jsx9("span", { className: "ml-2 text-xs italic text-[var(--color-text-muted)]", children: "umbrella" })
609
+ genrePlaylist.isUmbrella && /* @__PURE__ */ jsx5("span", { className: "ml-2 text-xs italic text-[var(--color-text-muted)]", children: "umbrella" })
931
610
  ]
932
611
  }
933
612
  ) }, genrePlaylist.uuid)),
934
- extraResults && extraItems.length > 0 && /* @__PURE__ */ jsx9("li", { className: heading, children: extraResults.label }),
935
- extraItems.map((item) => /* @__PURE__ */ jsx9("li", { children: /* @__PURE__ */ jsx9(
613
+ extraResults && extraItems.length > 0 && /* @__PURE__ */ jsx5("li", { className: heading, children: extraResults.label }),
614
+ extraItems.map((item) => /* @__PURE__ */ jsx5("li", { children: /* @__PURE__ */ jsx5(
936
615
  "button",
937
616
  {
938
617
  type: "button",
@@ -949,26 +628,29 @@ function GenreSearch({
949
628
  ] });
950
629
  }
951
630
 
631
+ // src/genre-tree/GenreInfoPanelBody.tsx
632
+ import { usePlayer as usePlayer5 } from "@behindthemusictree/app-kit/player";
633
+
952
634
  // src/genre-tree/GenreActionBand.tsx
953
635
  import { ArrowLeftRight, MoveUpRight, Pause, Pencil, Play, Plus, ZoomIn } from "lucide-react";
954
636
 
955
637
  // src/genre-tree/ShareActionButton.tsx
956
638
  import { Share2 as Share22 } from "lucide-react";
957
- import { usePopup as usePopup4 } from "@behindthemusictree/app-kit/popup";
639
+ import { usePopup as usePopup3 } from "@behindthemusictree/app-kit/popup";
958
640
 
959
641
  // src/genre-tree/SharePopup.tsx
960
- import { useCallback as useCallback5, useEffect as useEffect3, useMemo as useMemo5, useState as useState3 } from "react";
642
+ import { useCallback as useCallback5, useEffect as useEffect2, useMemo as useMemo5, useState as useState2 } from "react";
961
643
  import { Link2, Share2 } from "lucide-react";
962
644
  import { FaWhatsapp } from "react-icons/fa";
963
645
  import { SiGmail } from "react-icons/si";
964
646
 
965
647
  // src/genre-tree/ShareDestinationButton.tsx
966
- import { Fragment as Fragment4, jsx as jsx10, jsxs as jsxs6 } from "react/jsx-runtime";
648
+ import { Fragment, jsx as jsx6, jsxs as jsxs3 } from "react/jsx-runtime";
967
649
  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";
968
650
  function Contents({ icon, label }) {
969
- return /* @__PURE__ */ jsxs6(Fragment4, { children: [
970
- /* @__PURE__ */ jsx10("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 }),
971
- /* @__PURE__ */ jsx10("span", { className: "max-w-full truncate", children: label })
651
+ return /* @__PURE__ */ jsxs3(Fragment, { children: [
652
+ /* @__PURE__ */ jsx6("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 }),
653
+ /* @__PURE__ */ jsx6("span", { className: "max-w-full truncate", children: label })
972
654
  ] });
973
655
  }
974
656
  function ShareDestinationButton({
@@ -980,29 +662,29 @@ function ShareDestinationButton({
980
662
  ariaLabel
981
663
  }) {
982
664
  if (href) {
983
- return /* @__PURE__ */ jsx10("a", { href, target: "_blank", rel: "noopener noreferrer", "aria-label": ariaLabel, className, children: /* @__PURE__ */ jsx10(Contents, { icon, label }) });
665
+ return /* @__PURE__ */ jsx6("a", { href, target: "_blank", rel: "noopener noreferrer", "aria-label": ariaLabel, className, children: /* @__PURE__ */ jsx6(Contents, { icon, label }) });
984
666
  }
985
- return /* @__PURE__ */ jsx10("button", { type: "button", onClick, disabled, "aria-label": ariaLabel, className, children: /* @__PURE__ */ jsx10(Contents, { icon, label }) });
667
+ return /* @__PURE__ */ jsx6("button", { type: "button", onClick, disabled, "aria-label": ariaLabel, className, children: /* @__PURE__ */ jsx6(Contents, { icon, label }) });
986
668
  }
987
669
 
988
670
  // src/genre-tree/SharePreviewCard.tsx
989
671
  import { Music2, UserRound } from "lucide-react";
990
- import { jsx as jsx11, jsxs as jsxs7 } from "react/jsx-runtime";
672
+ import { jsx as jsx7, jsxs as jsxs4 } from "react/jsx-runtime";
991
673
  function SharePreviewCard({ target, url }) {
992
674
  const Icon = target.kind === "genre" ? Music2 : UserRound;
993
675
  const typeLabel = target.kind === "genre" ? "Genre" : "Artist";
994
- return /* @__PURE__ */ jsx11("div", { className: "rounded-3xl bg-[var(--color-surface-muted)] p-4", children: /* @__PURE__ */ jsxs7("div", { className: "flex min-h-32 overflow-hidden rounded-2xl bg-[var(--color-surface)] shadow-lg", children: [
995
- /* @__PURE__ */ jsx11("div", { className: "flex w-28 shrink-0 items-center justify-center bg-[var(--color-primary)]/15 text-[var(--color-primary)]", children: /* @__PURE__ */ jsx11(Icon, { "aria-hidden": "true", className: "h-10 w-10" }) }),
996
- /* @__PURE__ */ jsxs7("div", { className: "flex min-w-0 flex-1 flex-col justify-center gap-1 p-4", children: [
997
- /* @__PURE__ */ jsx11("span", { className: "text-xs font-medium uppercase tracking-wide text-[var(--color-text-muted)]", children: typeLabel }),
998
- /* @__PURE__ */ jsx11("span", { className: "truncate text-base font-semibold text-[var(--color-text)]", title: target.name, children: target.name }),
999
- /* @__PURE__ */ jsx11("span", { className: "truncate text-sm text-[var(--color-primary)]", title: url, children: url })
676
+ return /* @__PURE__ */ jsx7("div", { className: "rounded-3xl bg-[var(--color-surface-muted)] p-4", children: /* @__PURE__ */ jsxs4("div", { className: "flex min-h-32 overflow-hidden rounded-2xl bg-[var(--color-surface)] shadow-lg", children: [
677
+ /* @__PURE__ */ jsx7("div", { className: "flex w-28 shrink-0 items-center justify-center bg-[var(--color-primary)]/15 text-[var(--color-primary)]", children: /* @__PURE__ */ jsx7(Icon, { "aria-hidden": "true", className: "h-10 w-10" }) }),
678
+ /* @__PURE__ */ jsxs4("div", { className: "flex min-w-0 flex-1 flex-col justify-center gap-1 p-4", children: [
679
+ /* @__PURE__ */ jsx7("span", { className: "text-xs font-medium uppercase tracking-wide text-[var(--color-text-muted)]", children: typeLabel }),
680
+ /* @__PURE__ */ jsx7("span", { className: "truncate text-base font-semibold text-[var(--color-text)]", title: target.name, children: target.name }),
681
+ /* @__PURE__ */ jsx7("span", { className: "truncate text-sm text-[var(--color-primary)]", title: url, children: url })
1000
682
  ] })
1001
683
  ] }) });
1002
684
  }
1003
685
 
1004
686
  // src/genre-tree/SharePopup.tsx
1005
- import { Fragment as Fragment5, jsx as jsx12, jsxs as jsxs8 } from "react/jsx-runtime";
687
+ import { Fragment as Fragment2, jsx as jsx8, jsxs as jsxs5 } from "react/jsx-runtime";
1006
688
  function resolveAbsoluteUrl(url) {
1007
689
  try {
1008
690
  const parsed = new URL(url, window.location.href);
@@ -1035,17 +717,17 @@ async function copyText(text) {
1035
717
  }
1036
718
  }
1037
719
  function SharePopup({ target, url, onClose }) {
1038
- const [absoluteUrl, setAbsoluteUrl] = useState3(url);
1039
- const [copyStatus, setCopyStatus] = useState3("idle");
1040
- const [canUseNativeShare, setCanUseNativeShare] = useState3(false);
1041
- const [shareError, setShareError] = useState3(null);
1042
- const [toastId, setToastId] = useState3(0);
1043
- useEffect3(() => {
720
+ const [absoluteUrl, setAbsoluteUrl] = useState2(url);
721
+ const [copyStatus, setCopyStatus] = useState2("idle");
722
+ const [canUseNativeShare, setCanUseNativeShare] = useState2(false);
723
+ const [shareError, setShareError] = useState2(null);
724
+ const [toastId, setToastId] = useState2(0);
725
+ useEffect2(() => {
1044
726
  setAbsoluteUrl(resolveAbsoluteUrl(url) ?? "");
1045
727
  setCopyStatus("idle");
1046
728
  setShareError(null);
1047
729
  }, [url]);
1048
- useEffect3(() => {
730
+ useEffect2(() => {
1049
731
  setCanUseNativeShare(typeof navigator !== "undefined" && typeof navigator.share === "function");
1050
732
  }, []);
1051
733
  const destinationUrls = useMemo5(() => {
@@ -1077,8 +759,8 @@ function SharePopup({ target, url, onClose }) {
1077
759
  setShareError("Could not open the share options.");
1078
760
  }
1079
761
  };
1080
- return /* @__PURE__ */ jsxs8(Fragment5, { children: [
1081
- /* @__PURE__ */ jsx12(
762
+ return /* @__PURE__ */ jsxs5(Fragment2, { children: [
763
+ /* @__PURE__ */ jsx8(
1082
764
  BasePopup_default,
1083
765
  {
1084
766
  title: "Share",
@@ -1093,175 +775,550 @@ function SharePopup({ target, url, onClose }) {
1093
775
  titleTextClassName: "!text-xl !font-semibold !text-[var(--color-text)]",
1094
776
  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)]",
1095
777
  contentClassName: "!rounded-none !bg-transparent !px-6 !pb-6 !pt-2",
1096
- children: /* @__PURE__ */ jsxs8("div", { className: "flex flex-col gap-4", children: [
1097
- /* @__PURE__ */ jsx12(SharePreviewCard, { target, url: absoluteUrl || url }),
1098
- /* @__PURE__ */ jsxs8("div", { className: "grid grid-cols-3 gap-1 rounded-3xl bg-[var(--color-surface)] p-3 sm:grid-cols-4", children: [
1099
- /* @__PURE__ */ jsx12(
778
+ children: /* @__PURE__ */ jsxs5("div", { className: "flex flex-col gap-4", children: [
779
+ /* @__PURE__ */ jsx8(SharePreviewCard, { target, url: absoluteUrl || url }),
780
+ /* @__PURE__ */ jsxs5("div", { className: "grid grid-cols-3 gap-1 rounded-3xl bg-[var(--color-surface)] p-3 sm:grid-cols-4", children: [
781
+ /* @__PURE__ */ jsx8(
1100
782
  ShareDestinationButton,
1101
783
  {
1102
784
  label: "Copy Link",
1103
- icon: /* @__PURE__ */ jsx12(Link2, { "aria-hidden": "true", className: "h-6 w-6" }),
785
+ icon: /* @__PURE__ */ jsx8(Link2, { "aria-hidden": "true", className: "h-6 w-6" }),
1104
786
  onClick: () => void handleCopy(),
1105
787
  ariaLabel: "Copy link",
1106
788
  disabled: !absoluteUrl
1107
789
  }
1108
790
  ),
1109
- /* @__PURE__ */ jsx12(
791
+ /* @__PURE__ */ jsx8(
1110
792
  ShareDestinationButton,
1111
793
  {
1112
794
  label: "WhatsApp",
1113
- icon: /* @__PURE__ */ jsx12(FaWhatsapp, { "aria-hidden": "true", className: "h-9 w-9 text-[#25D366]" }),
795
+ icon: /* @__PURE__ */ jsx8(FaWhatsapp, { "aria-hidden": "true", className: "h-9 w-9 text-[#25D366]" }),
1114
796
  href: destinationUrls?.whatsapp,
1115
797
  ariaLabel: `Share ${target.name} via WhatsApp`,
1116
798
  disabled: !absoluteUrl
1117
799
  }
1118
800
  ),
1119
- /* @__PURE__ */ jsx12(
801
+ /* @__PURE__ */ jsx8(
1120
802
  ShareDestinationButton,
1121
803
  {
1122
804
  label: "Gmail",
1123
- icon: /* @__PURE__ */ jsx12(SiGmail, { "aria-hidden": "true", className: "h-8 w-8 text-[#EA4335]" }),
805
+ icon: /* @__PURE__ */ jsx8(SiGmail, { "aria-hidden": "true", className: "h-8 w-8 text-[#EA4335]" }),
1124
806
  href: destinationUrls?.gmail,
1125
807
  ariaLabel: `Email ${target.name} with Gmail`,
1126
808
  disabled: !absoluteUrl
1127
809
  }
1128
810
  ),
1129
- canUseNativeShare && /* @__PURE__ */ jsx12(
811
+ canUseNativeShare && /* @__PURE__ */ jsx8(
1130
812
  ShareDestinationButton,
1131
813
  {
1132
814
  label: "More",
1133
- icon: /* @__PURE__ */ jsx12(Share2, { "aria-hidden": "true", className: "h-6 w-6" }),
815
+ icon: /* @__PURE__ */ jsx8(Share2, { "aria-hidden": "true", className: "h-6 w-6" }),
1134
816
  onClick: () => void handleNativeShare(),
1135
817
  ariaLabel: "More sharing options",
1136
818
  disabled: !absoluteUrl
1137
819
  }
1138
820
  )
1139
821
  ] }),
1140
- copyStatus === "failed" && /* @__PURE__ */ jsx12("p", { role: "alert", className: "text-center text-sm text-[var(--color-red-500)]", children: "Could not copy the link." }),
1141
- shareError && /* @__PURE__ */ jsx12("p", { role: "alert", className: "text-center text-sm text-[var(--color-red-500)]", children: shareError })
822
+ copyStatus === "failed" && /* @__PURE__ */ jsx8("p", { role: "alert", className: "text-center text-sm text-[var(--color-red-500)]", children: "Could not copy the link." }),
823
+ shareError && /* @__PURE__ */ jsx8("p", { role: "alert", className: "text-center text-sm text-[var(--color-red-500)]", children: shareError })
1142
824
  ] })
1143
825
  }
1144
- ),
1145
- toastId > 0 && /* @__PURE__ */ jsx12(Toast, { message: "Link copied", onClose: dismissToast }, toastId)
826
+ ),
827
+ toastId > 0 && /* @__PURE__ */ jsx8(Toast, { message: "Link copied", onClose: dismissToast }, toastId)
828
+ ] });
829
+ }
830
+
831
+ // src/genre-tree/ShareActionButton.tsx
832
+ import { jsx as jsx9, jsxs as jsxs6 } from "react/jsx-runtime";
833
+ function ShareActionButton({ target, url }) {
834
+ const { showPopup, hidePopup } = usePopup3();
835
+ const openSharePopup = () => {
836
+ showPopup(/* @__PURE__ */ jsx9(SharePopup, { target, url, onClose: hidePopup }));
837
+ };
838
+ return /* @__PURE__ */ jsxs6(
839
+ "button",
840
+ {
841
+ type: "button",
842
+ onClick: openSharePopup,
843
+ className: "group flex flex-col items-center gap-1 w-16 text-xs text-center text-[var(--color-text)] focus-visible:outline-none",
844
+ "aria-label": `Share ${target.name}`,
845
+ title: `Share ${target.name}`,
846
+ children: [
847
+ /* @__PURE__ */ jsx9("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__ */ jsx9(Share22, { "aria-hidden": "true", className: "w-5 h-5" }) }),
848
+ "Share"
849
+ ]
850
+ }
851
+ );
852
+ }
853
+
854
+ // src/genre-tree/GenreActionBand.tsx
855
+ import { jsx as jsx10, jsxs as jsxs7 } from "react/jsx-runtime";
856
+ function GenreActionBand({
857
+ node,
858
+ onPlayPause,
859
+ isPlaying = false,
860
+ onZoomIn,
861
+ onAddChild,
862
+ onRename,
863
+ onChangeParent,
864
+ side,
865
+ onToggleSide,
866
+ toggleSideDisabledReason,
867
+ additionalActions,
868
+ shareTarget,
869
+ shareUrl
870
+ }) {
871
+ const items = [];
872
+ const adminItems = [];
873
+ if (onPlayPause) {
874
+ items.push({
875
+ key: "play-pause",
876
+ label: isPlaying ? "Pause" : "Play",
877
+ icon: isPlaying ? /* @__PURE__ */ jsx10(Pause, { "aria-hidden": "true", className: "w-5 h-5" }) : /* @__PURE__ */ jsx10(Play, { "aria-hidden": "true", className: "w-5 h-5" }),
878
+ onClick: onPlayPause
879
+ });
880
+ }
881
+ if (onZoomIn) {
882
+ items.push({
883
+ key: "zoom-in",
884
+ label: "Focus",
885
+ icon: /* @__PURE__ */ jsx10(ZoomIn, { "aria-hidden": "true", className: "w-5 h-5" }),
886
+ onClick: onZoomIn
887
+ });
888
+ }
889
+ if (onAddChild) {
890
+ adminItems.push({ key: "add-child", label: "Add", icon: /* @__PURE__ */ jsx10(Plus, { "aria-hidden": "true", className: "w-5 h-5" }), onClick: onAddChild });
891
+ }
892
+ if (onRename) {
893
+ adminItems.push({ key: "rename", label: "Rename", icon: /* @__PURE__ */ jsx10(Pencil, { "aria-hidden": "true", className: "w-5 h-5" }), onClick: onRename });
894
+ }
895
+ if (onChangeParent) {
896
+ adminItems.push({ key: "change-parent", label: "Parent", icon: /* @__PURE__ */ jsx10(MoveUpRight, { "aria-hidden": "true", className: "w-5 h-5" }), onClick: onChangeParent });
897
+ }
898
+ if (onToggleSide) {
899
+ adminItems.push({
900
+ key: "toggle-side",
901
+ label: side === "pop" ? "Make core" : "Make pop",
902
+ icon: /* @__PURE__ */ jsx10(ArrowLeftRight, { "aria-hidden": "true", className: "w-5 h-5" }),
903
+ onClick: onToggleSide,
904
+ disabled: !!toggleSideDisabledReason,
905
+ title: toggleSideDisabledReason
906
+ });
907
+ }
908
+ for (const action of additionalActions?.(node) ?? []) {
909
+ items.push({
910
+ key: action.key,
911
+ label: action.label(node),
912
+ icon: action.icon(node),
913
+ onClick: (event) => action.onClick(event, node),
914
+ disabled: action.enabled ? !action.enabled(node) : false
915
+ });
916
+ }
917
+ if (items.length === 0 && (!shareTarget || !shareUrl) && adminItems.length === 0) return null;
918
+ const renderItems = (list) => list.map((item) => /* @__PURE__ */ jsxs7(
919
+ "button",
920
+ {
921
+ type: "button",
922
+ disabled: item.disabled,
923
+ title: item.title,
924
+ onClick: item.onClick,
925
+ 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",
926
+ children: [
927
+ /* @__PURE__ */ jsx10("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 }),
928
+ item.label
929
+ ]
930
+ },
931
+ item.key
932
+ ));
933
+ return /* @__PURE__ */ jsxs7("div", { className: "genre-action-band border-b border-[var(--color-border)]", children: [
934
+ (items.length > 0 || shareTarget && shareUrl) && /* @__PURE__ */ jsxs7("div", { className: "flex flex-wrap gap-1 px-3.5 py-3", children: [
935
+ renderItems(items),
936
+ shareTarget && shareUrl && /* @__PURE__ */ jsx10(ShareActionButton, { target: shareTarget, url: shareUrl })
937
+ ] }),
938
+ adminItems.length > 0 && /* @__PURE__ */ jsxs7(
939
+ "section",
940
+ {
941
+ "aria-label": "Admin actions",
942
+ className: `px-3.5 py-3 ${items.length > 0 ? "border-t border-[var(--color-border)]" : ""}`,
943
+ children: [
944
+ /* @__PURE__ */ jsx10("h3", { className: "mb-2 text-xs font-semibold uppercase tracking-wide text-[var(--color-text-muted)]", children: "Admin" }),
945
+ /* @__PURE__ */ jsx10("div", { className: "flex flex-wrap gap-1", children: renderItems(adminItems) })
946
+ ]
947
+ }
948
+ )
949
+ ] });
950
+ }
951
+
952
+ // src/genre-tree/GenreDetailExtras.tsx
953
+ import { Skeleton as Skeleton2 } from "@behindthemusictree/ui";
954
+ import { useFetchGenreOverview } from "@behindthemusictree/app-kit/genre-tree";
955
+
956
+ // src/genre-tree/ClampedText.tsx
957
+ import { useLayoutEffect, useRef as useRef2, useState as useState3 } from "react";
958
+ import { Fragment as Fragment3, jsx as jsx11, jsxs as jsxs8 } from "react/jsx-runtime";
959
+ function ClampedText({ children, className: className2 = "" }) {
960
+ const ref = useRef2(null);
961
+ const [expanded, setExpanded] = useState3(false);
962
+ const [overflows, setOverflows] = useState3(false);
963
+ useLayoutEffect(() => {
964
+ const element = ref.current;
965
+ if (!element || expanded) return;
966
+ setOverflows(element.scrollHeight > element.clientHeight);
967
+ }, [children, expanded]);
968
+ return /* @__PURE__ */ jsxs8(Fragment3, { children: [
969
+ /* @__PURE__ */ jsx11("p", { ref, className: `${className2} ${expanded ? "" : "line-clamp-3"}`, children }),
970
+ (overflows || expanded) && /* @__PURE__ */ jsx11(
971
+ "button",
972
+ {
973
+ type: "button",
974
+ "aria-expanded": expanded,
975
+ onClick: () => setExpanded((value) => !value),
976
+ className: "mt-1 text-xs text-[var(--color-text-muted)] underline hover:text-[var(--color-text)]",
977
+ children: expanded ? "Show less" : "Show more"
978
+ }
979
+ )
980
+ ] });
981
+ }
982
+
983
+ // src/genre-tree/GenrePlaylistTracks.tsx
984
+ import { useEffect as useEffect3, useRef as useRef3 } from "react";
985
+ import { useInfiniteQuery } from "@tanstack/react-query";
986
+ import { z } from "zod";
987
+ import { FaPlay, FaPause } from "react-icons/fa";
988
+ import { RingLoader, Skeleton } from "@behindthemusictree/ui";
989
+ import { useSession } from "@behindthemusictree/app-kit/auth";
990
+ import { useFetchWrapper } from "@behindthemusictree/app-kit/transport";
991
+ import { PaginatedResponseSchema } from "@behindthemusictree/app-kit/transport";
992
+ import { parseWithLog } from "@behindthemusictree/app-kit/transport";
993
+ import { usePlayer as usePlayer4 } from "@behindthemusictree/app-kit/player";
994
+ import { PlayStates } from "@behindthemusictree/app-kit/player";
995
+ import { usePopup as usePopup4 } from "@behindthemusictree/app-kit/popup";
996
+ import { ErrorCode as ErrorCode3 } from "@behindthemusictree/app-kit/transport";
997
+ import { genrePlaylistQueryKeys } from "@behindthemusictree/app-kit/genre-tree";
998
+ import { libraryEndpoints, libraryQueryKeys } from "@behindthemusictree/app-kit/genre-tree";
999
+ import { isSong, songUnplayableReason, SongDetailedSchema } from "@behindthemusictree/app-kit/genre-tree";
1000
+ import { useTrackList as useTrackList2 } from "@behindthemusictree/app-kit/genre-tree";
1001
+
1002
+ // src/genre-tree/MusicBrainzRecordingLink.tsx
1003
+ import { musicBrainzRecordingUrl } from "@behindthemusictree/app-kit/genre-tree";
1004
+ import { Fragment as Fragment4, jsx as jsx12 } from "react/jsx-runtime";
1005
+ function MusicBrainzRecordingLink({
1006
+ song,
1007
+ children,
1008
+ className: className2 = ""
1009
+ }) {
1010
+ const url = musicBrainzRecordingUrl(song);
1011
+ if (!url) return /* @__PURE__ */ jsx12(Fragment4, { children });
1012
+ return /* @__PURE__ */ jsx12(
1013
+ "a",
1014
+ {
1015
+ href: url,
1016
+ target: "_blank",
1017
+ rel: "noopener noreferrer",
1018
+ "aria-label": `View ${song.title} on MusicBrainz`,
1019
+ className: className2,
1020
+ onClick: (event) => event.stopPropagation(),
1021
+ children
1022
+ }
1023
+ );
1024
+ }
1025
+
1026
+ // src/genre-tree/GenrePlaylistTracks.tsx
1027
+ import { ArtistNames } from "@behindthemusictree/app-kit/genre-tree";
1028
+ import { capitalize as capitalize4, useTrackNoun as useTrackNoun4 } from "@behindthemusictree/app-kit/track-noun";
1029
+ import { jsx as jsx13, jsxs as jsxs9 } from "react/jsx-runtime";
1030
+ var CATALOGUE_PAGE_SIZE = 100;
1031
+ var SongPageSchema = PaginatedResponseSchema(SongDetailedSchema).extend({
1032
+ overallTotal: z.number().nullable(),
1033
+ page: z.number().nullable(),
1034
+ totalPages: z.number().nullable()
1035
+ });
1036
+ var TEXT_CELL = "min-w-0 flex-1 truncate [@container(min-width:40rem)]:flex [@container(min-width:40rem)]:gap-2";
1037
+ var TITLE_CELL = "[@container(min-width:40rem)]:flex-[2] [@container(min-width:40rem)]:truncate";
1038
+ var COLUMN_CELL = "[@container(min-width:40rem)]:flex-1 [@container(min-width:40rem)]:truncate";
1039
+ var ALBUM_CELL = `hidden [@container(min-width:40rem)]:block ${COLUMN_CELL}`;
1040
+ var SEPARATOR = "[@container(min-width:40rem)]:hidden";
1041
+ var nextCursor = (next) => {
1042
+ const cursor = new URL(next).searchParams.get("cursor");
1043
+ if (!cursor) throw new Error(`Songs next link has no cursor: ${next}`);
1044
+ return cursor;
1045
+ };
1046
+ function GenrePlaylistTracks({
1047
+ genreUuid,
1048
+ genrePlaylist,
1049
+ scope,
1050
+ getBackendBaseUrl,
1051
+ renderArtistLink,
1052
+ renderSongLeading,
1053
+ renderSongActions,
1054
+ renderTracksToolbar,
1055
+ artist = null
1056
+ }) {
1057
+ const { fetchGenrePlaylistTracksPage, playNewTrackListFromGenrePlaylist, playNewTrackListFromArtist } = useTrackList2();
1058
+ const { session, sessionRestored } = useSession();
1059
+ const { playerTrackObject, playState, handlePlayPauseAction } = usePlayer4();
1060
+ const { showPopup } = usePopup4();
1061
+ const trackNoun = useTrackNoun4();
1062
+ const { uuid } = genrePlaylist;
1063
+ const { fetch } = useFetchWrapper(getBackendBaseUrl);
1064
+ const { data, isPending, isError, refetch, hasNextPage, isFetchingNextPage, fetchNextPage } = useInfiniteQuery({
1065
+ queryKey: scope === "me" ? genrePlaylistQueryKeys.me.tracks(uuid) : artist ? [...libraryQueryKeys.reference.songs.byGenre(getBackendBaseUrl(), genreUuid), "artist", artist.uuid] : libraryQueryKeys.reference.songs.byGenre(getBackendBaseUrl(), genreUuid),
1066
+ queryFn: async ({ pageParam }) => {
1067
+ if (scope === "me") return fetchGenrePlaylistTracksPage(uuid, scope, Number(pageParam));
1068
+ const response = await fetch(
1069
+ libraryEndpoints.reference.songs.list(),
1070
+ true,
1071
+ false,
1072
+ {},
1073
+ { genre: genreUuid, ...artist && { artist: artist.uuid }, pageSize: CATALOGUE_PAGE_SIZE, withTotal: false, ...pageParam && { cursor: pageParam } }
1074
+ );
1075
+ const parsed = parseWithLog(SongPageSchema, response, "GenrePlaylistTracks");
1076
+ return { tracks: parsed.results, total: parsed.overallTotal, nextPage: parsed.next ? nextCursor(parsed.next) : null };
1077
+ },
1078
+ initialPageParam: scope === "me" ? 1 : "",
1079
+ getNextPageParam: (lastPage) => lastPage.nextPage ?? void 0,
1080
+ enabled: scope !== "me" || sessionRestored && !!session?.accessToken
1081
+ });
1082
+ const sentinelRef = useRef3(null);
1083
+ useEffect3(() => {
1084
+ const sentinel = sentinelRef.current;
1085
+ if (!hasNextPage || isFetchingNextPage || !sentinel) return;
1086
+ const observer = new IntersectionObserver((entries) => {
1087
+ if (entries.some((entry) => entry.isIntersecting)) {
1088
+ void fetchNextPage();
1089
+ }
1090
+ });
1091
+ observer.observe(sentinel);
1092
+ return () => observer.disconnect();
1093
+ }, [hasNextPage, isFetchingNextPage, fetchNextPage]);
1094
+ const pages = data?.pages ?? [];
1095
+ const tracks = pages.flatMap((page) => page.tracks);
1096
+ const total = pages[0]?.total ?? void 0;
1097
+ const play = (startSongUuid) => {
1098
+ const started = artist ? playNewTrackListFromArtist(artist, scope, { startSongUuid, ordering: "title", genreUuid }) : playNewTrackListFromGenrePlaylist(genrePlaylist, scope, { startSongUuid });
1099
+ started.catch((error) => {
1100
+ console.error("Failed to play genre playlist:", error);
1101
+ showPopup(/* @__PURE__ */ jsx13(InternalErrorPopup, { errorCode: ErrorCode3.CLIENT_UNKNOWN }));
1102
+ });
1103
+ };
1104
+ return /* @__PURE__ */ jsxs9("div", { className: "gtv-info-panel-children", children: [
1105
+ /* @__PURE__ */ jsxs9("span", { className: "gtv-info-panel-children-title", children: [
1106
+ capitalize4(trackNoun.plural),
1107
+ total !== void 0 && ` (${total})`
1108
+ ] }),
1109
+ renderTracksToolbar?.(),
1110
+ isPending ? /* @__PURE__ */ jsx13(Skeleton, { className: "h-4 w-3/4", "aria-busy": "true" }) : isError && tracks.length === 0 ? /* @__PURE__ */ jsxs9("p", { role: "alert", children: [
1111
+ "Couldn't load ",
1112
+ trackNoun.plural,
1113
+ ".",
1114
+ " ",
1115
+ /* @__PURE__ */ jsx13("button", { type: "button", className: "underline", onClick: () => void refetch(), children: "Retry" })
1116
+ ] }) : tracks.length === 0 ? /* @__PURE__ */ jsx13("p", { children: "\u2014" }) : /* @__PURE__ */ jsxs9("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: [
1117
+ tracks.map((track, index) => {
1118
+ const label = track.artists?.length ? `${track.title} \u2014 ${track.artists.map((artist2) => artist2.name).join(", ")}` : track.title;
1119
+ const isCurrent = playerTrackObject?.track.id === track.uuid;
1120
+ const isPlaying = isCurrent && playState === PlayStates.PLAYING;
1121
+ const text = /* @__PURE__ */ jsxs9("span", { className: TEXT_CELL, children: [
1122
+ /* @__PURE__ */ jsx13("span", { className: TITLE_CELL, children: isSong(track) ? /* @__PURE__ */ jsx13(MusicBrainzRecordingLink, { song: track, className: "hover:underline focus-visible:underline", children: track.title }) : track.title }),
1123
+ track.artists?.length ? /* @__PURE__ */ jsx13("span", { className: SEPARATOR, children: " \u2014 " }) : null,
1124
+ /* @__PURE__ */ jsx13("span", { className: COLUMN_CELL, children: track.artists?.length ? /* @__PURE__ */ jsx13(ArtistNames, { artists: track.artists, renderArtistLink }) : null }),
1125
+ track.album && /* @__PURE__ */ jsx13("span", { className: ALBUM_CELL, children: track.album.name })
1126
+ ] });
1127
+ const leading = renderSongLeading?.(track, index);
1128
+ const actions = renderSongActions?.(track);
1129
+ const isUnplayable = isSong(track) && songUnplayableReason(track) !== null;
1130
+ if (isUnplayable) {
1131
+ return /* @__PURE__ */ jsxs9("li", { className: "flex min-h-8 items-center gap-1 px-1 opacity-50", children: [
1132
+ leading,
1133
+ /* @__PURE__ */ jsx13(
1134
+ "button",
1135
+ {
1136
+ type: "button",
1137
+ disabled: true,
1138
+ "aria-disabled": "true",
1139
+ "aria-label": label,
1140
+ className: "flex h-6 w-6 shrink-0 items-center justify-center rounded-full opacity-0 cursor-not-allowed",
1141
+ children: /* @__PURE__ */ jsx13(FaPlay, { size: 10 })
1142
+ }
1143
+ ),
1144
+ text,
1145
+ actions
1146
+ ] }, track.uuid);
1147
+ }
1148
+ return /* @__PURE__ */ jsxs9(
1149
+ "li",
1150
+ {
1151
+ 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)]" : ""}`,
1152
+ children: [
1153
+ leading,
1154
+ /* @__PURE__ */ jsx13(
1155
+ "button",
1156
+ {
1157
+ type: "button",
1158
+ "aria-label": `${isPlaying ? "Pause" : "Play"} ${label}`,
1159
+ 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 !== PlayStates.STOPPED ? "opacity-100" : "opacity-0 [@media(pointer:coarse)]:opacity-100"}`,
1160
+ onClick: () => isCurrent ? handlePlayPauseAction() : play(track.uuid),
1161
+ children: isCurrent && playState === PlayStates.LOADING ? /* @__PURE__ */ jsx13(RingLoader, { size: 10 }) : isPlaying ? /* @__PURE__ */ jsx13(FaPause, { size: 10 }) : /* @__PURE__ */ jsx13(FaPlay, { size: 10 })
1162
+ }
1163
+ ),
1164
+ text,
1165
+ actions
1166
+ ]
1167
+ },
1168
+ track.uuid
1169
+ );
1170
+ }),
1171
+ hasNextPage && /* @__PURE__ */ jsx13("li", { ref: sentinelRef, "aria-hidden": "true", className: "h-4" })
1172
+ ] })
1173
+ ] });
1174
+ }
1175
+
1176
+ // src/genre-tree/GenreDetailExtras.tsx
1177
+ import { useTrackNoun as useTrackNoun5 } from "@behindthemusictree/app-kit/track-noun";
1178
+ import { SiWikidata, SiWikipedia } from "react-icons/si";
1179
+ import { Fragment as Fragment5, jsx as jsx14, jsxs as jsxs10 } from "react/jsx-runtime";
1180
+ function GenreDetailExtras({
1181
+ genreUuid,
1182
+ genrePlaylist,
1183
+ wikidataId,
1184
+ scope,
1185
+ getBackendBaseUrl,
1186
+ criteriaOverviewSchema,
1187
+ renderGenreDetailExtras,
1188
+ renderArtistLink,
1189
+ renderSummary,
1190
+ renderEssentialTracks,
1191
+ family,
1192
+ ...trackSlots
1193
+ }) {
1194
+ const trackNoun = useTrackNoun5();
1195
+ const { data: overview, isPending } = useFetchGenreOverview(
1196
+ genreUuid,
1197
+ scope,
1198
+ getBackendBaseUrl,
1199
+ criteriaOverviewSchema
1200
+ );
1201
+ if (isPending) {
1202
+ return /* @__PURE__ */ jsxs10(Fragment5, { children: [
1203
+ /* @__PURE__ */ jsxs10("div", { className: "gtv-info-panel-children", "aria-busy": "true", "data-testid": "genre-detail-extras-skeleton", children: [
1204
+ /* @__PURE__ */ jsx14("span", { className: "gtv-info-panel-children-title", children: "Summary" }),
1205
+ /* @__PURE__ */ jsx14(Skeleton2, { className: "h-4 w-3/4" })
1206
+ ] }),
1207
+ family
1208
+ ] });
1209
+ }
1210
+ if (!overview) return /* @__PURE__ */ jsx14(Fragment5, { children: family });
1211
+ const { summary, summarySourceUrl, essentialSongs } = overview;
1212
+ const wikidataUrl = wikidataId ? `https://www.wikidata.org/wiki/${encodeURIComponent(wikidataId)}` : null;
1213
+ return /* @__PURE__ */ jsxs10(Fragment5, { children: [
1214
+ (summarySourceUrl || wikidataUrl) && /* @__PURE__ */ jsxs10("div", { className: "gtv-info-panel-children text-[0.8125rem] leading-[1.45] text-[var(--color-text)]", children: [
1215
+ /* @__PURE__ */ jsx14("span", { className: "gtv-info-panel-children-title", children: "Links" }),
1216
+ /* @__PURE__ */ jsxs10("div", { className: "mt-1 flex flex-wrap gap-x-3 gap-y-1 text-xs", children: [
1217
+ summarySourceUrl && /* @__PURE__ */ jsxs10(
1218
+ "a",
1219
+ {
1220
+ href: summarySourceUrl,
1221
+ target: "_blank",
1222
+ rel: "noopener noreferrer",
1223
+ className: "inline-flex items-center gap-1.5 text-[var(--color-text-muted)] hover:text-[var(--color-text)]",
1224
+ children: [
1225
+ /* @__PURE__ */ jsx14(SiWikipedia, { "aria-hidden": "true", className: "h-3.5 w-3.5" }),
1226
+ "Wikipedia"
1227
+ ]
1228
+ }
1229
+ ),
1230
+ wikidataUrl && /* @__PURE__ */ jsxs10(
1231
+ "a",
1232
+ {
1233
+ href: wikidataUrl,
1234
+ target: "_blank",
1235
+ rel: "noopener noreferrer",
1236
+ className: "inline-flex items-center gap-1.5 text-[var(--color-text-muted)] hover:text-[var(--color-text)]",
1237
+ children: [
1238
+ /* @__PURE__ */ jsx14(SiWikidata, { "aria-hidden": "true", className: "h-3.5 w-3.5" }),
1239
+ "Wikidata"
1240
+ ]
1241
+ }
1242
+ )
1243
+ ] })
1244
+ ] }),
1245
+ renderSummary ? renderSummary(overview) : /* @__PURE__ */ jsxs10("div", { className: "gtv-info-panel-children text-[0.8125rem] leading-[1.45] text-[var(--color-text)]", children: [
1246
+ /* @__PURE__ */ jsx14("span", { className: "gtv-info-panel-children-title", children: "Summary" }),
1247
+ /* @__PURE__ */ jsx14(ClampedText, { className: "mt-1", children: summary ?? "\u2014" }, genreUuid)
1248
+ ] }),
1249
+ family,
1250
+ renderGenreDetailExtras?.(overview),
1251
+ renderEssentialTracks ? renderEssentialTracks(overview) : essentialSongs && /* @__PURE__ */ jsxs10("div", { className: "gtv-info-panel-children text-[0.8125rem] leading-[1.45] text-[var(--color-text)]", children: [
1252
+ /* @__PURE__ */ jsxs10("span", { className: "gtv-info-panel-children-title", children: [
1253
+ "Essential ",
1254
+ trackNoun.plural
1255
+ ] }),
1256
+ essentialSongs.length > 0 ? /* @__PURE__ */ jsx14("ul", { className: "mt-1 divide-y divide-[var(--color-border)]", children: essentialSongs.map((track) => /* @__PURE__ */ jsx14("li", { className: "py-1", children: track.title }, track.uuid)) }) : /* @__PURE__ */ jsx14("p", { className: "mt-1", children: "\u2014" })
1257
+ ] }),
1258
+ /* @__PURE__ */ jsx14(
1259
+ GenrePlaylistTracks,
1260
+ {
1261
+ genreUuid,
1262
+ genrePlaylist,
1263
+ scope,
1264
+ getBackendBaseUrl,
1265
+ renderArtistLink,
1266
+ ...trackSlots
1267
+ }
1268
+ )
1146
1269
  ] });
1147
1270
  }
1148
1271
 
1149
- // src/genre-tree/ShareActionButton.tsx
1150
- import { jsx as jsx13, jsxs as jsxs9 } from "react/jsx-runtime";
1151
- function ShareActionButton({ target, url }) {
1152
- const { showPopup, hidePopup } = usePopup4();
1153
- const openSharePopup = () => {
1154
- showPopup(/* @__PURE__ */ jsx13(SharePopup, { target, url, onClose: hidePopup }));
1272
+ // src/genre-tree/GenreInfoPanelBody.tsx
1273
+ import { Fragment as Fragment6, jsx as jsx15, jsxs as jsxs11 } from "react/jsx-runtime";
1274
+ import { createElement } from "react";
1275
+ function GenreNodeActionBand({ genrePlaylist, playback, ...bandProps }) {
1276
+ const { isPlaying, setIsPlaying } = usePlayer5();
1277
+ const isCurrentTrackList = playback.playingNodeId === genrePlaylist.uuid;
1278
+ const handlePlayPause = () => {
1279
+ if (isCurrentTrackList) {
1280
+ setIsPlaying(!isPlaying);
1281
+ return;
1282
+ }
1283
+ playback.play(genrePlaylist).catch((error) => {
1284
+ console.error("Failed to load genre playlist tracks:", error);
1285
+ });
1155
1286
  };
1156
- return /* @__PURE__ */ jsxs9(
1157
- "button",
1287
+ return /* @__PURE__ */ jsx15(
1288
+ GenreActionBand,
1158
1289
  {
1159
- type: "button",
1160
- onClick: openSharePopup,
1161
- className: "group flex flex-col items-center gap-1 w-16 text-xs text-center text-[var(--color-text)] focus-visible:outline-none",
1162
- "aria-label": `Share ${target.name}`,
1163
- title: `Share ${target.name}`,
1164
- children: [
1165
- /* @__PURE__ */ jsx13("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__ */ jsx13(Share22, { "aria-hidden": "true", className: "w-5 h-5" }) }),
1166
- "Share"
1167
- ]
1290
+ ...bandProps,
1291
+ onPlayPause: genrePlaylist.tracksCount > 0 ? handlePlayPause : void 0,
1292
+ isPlaying: isCurrentTrackList && isPlaying
1168
1293
  }
1169
1294
  );
1170
1295
  }
1171
-
1172
- // src/genre-tree/GenreActionBand.tsx
1173
- import { jsx as jsx14, jsxs as jsxs10 } from "react/jsx-runtime";
1174
- function GenreActionBand({
1175
- node,
1176
- onPlayPause,
1177
- isPlaying = false,
1178
- onZoomIn,
1179
- onAddChild,
1180
- onRename,
1181
- onChangeParent,
1182
- side,
1183
- onToggleSide,
1184
- toggleSideDisabledReason,
1185
- additionalActions,
1186
- shareTarget,
1187
- shareUrl
1296
+ function GenreInfoPanelBody({
1297
+ toolbar,
1298
+ family,
1299
+ genreUuid,
1300
+ genrePlaylist,
1301
+ wikidataId,
1302
+ scope,
1303
+ getBackendBaseUrl,
1304
+ playback,
1305
+ band,
1306
+ extras
1188
1307
  }) {
1189
- const items = [];
1190
- const adminItems = [];
1191
- if (onPlayPause) {
1192
- items.push({
1193
- key: "play-pause",
1194
- label: isPlaying ? "Pause" : "Play",
1195
- icon: isPlaying ? /* @__PURE__ */ jsx14(Pause, { "aria-hidden": "true", className: "w-5 h-5" }) : /* @__PURE__ */ jsx14(Play, { "aria-hidden": "true", className: "w-5 h-5" }),
1196
- onClick: onPlayPause
1197
- });
1198
- }
1199
- if (onZoomIn) {
1200
- items.push({
1201
- key: "zoom-in",
1202
- label: "Focus",
1203
- icon: /* @__PURE__ */ jsx14(ZoomIn, { "aria-hidden": "true", className: "w-5 h-5" }),
1204
- onClick: onZoomIn
1205
- });
1206
- }
1207
- if (onAddChild) {
1208
- adminItems.push({ key: "add-child", label: "Add", icon: /* @__PURE__ */ jsx14(Plus, { "aria-hidden": "true", className: "w-5 h-5" }), onClick: onAddChild });
1209
- }
1210
- if (onRename) {
1211
- adminItems.push({ key: "rename", label: "Rename", icon: /* @__PURE__ */ jsx14(Pencil, { "aria-hidden": "true", className: "w-5 h-5" }), onClick: onRename });
1212
- }
1213
- if (onChangeParent) {
1214
- adminItems.push({ key: "change-parent", label: "Parent", icon: /* @__PURE__ */ jsx14(MoveUpRight, { "aria-hidden": "true", className: "w-5 h-5" }), onClick: onChangeParent });
1215
- }
1216
- if (onToggleSide) {
1217
- adminItems.push({
1218
- key: "toggle-side",
1219
- label: side === "pop" ? "Make core" : "Make pop",
1220
- icon: /* @__PURE__ */ jsx14(ArrowLeftRight, { "aria-hidden": "true", className: "w-5 h-5" }),
1221
- onClick: onToggleSide,
1222
- disabled: !!toggleSideDisabledReason,
1223
- title: toggleSideDisabledReason
1224
- });
1225
- }
1226
- for (const action of additionalActions?.(node) ?? []) {
1227
- items.push({
1228
- key: action.key,
1229
- label: action.label(node),
1230
- icon: action.icon(node),
1231
- onClick: (event) => action.onClick(event, node),
1232
- disabled: action.enabled ? !action.enabled(node) : false
1233
- });
1234
- }
1235
- if (items.length === 0 && (!shareTarget || !shareUrl) && adminItems.length === 0) return null;
1236
- const renderItems = (list) => list.map((item) => /* @__PURE__ */ jsxs10(
1237
- "button",
1238
- {
1239
- type: "button",
1240
- disabled: item.disabled,
1241
- title: item.title,
1242
- onClick: item.onClick,
1243
- 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",
1244
- children: [
1245
- /* @__PURE__ */ jsx14("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 }),
1246
- item.label
1247
- ]
1248
- },
1249
- item.key
1250
- ));
1251
- return /* @__PURE__ */ jsxs10("div", { className: "genre-action-band border-b border-[var(--color-border)]", children: [
1252
- (items.length > 0 || shareTarget && shareUrl) && /* @__PURE__ */ jsxs10("div", { className: "flex flex-wrap gap-1 px-3.5 py-3", children: [
1253
- renderItems(items),
1254
- shareTarget && shareUrl && /* @__PURE__ */ jsx14(ShareActionButton, { target: shareTarget, url: shareUrl })
1255
- ] }),
1256
- adminItems.length > 0 && /* @__PURE__ */ jsxs10(
1257
- "section",
1308
+ return /* @__PURE__ */ jsxs11(Fragment6, { children: [
1309
+ toolbar,
1310
+ /* @__PURE__ */ jsx15(GenreNodeActionBand, { ...band, genrePlaylist, playback }),
1311
+ /* @__PURE__ */ createElement(
1312
+ GenreDetailExtras,
1258
1313
  {
1259
- "aria-label": "Admin actions",
1260
- className: `px-3.5 py-3 ${items.length > 0 ? "border-t border-[var(--color-border)]" : ""}`,
1261
- children: [
1262
- /* @__PURE__ */ jsx14("h3", { className: "mb-2 text-xs font-semibold uppercase tracking-wide text-[var(--color-text-muted)]", children: "Admin" }),
1263
- /* @__PURE__ */ jsx14("div", { className: "flex flex-wrap gap-1", children: renderItems(adminItems) })
1264
- ]
1314
+ ...extras,
1315
+ key: genreUuid,
1316
+ family,
1317
+ genreUuid,
1318
+ genrePlaylist,
1319
+ wikidataId,
1320
+ scope,
1321
+ getBackendBaseUrl
1265
1322
  }
1266
1323
  )
1267
1324
  ] });
@@ -1270,14 +1327,14 @@ function GenreActionBand({
1270
1327
  // src/genre-tree/PanelEdgeTab.tsx
1271
1328
  import { ChevronLeft, ChevronRight } from "lucide-react";
1272
1329
  import { twMerge } from "tailwind-merge";
1273
- import { jsx as jsx15, jsxs as jsxs11 } from "react/jsx-runtime";
1330
+ import { jsx as jsx16, jsxs as jsxs12 } from "react/jsx-runtime";
1274
1331
  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)]";
1275
1332
  function PanelEdgeTab({ side, edge, center, controls, className: className2 }) {
1276
1333
  if (!edge && !center) return null;
1277
1334
  const isLeft = side === "left";
1278
1335
  const EdgeIcon = isLeft ? ChevronLeft : ChevronRight;
1279
1336
  const CenterIcon = isLeft ? ChevronRight : ChevronLeft;
1280
- const renderButton = (action, icon) => /* @__PURE__ */ jsx15(
1337
+ const renderButton = (action, icon) => /* @__PURE__ */ jsx16(
1281
1338
  "button",
1282
1339
  {
1283
1340
  type: "button",
@@ -1290,10 +1347,10 @@ function PanelEdgeTab({ side, edge, center, controls, className: className2 }) {
1290
1347
  children: icon
1291
1348
  }
1292
1349
  );
1293
- const edgeButton = edge && renderButton(edge, /* @__PURE__ */ jsx15(EdgeIcon, { "aria-hidden": "true", className: "h-5 w-5" }));
1294
- const centerButton = center && renderButton(center, /* @__PURE__ */ jsx15(CenterIcon, { "aria-hidden": "true", className: "h-5 w-5" }));
1350
+ const edgeButton = edge && renderButton(edge, /* @__PURE__ */ jsx16(EdgeIcon, { "aria-hidden": "true", className: "h-5 w-5" }));
1351
+ const centerButton = center && renderButton(center, /* @__PURE__ */ jsx16(CenterIcon, { "aria-hidden": "true", className: "h-5 w-5" }));
1295
1352
  const buttons = isLeft ? [edgeButton, centerButton] : [centerButton, edgeButton];
1296
- return /* @__PURE__ */ jsxs11(
1353
+ return /* @__PURE__ */ jsxs12(
1297
1354
  "div",
1298
1355
  {
1299
1356
  className: twMerge(
@@ -1303,20 +1360,33 @@ function PanelEdgeTab({ side, edge, center, controls, className: className2 }) {
1303
1360
  ),
1304
1361
  children: [
1305
1362
  buttons[0],
1306
- buttons[0] && buttons[1] && /* @__PURE__ */ jsx15("div", { role: "presentation", className: "h-px shrink-0 bg-[var(--color-border)]" }),
1363
+ buttons[0] && buttons[1] && /* @__PURE__ */ jsx16("div", { role: "presentation", className: "h-px shrink-0 bg-[var(--color-border)]" }),
1307
1364
  buttons[1]
1308
1365
  ]
1309
1366
  }
1310
1367
  );
1311
1368
  }
1312
1369
 
1370
+ // src/genre-tree/useLeftDetailPanelLayout.ts
1371
+ import { useEffect as useEffect4 } from "react";
1372
+ function useLeftDetailPanelLayout(isPanelShown, enabled = true) {
1373
+ useEffect4(() => {
1374
+ if (!enabled || isPanelShown) return void 0;
1375
+ const root = document.documentElement;
1376
+ root.style.setProperty("--layout-left-w", "0px");
1377
+ return () => {
1378
+ root.style.removeProperty("--layout-left-w");
1379
+ };
1380
+ }, [enabled, isPanelShown]);
1381
+ }
1382
+
1313
1383
  // src/genre-tree/ChangeParentPopup.tsx
1314
- import { useEffect as useEffect4, useMemo as useMemo6 } from "react";
1384
+ import { useEffect as useEffect5, useMemo as useMemo6 } from "react";
1315
1385
  import {
1316
1386
  getReparentCandidates,
1317
1387
  useUpdateGenre
1318
1388
  } from "@behindthemusictree/app-kit/genre-tree";
1319
- import { jsx as jsx16, jsxs as jsxs12 } from "react/jsx-runtime";
1389
+ import { jsx as jsx17, jsxs as jsxs13 } from "react/jsx-runtime";
1320
1390
  function ChangeParentPopup({
1321
1391
  genrePlaylist,
1322
1392
  genrePlaylists,
@@ -1344,19 +1414,19 @@ function ChangeParentPopup({
1344
1414
  }
1345
1415
  );
1346
1416
  };
1347
- useEffect4(() => {
1417
+ useEffect5(() => {
1348
1418
  const handleKeyDown = (event) => {
1349
1419
  if (event.key === "Escape") onClose();
1350
1420
  };
1351
1421
  window.addEventListener("keydown", handleKeyDown);
1352
1422
  return () => window.removeEventListener("keydown", handleKeyDown);
1353
1423
  }, [onClose]);
1354
- return /* @__PURE__ */ jsxs12(BasePopup_default, { title: `Change parent of ${genrePlaylist.name}`, onClose, showCancelButton: true, fitContent: true, children: [
1355
- /* @__PURE__ */ jsxs12("p", { className: "mb-3 text-sm text-[var(--color-text-muted)]", children: [
1424
+ return /* @__PURE__ */ jsxs13(BasePopup_default, { title: `Change parent of ${genrePlaylist.name}`, onClose, showCancelButton: true, fitContent: true, children: [
1425
+ /* @__PURE__ */ jsxs13("p", { className: "mb-3 text-sm text-[var(--color-text-muted)]", children: [
1356
1426
  "Current parent: ",
1357
1427
  genrePlaylist.parent?.name ?? "none"
1358
1428
  ] }),
1359
- /* @__PURE__ */ jsx16("div", { "aria-busy": isPending, className: isPending ? "pointer-events-none opacity-60" : void 0, children: /* @__PURE__ */ jsx16(
1429
+ /* @__PURE__ */ jsx17("div", { "aria-busy": isPending, className: isPending ? "pointer-events-none opacity-60" : void 0, children: /* @__PURE__ */ jsx17(
1360
1430
  GenreSearch,
1361
1431
  {
1362
1432
  genrePlaylists: candidates,
@@ -1364,7 +1434,7 @@ function ChangeParentPopup({
1364
1434
  placeholder: "Search a new parent\u2026"
1365
1435
  }
1366
1436
  ) }),
1367
- formErrors.length > 0 && /* @__PURE__ */ jsx16("p", { role: "alert", className: "mt-3 text-sm text-[var(--color-red-500)]", children: formErrors.map(({ message }) => message).join(" ") })
1437
+ formErrors.length > 0 && /* @__PURE__ */ jsx17("p", { role: "alert", className: "mt-3 text-sm text-[var(--color-red-500)]", children: formErrors.map(({ message }) => message).join(" ") })
1368
1438
  ] });
1369
1439
  }
1370
1440
 
@@ -1388,7 +1458,7 @@ import {
1388
1458
  SiYoutube
1389
1459
  } from "react-icons/si";
1390
1460
  import { Chip } from "@behindthemusictree/ui";
1391
- import { jsx as jsx17 } from "react/jsx-runtime";
1461
+ import { jsx as jsx18 } from "react/jsx-runtime";
1392
1462
  var LINK_KINDS = {
1393
1463
  official: { label: "Official website", Icon: Globe },
1394
1464
  wikipedia: { label: "Wikipedia", Icon: SiWikipedia2 },
@@ -1425,7 +1495,7 @@ function artistLinks(artist) {
1425
1495
  return links;
1426
1496
  }
1427
1497
  function ArtistLinks({ links }) {
1428
- return /* @__PURE__ */ jsx17("ul", { className: "flex flex-wrap gap-1.5", children: links.map(({ key, url, Icon, label }) => /* @__PURE__ */ jsx17("li", { children: /* @__PURE__ */ jsx17(Chip, { href: url, icon: /* @__PURE__ */ jsx17(Icon, { className: "h-3.5 w-3.5 shrink-0", "aria-hidden": "true" }), children: label }) }, key)) });
1498
+ return /* @__PURE__ */ jsx18("ul", { className: "flex flex-wrap gap-1.5", children: links.map(({ key, url, Icon, label }) => /* @__PURE__ */ jsx18("li", { children: /* @__PURE__ */ jsx18(Chip, { href: url, icon: /* @__PURE__ */ jsx18(Icon, { className: "h-3.5 w-3.5 shrink-0", "aria-hidden": "true" }), children: label }) }, key)) });
1429
1499
  }
1430
1500
 
1431
1501
  // src/genre-tree/artist/formatYears.ts
@@ -1436,7 +1506,7 @@ function formatYears(beginYear, endYear, ended) {
1436
1506
  }
1437
1507
 
1438
1508
  // src/genre-tree/artist/ArtistHeader.tsx
1439
- import { jsx as jsx18 } from "react/jsx-runtime";
1509
+ import { jsx as jsx19 } from "react/jsx-runtime";
1440
1510
  var countryNames = new Intl.DisplayNames(["en"], { type: "region" });
1441
1511
  function ArtistHeader({ artist }) {
1442
1512
  const fields = [];
@@ -1446,35 +1516,35 @@ function ArtistHeader({ artist }) {
1446
1516
  const years = formatYears(artist.beginYear, artist.endYear, artist.ended);
1447
1517
  if (years) fields.push({ label: "Active", value: years });
1448
1518
  const links = artistLinks(artist);
1449
- if (links.length > 0) fields.push({ label: "Links", value: /* @__PURE__ */ jsx18(ArtistLinks, { links }) });
1519
+ if (links.length > 0) fields.push({ label: "Links", value: /* @__PURE__ */ jsx19(ArtistLinks, { links }) });
1450
1520
  if (fields.length === 0) return null;
1451
- return /* @__PURE__ */ jsx18(FieldList, { items: fields });
1521
+ return /* @__PURE__ */ jsx19(FieldList, { items: fields });
1452
1522
  }
1453
1523
 
1454
1524
  // src/genre-tree/artist/ArtistPhoto.tsx
1455
- import { jsx as jsx19, jsxs as jsxs13 } from "react/jsx-runtime";
1525
+ import { jsx as jsx20, jsxs as jsxs14 } from "react/jsx-runtime";
1456
1526
  function ExternalLink({ href, children }) {
1457
- return /* @__PURE__ */ jsx19("a", { href, target: "_blank", rel: "noopener noreferrer", className: "underline-offset-2 hover:underline", children });
1527
+ return /* @__PURE__ */ jsx20("a", { href, target: "_blank", rel: "noopener noreferrer", className: "underline-offset-2 hover:underline", children });
1458
1528
  }
1459
1529
  function ArtistPhoto({ image, name }) {
1460
- return /* @__PURE__ */ jsxs13("figure", { className: "flex flex-col gap-1.5 px-6 py-2.5", children: [
1461
- /* @__PURE__ */ jsx19("img", { src: image.url, alt: name, className: "h-64 w-full object-contain bg-[var(--color-surface-muted)] rounded" }),
1462
- /* @__PURE__ */ jsxs13("figcaption", { className: "text-xs text-[var(--color-text-muted)]", children: [
1530
+ return /* @__PURE__ */ jsxs14("figure", { className: "flex flex-col gap-1.5 px-6 py-2.5", children: [
1531
+ /* @__PURE__ */ jsx20("img", { src: image.url, alt: name, className: "h-64 w-full object-contain bg-[var(--color-surface-muted)] rounded" }),
1532
+ /* @__PURE__ */ jsxs14("figcaption", { className: "text-xs text-[var(--color-text-muted)]", children: [
1463
1533
  "Photo: ",
1464
- /* @__PURE__ */ jsx19(ExternalLink, { href: image.pageUrl, children: image.author ?? "Wikimedia Commons" }),
1534
+ /* @__PURE__ */ jsx20(ExternalLink, { href: image.pageUrl, children: image.author ?? "Wikimedia Commons" }),
1465
1535
  ",",
1466
1536
  " ",
1467
- image.licenseUrl ? /* @__PURE__ */ jsx19(ExternalLink, { href: image.licenseUrl, children: image.license }) : image.license
1537
+ image.licenseUrl ? /* @__PURE__ */ jsx20(ExternalLink, { href: image.licenseUrl, children: image.license }) : image.license
1468
1538
  ] })
1469
1539
  ] });
1470
1540
  }
1471
1541
 
1472
1542
  // src/genre-tree/artist/ArtistMembers.tsx
1473
1543
  import { PanelGroup, PanelSection } from "@behindthemusictree/ui";
1474
- import { jsx as jsx20, jsxs as jsxs14 } from "react/jsx-runtime";
1544
+ import { jsx as jsx21, jsxs as jsxs15 } from "react/jsx-runtime";
1475
1545
  function MembershipList({ title, memberships, renderArtistLink }) {
1476
1546
  if (memberships.length === 0) return null;
1477
- return /* @__PURE__ */ jsx20(PanelGroup, { title, children: /* @__PURE__ */ jsx20("ul", { children: memberships.map((membership, index) => {
1547
+ return /* @__PURE__ */ jsx21(PanelGroup, { title, children: /* @__PURE__ */ jsx21("ul", { children: memberships.map((membership, index) => {
1478
1548
  const details = [
1479
1549
  formatYears(membership.beginYear, membership.endYear, membership.ended),
1480
1550
  ...membership.roles
@@ -1482,9 +1552,9 @@ function MembershipList({ title, memberships, renderArtistLink }) {
1482
1552
  const detailsText = details.filter(Boolean).join(" \xB7 ");
1483
1553
  return (
1484
1554
  // A member can have several periods in the same group.
1485
- /* @__PURE__ */ jsxs14("li", { className: "truncate py-0.5", title: detailsText || void 0, children: [
1486
- /* @__PURE__ */ jsx20("span", { className: "font-medium text-[var(--color-text)]", children: renderArtistLink(membership.artist) }),
1487
- detailsText && /* @__PURE__ */ jsxs14("span", { className: "text-[var(--color-text-muted)]", children: [
1555
+ /* @__PURE__ */ jsxs15("li", { className: "truncate py-0.5", title: detailsText || void 0, children: [
1556
+ /* @__PURE__ */ jsx21("span", { className: "font-medium text-[var(--color-text)]", children: renderArtistLink(membership.artist) }),
1557
+ detailsText && /* @__PURE__ */ jsxs15("span", { className: "text-[var(--color-text-muted)]", children: [
1488
1558
  " \u2014 ",
1489
1559
  detailsText
1490
1560
  ] })
@@ -1495,9 +1565,9 @@ function MembershipList({ title, memberships, renderArtistLink }) {
1495
1565
  function ArtistMembers({ title, memberships, renderArtistLink }) {
1496
1566
  if (memberships.length === 0) return null;
1497
1567
  const isCurrent = (membership) => !membership.ended && membership.endYear === null;
1498
- return /* @__PURE__ */ jsxs14(PanelSection, { title, children: [
1499
- /* @__PURE__ */ jsx20(MembershipList, { title: "Current", memberships: memberships.filter(isCurrent), renderArtistLink }),
1500
- /* @__PURE__ */ jsx20(
1568
+ return /* @__PURE__ */ jsxs15(PanelSection, { title, children: [
1569
+ /* @__PURE__ */ jsx21(MembershipList, { title: "Current", memberships: memberships.filter(isCurrent), renderArtistLink }),
1570
+ /* @__PURE__ */ jsx21(
1501
1571
  MembershipList,
1502
1572
  {
1503
1573
  title: "Past",
@@ -1510,7 +1580,7 @@ function ArtistMembers({ title, memberships, renderArtistLink }) {
1510
1580
 
1511
1581
  // src/genre-tree/artist/ArtistDiscography.tsx
1512
1582
  import { PanelGroup as PanelGroup2, PanelSection as PanelSection2 } from "@behindthemusictree/ui";
1513
- import { jsx as jsx21, jsxs as jsxs15 } from "react/jsx-runtime";
1583
+ import { jsx as jsx22, jsxs as jsxs16 } from "react/jsx-runtime";
1514
1584
  var GROUPS = ["Albums", "EPs", "Singles", "Live", "Compilations", "Other"];
1515
1585
  var PRIMARY_GROUPS = { album: "Albums", ep: "EPs", single: "Singles" };
1516
1586
  function discographyGroup({ primaryType, secondaryTypes }) {
@@ -1530,12 +1600,12 @@ function byYear(a, b) {
1530
1600
  function ArtistDiscography({ albums }) {
1531
1601
  if (albums.length === 0) return null;
1532
1602
  const sorted = [...albums].sort(byYear);
1533
- return /* @__PURE__ */ jsx21(PanelSection2, { title: "Discography", children: GROUPS.map((group) => {
1603
+ return /* @__PURE__ */ jsx22(PanelSection2, { title: "Discography", children: GROUPS.map((group) => {
1534
1604
  const groupAlbums = sorted.filter((album) => discographyGroup(album) === group);
1535
1605
  if (groupAlbums.length === 0) return null;
1536
- return /* @__PURE__ */ jsx21(PanelGroup2, { title: group, children: /* @__PURE__ */ jsx21("ul", { children: groupAlbums.map((album) => /* @__PURE__ */ jsxs15("li", { className: "grid grid-cols-[2.5rem_minmax(0,1fr)] gap-2 py-0.5", children: [
1537
- /* @__PURE__ */ jsx21("span", { className: "tabular-nums text-[var(--color-text-muted)]", children: album.year ?? "\u2014" }),
1538
- /* @__PURE__ */ jsx21("span", { className: "truncate text-[var(--color-text)]", title: album.name, children: album.name })
1606
+ return /* @__PURE__ */ jsx22(PanelGroup2, { title: group, children: /* @__PURE__ */ jsx22("ul", { children: groupAlbums.map((album) => /* @__PURE__ */ jsxs16("li", { className: "grid grid-cols-[2.5rem_minmax(0,1fr)] gap-2 py-0.5", children: [
1607
+ /* @__PURE__ */ jsx22("span", { className: "tabular-nums text-[var(--color-text-muted)]", children: album.year ?? "\u2014" }),
1608
+ /* @__PURE__ */ jsx22("span", { className: "truncate text-[var(--color-text)]", title: album.name, children: album.name })
1539
1609
  ] }, album.uuid)) }) }, group);
1540
1610
  }) });
1541
1611
  }
@@ -1545,7 +1615,7 @@ import { useState as useState4 } from "react";
1545
1615
  import { keepPreviousData } from "@tanstack/react-query";
1546
1616
  import { Play as Play2 } from "lucide-react";
1547
1617
  import { Pagination, PanelSection as PanelSection3, Skeleton as Skeleton3 } from "@behindthemusictree/ui";
1548
- import { usePlayer as usePlayer7 } from "@behindthemusictree/app-kit/player";
1618
+ import { usePlayer as usePlayer8 } from "@behindthemusictree/app-kit/player";
1549
1619
  import {
1550
1620
  SongDetailedSchema as SongDetailedSchema2,
1551
1621
  libraryEndpoints as libraryEndpoints2,
@@ -1555,35 +1625,35 @@ import {
1555
1625
  import { PaginatedResponseSchema as PaginatedResponseSchema2, useFetchWrapper as useFetchWrapper2, useQueryWithParse } from "@behindthemusictree/app-kit/transport";
1556
1626
 
1557
1627
  // src/genre-tree/track-list-sidebar/TrackItem.tsx
1558
- import { usePlayer as usePlayer6 } from "@behindthemusictree/app-kit/player";
1628
+ import { usePlayer as usePlayer7 } from "@behindthemusictree/app-kit/player";
1559
1629
  import { useTrackList as useTrackList3 } from "@behindthemusictree/app-kit/genre-tree";
1560
1630
 
1561
1631
  // src/genre-tree/TrackPositionPlayPause.tsx
1562
1632
  import { FaPlay as FaPlay2, FaPause as FaPause2 } from "react-icons/fa";
1563
- import { usePlayer as usePlayer5 } from "@behindthemusictree/app-kit/player";
1633
+ import { usePlayer as usePlayer6 } from "@behindthemusictree/app-kit/player";
1564
1634
  import { PlayStates as PlayStates2 } from "@behindthemusictree/app-kit/player";
1565
- import { jsx as jsx22, jsxs as jsxs16 } from "react/jsx-runtime";
1635
+ import { jsx as jsx23, jsxs as jsxs17 } from "react/jsx-runtime";
1566
1636
  function TrackPositionPlayPause({
1567
1637
  position,
1568
1638
  uuid,
1569
1639
  handlePlayPauseClick,
1570
1640
  isSelected = false
1571
1641
  }) {
1572
- const { playerTrackObject, playbackConfirmedTrackId, playState, isLoading: isPlayerLoading } = usePlayer5();
1642
+ const { playerTrackObject, playbackConfirmedTrackId, playState, isLoading: isPlayerLoading } = usePlayer6();
1573
1643
  const isThisTrack = playerTrackObject?.track.id === uuid;
1574
1644
  const isConfirmedPlayable = playbackConfirmedTrackId === uuid;
1575
1645
  const isAwaitingPlayback = (isSelected || isThisTrack) && !isConfirmedPlayable && !(isThisTrack && playerTrackObject?.loadError) && (!isThisTrack || isPlayerLoading || playState === PlayStates2.LOADING || playState === PlayStates2.PLAYING);
1576
1646
  const isPlaying = isThisTrack && isConfirmedPlayable && playState === PlayStates2.PLAYING;
1577
1647
  const isPaused = isThisTrack && isConfirmedPlayable && playState === PlayStates2.PAUSED;
1578
1648
  const isCurrentTrackActive = isPlaying || isPaused;
1579
- return /* @__PURE__ */ jsxs16(
1649
+ return /* @__PURE__ */ jsxs17(
1580
1650
  "div",
1581
1651
  {
1582
1652
  className: "track-position-play-pause relative flex items-center justify-center gap-2 text-lg w-16 cursor-pointer",
1583
1653
  style: { minWidth: "4rem", maxWidth: "4rem" },
1584
1654
  onClick: handlePlayPauseClick,
1585
1655
  children: [
1586
- /* @__PURE__ */ jsx22("div", { className: "track-position-play-pause__status group-hover:hidden flex items-center justify-center", children: isAwaitingPlayback ? /* @__PURE__ */ jsx22("span", { className: "track-position-play-pause__spinner", role: "status", "aria-label": "Loading track" }) : isCurrentTrackActive ? /* @__PURE__ */ jsxs16(
1656
+ /* @__PURE__ */ jsx23("div", { className: "track-position-play-pause__status group-hover:hidden flex items-center justify-center", children: isAwaitingPlayback ? /* @__PURE__ */ jsx23("span", { className: "track-position-play-pause__spinner", role: "status", "aria-label": "Loading track" }) : isCurrentTrackActive ? /* @__PURE__ */ jsxs17(
1587
1657
  "span",
1588
1658
  {
1589
1659
  className: "track-position-play-pause__bars",
@@ -1591,18 +1661,18 @@ function TrackPositionPlayPause({
1591
1661
  role: "img",
1592
1662
  "aria-label": isPlaying ? "Playing" : "Paused",
1593
1663
  children: [
1594
- /* @__PURE__ */ jsx22("span", { className: "track-position-play-pause__bar" }),
1595
- /* @__PURE__ */ jsx22("span", { className: "track-position-play-pause__bar" }),
1596
- /* @__PURE__ */ jsx22("span", { className: "track-position-play-pause__bar" })
1664
+ /* @__PURE__ */ jsx23("span", { className: "track-position-play-pause__bar" }),
1665
+ /* @__PURE__ */ jsx23("span", { className: "track-position-play-pause__bar" }),
1666
+ /* @__PURE__ */ jsx23("span", { className: "track-position-play-pause__bar" })
1597
1667
  ]
1598
1668
  }
1599
1669
  ) : position }),
1600
- /* @__PURE__ */ jsx22(
1670
+ /* @__PURE__ */ jsx23(
1601
1671
  "div",
1602
1672
  {
1603
1673
  className: `hidden group-hover:flex items-center justify-center ${isCurrentTrackActive ? "" : "absolute inset-0"}`,
1604
1674
  "aria-hidden": "true",
1605
- children: isAwaitingPlayback ? /* @__PURE__ */ jsx22("span", { className: "track-position-play-pause__spinner" }) : isPlaying ? /* @__PURE__ */ jsx22(FaPause2, {}) : /* @__PURE__ */ jsx22(FaPlay2, {})
1675
+ children: isAwaitingPlayback ? /* @__PURE__ */ jsx23("span", { className: "track-position-play-pause__spinner" }) : isPlaying ? /* @__PURE__ */ jsx23(FaPause2, {}) : /* @__PURE__ */ jsx23(FaPlay2, {})
1606
1676
  }
1607
1677
  )
1608
1678
  ]
@@ -1613,7 +1683,7 @@ function TrackPositionPlayPause({
1613
1683
  // src/genre-tree/track-list-sidebar/TrackItem.tsx
1614
1684
  import { isSong as isSong2, songUnplayableReason as songUnplayableReason2 } from "@behindthemusictree/app-kit/genre-tree";
1615
1685
  import { ArtistNames as ArtistNames2 } from "@behindthemusictree/app-kit/genre-tree";
1616
- import { jsx as jsx23, jsxs as jsxs17 } from "react/jsx-runtime";
1686
+ import { jsx as jsx24, jsxs as jsxs18 } from "react/jsx-runtime";
1617
1687
  function TrackItem({
1618
1688
  track,
1619
1689
  position,
@@ -1621,7 +1691,7 @@ function TrackItem({
1621
1691
  renderActions,
1622
1692
  renderArtistLink
1623
1693
  }) {
1624
- const { handlePlayPauseAction, playerTrackObject, loadTrackForPlayer } = usePlayer6();
1694
+ const { handlePlayPauseAction, playerTrackObject, loadTrackForPlayer } = usePlayer7();
1625
1695
  const { trackList, selectedTrack, toTrackAtPosition, runtimeUnplayableTrackIds } = useTrackList3();
1626
1696
  const scope = trackList?.origin?.scope ?? null;
1627
1697
  const isCurrentTrack = playerTrackObject?.track.id === track.uuid;
@@ -1636,12 +1706,12 @@ function TrackItem({
1636
1706
  loadTrackForPlayer(track.uuid);
1637
1707
  }
1638
1708
  };
1639
- return /* @__PURE__ */ jsxs17(
1709
+ return /* @__PURE__ */ jsxs18(
1640
1710
  "div",
1641
1711
  {
1642
1712
  className: `track-item flex h-14 text-[var(--color-text)] ${isUnplayable ? "opacity-50" : "hover:bg-[var(--color-surface-muted)] group"}`,
1643
1713
  children: [
1644
- isUnplayable ? /* @__PURE__ */ jsx23(
1714
+ isUnplayable ? /* @__PURE__ */ jsx24(
1645
1715
  "div",
1646
1716
  {
1647
1717
  className: "flex items-center justify-center text-lg w-16",
@@ -1649,7 +1719,7 @@ function TrackItem({
1649
1719
  "aria-disabled": "true",
1650
1720
  children: position
1651
1721
  }
1652
- ) : /* @__PURE__ */ jsx23(
1722
+ ) : /* @__PURE__ */ jsx24(
1653
1723
  TrackPositionPlayPause,
1654
1724
  {
1655
1725
  position,
@@ -1658,12 +1728,12 @@ function TrackItem({
1658
1728
  isSelected: selectedTrack?.uuid === track.uuid
1659
1729
  }
1660
1730
  ),
1661
- /* @__PURE__ */ jsxs17("div", { className: "title-artist-container flex flex-col items-start justify-center min-w-0 flex-1", children: [
1662
- /* @__PURE__ */ jsx23("div", { className: "flex max-w-full items-center gap-2", children: /* @__PURE__ */ jsx23("div", { className: "title text-lg font-bold text-[var(--color-text)] max-w-full truncate", children: isSong2(track) ? /* @__PURE__ */ jsx23(MusicBrainzRecordingLink, { song: track, className: "hover:underline focus-visible:underline", children: track.title }) : track.title }) }),
1663
- track.artists && track.artists.length > 0 ? /* @__PURE__ */ jsx23("div", { className: "artist text-base max-w-full truncate text-[var(--color-text-muted)]", children: /* @__PURE__ */ jsx23(ArtistNames2, { artists: track.artists, renderArtistLink }) }) : ""
1731
+ /* @__PURE__ */ jsxs18("div", { className: "title-artist-container flex flex-col items-start justify-center min-w-0 flex-1", children: [
1732
+ /* @__PURE__ */ jsx24("div", { className: "flex max-w-full items-center gap-2", children: /* @__PURE__ */ jsx24("div", { className: "title text-lg font-bold text-[var(--color-text)] max-w-full truncate", children: isSong2(track) ? /* @__PURE__ */ jsx24(MusicBrainzRecordingLink, { song: track, className: "hover:underline focus-visible:underline", children: track.title }) : track.title }) }),
1733
+ track.artists && track.artists.length > 0 ? /* @__PURE__ */ jsx24("div", { className: "artist text-base max-w-full truncate text-[var(--color-text-muted)]", children: /* @__PURE__ */ jsx24(ArtistNames2, { artists: track.artists, renderArtistLink }) }) : ""
1664
1734
  ] }),
1665
- /* @__PURE__ */ jsx23("div", { className: "genre-name-container flex items-center justify-start w-[21.6%] min-w-0 shrink-0", children: track.genre ? /* @__PURE__ */ jsx23("div", { className: "genre-name max-w-full whitespace-normal break-words p-1 text-xs font-bold", children: track.genre.name }) : "" }),
1666
- renderDuration && /* @__PURE__ */ jsx23(
1735
+ /* @__PURE__ */ jsx24("div", { className: "genre-name-container flex items-center justify-start w-[21.6%] min-w-0 shrink-0", children: track.genre ? /* @__PURE__ */ jsx24("div", { className: "genre-name max-w-full whitespace-normal break-words p-1 text-xs font-bold", children: track.genre.name }) : "" }),
1736
+ renderDuration && /* @__PURE__ */ jsx24(
1667
1737
  "div",
1668
1738
  {
1669
1739
  className: "duration flex text-base w-16 items-center justify-center",
@@ -1671,14 +1741,14 @@ function TrackItem({
1671
1741
  children: renderDuration(track)
1672
1742
  }
1673
1743
  ),
1674
- renderActions && /* @__PURE__ */ jsx23("div", { className: "edit flex text-base w-6 items-center justify-center mr-2 cursor-pointer", children: renderActions(track) })
1744
+ renderActions && /* @__PURE__ */ jsx24("div", { className: "edit flex text-base w-6 items-center justify-center mr-2 cursor-pointer", children: renderActions(track) })
1675
1745
  ]
1676
1746
  }
1677
1747
  );
1678
1748
  }
1679
1749
 
1680
1750
  // src/genre-tree/artist/ArtistSongs.tsx
1681
- import { Fragment as Fragment6, jsx as jsx24, jsxs as jsxs18 } from "react/jsx-runtime";
1751
+ import { Fragment as Fragment7, jsx as jsx25, jsxs as jsxs19 } from "react/jsx-runtime";
1682
1752
  var SongPageSchema2 = PaginatedResponseSchema2(SongDetailedSchema2);
1683
1753
  var SORTS = [
1684
1754
  { ordering: "title", label: "Title" },
@@ -1689,7 +1759,7 @@ function ArtistSongs({ artist, getBackendBaseUrl, renderArtistLink }) {
1689
1759
  const [ordering, setOrdering] = useState4("title");
1690
1760
  const { fetch } = useFetchWrapper2(getBackendBaseUrl);
1691
1761
  const { playNewTrackListFromArtist } = useTrackList4();
1692
- const { playerTrackObject } = usePlayer7();
1762
+ const { playerTrackObject } = usePlayer8();
1693
1763
  const { data, isPending, isError } = useQueryWithParse({
1694
1764
  queryKey: ["referenceSongs", "artist", artist.uuid, ordering, page],
1695
1765
  queryFn: () => fetch(libraryEndpoints2.reference.songs.list(), true, false, void 0, {
@@ -1711,21 +1781,21 @@ function ArtistSongs({ artist, getBackendBaseUrl, renderArtistLink }) {
1711
1781
  event.stopPropagation();
1712
1782
  void play(song.uuid);
1713
1783
  };
1714
- return /* @__PURE__ */ jsx24(PanelSection3, { title: "Songs", children: isPending ? /* @__PURE__ */ jsx24(Skeleton3, { className: "h-14", "aria-busy": "true" }) : isError ? /* @__PURE__ */ jsx24("p", { className: "text-red-600", children: "Could not load songs." }) : data.results.length === 0 ? /* @__PURE__ */ jsx24("p", { className: "text-[var(--color-text-muted)]", children: "No songs yet." }) : /* @__PURE__ */ jsxs18(Fragment6, { children: [
1715
- /* @__PURE__ */ jsxs18("div", { className: "flex flex-wrap items-center justify-between gap-2 mb-2", children: [
1716
- /* @__PURE__ */ jsxs18(
1784
+ return /* @__PURE__ */ jsx25(PanelSection3, { title: "Songs", children: isPending ? /* @__PURE__ */ jsx25(Skeleton3, { className: "h-14", "aria-busy": "true" }) : isError ? /* @__PURE__ */ jsx25("p", { className: "text-red-600", children: "Could not load songs." }) : data.results.length === 0 ? /* @__PURE__ */ jsx25("p", { className: "text-[var(--color-text-muted)]", children: "No songs yet." }) : /* @__PURE__ */ jsxs19(Fragment7, { children: [
1785
+ /* @__PURE__ */ jsxs19("div", { className: "flex flex-wrap items-center justify-between gap-2 mb-2", children: [
1786
+ /* @__PURE__ */ jsxs19(
1717
1787
  "button",
1718
1788
  {
1719
1789
  type: "button",
1720
1790
  onClick: () => void play(),
1721
1791
  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",
1722
1792
  children: [
1723
- /* @__PURE__ */ jsx24(Play2, { className: "h-3.5 w-3.5", "aria-hidden": "true" }),
1793
+ /* @__PURE__ */ jsx25(Play2, { className: "h-3.5 w-3.5", "aria-hidden": "true" }),
1724
1794
  "Play all"
1725
1795
  ]
1726
1796
  }
1727
1797
  ),
1728
- /* @__PURE__ */ jsx24("div", { role: "group", "aria-label": "Sort songs by", className: "flex gap-1 text-xs", children: SORTS.map((sort) => /* @__PURE__ */ jsx24(
1798
+ /* @__PURE__ */ jsx25("div", { role: "group", "aria-label": "Sort songs by", className: "flex gap-1 text-xs", children: SORTS.map((sort) => /* @__PURE__ */ jsx25(
1729
1799
  "button",
1730
1800
  {
1731
1801
  type: "button",
@@ -1740,7 +1810,7 @@ function ArtistSongs({ artist, getBackendBaseUrl, renderArtistLink }) {
1740
1810
  sort.ordering
1741
1811
  )) })
1742
1812
  ] }),
1743
- /* @__PURE__ */ jsx24("ul", { className: "overflow-hidden mb-2 bg-zinc-950 rounded-lg", children: data.results.map((song, index) => /* @__PURE__ */ jsx24("li", { onClickCapture: (event) => handleRowClickCapture(event, song), children: /* @__PURE__ */ jsx24(
1813
+ /* @__PURE__ */ jsx25("ul", { className: "overflow-hidden mb-2 bg-zinc-950 rounded-lg", children: data.results.map((song, index) => /* @__PURE__ */ jsx25("li", { onClickCapture: (event) => handleRowClickCapture(event, song), children: /* @__PURE__ */ jsx25(
1744
1814
  TrackItem,
1745
1815
  {
1746
1816
  track: song,
@@ -1748,12 +1818,12 @@ function ArtistSongs({ artist, getBackendBaseUrl, renderArtistLink }) {
1748
1818
  renderArtistLink
1749
1819
  }
1750
1820
  ) }, song.uuid)) }),
1751
- /* @__PURE__ */ jsx24(Pagination, { currentPage: page, totalPages: data.totalPages, onPageChange: setPage })
1821
+ /* @__PURE__ */ jsx25(Pagination, { currentPage: page, totalPages: data.totalPages, onPageChange: setPage })
1752
1822
  ] }) });
1753
1823
  }
1754
1824
 
1755
1825
  // src/genre-tree/artist/useArtistPanel.tsx
1756
- import { Fragment as Fragment7, jsx as jsx25, jsxs as jsxs19 } from "react/jsx-runtime";
1826
+ import { Fragment as Fragment8, jsx as jsx26, jsxs as jsxs20 } from "react/jsx-runtime";
1757
1827
  function useArtistPanel({
1758
1828
  uuid,
1759
1829
  scope,
@@ -1773,26 +1843,26 @@ function useArtistPanel({
1773
1843
  return {
1774
1844
  ...panel,
1775
1845
  title: "Artist",
1776
- content: /* @__PURE__ */ jsxs19("div", { "aria-busy": "true", className: "flex flex-col gap-2.5 px-6 py-2.5", children: [
1777
- /* @__PURE__ */ jsx25(Skeleton4, { className: "h-3 w-16" }),
1778
- /* @__PURE__ */ jsx25(Skeleton4, { className: "h-4 w-32" }),
1779
- /* @__PURE__ */ jsx25(Skeleton4, { className: "h-3 w-16" }),
1780
- /* @__PURE__ */ jsx25(Skeleton4, { className: "h-4 w-24" }),
1781
- /* @__PURE__ */ jsx25(Skeleton4, { className: "h-16 mt-2.5" }),
1782
- /* @__PURE__ */ jsx25(Skeleton4, { className: "h-16" })
1846
+ content: /* @__PURE__ */ jsxs20("div", { "aria-busy": "true", className: "flex flex-col gap-2.5 px-6 py-2.5", children: [
1847
+ /* @__PURE__ */ jsx26(Skeleton4, { className: "h-3 w-16" }),
1848
+ /* @__PURE__ */ jsx26(Skeleton4, { className: "h-4 w-32" }),
1849
+ /* @__PURE__ */ jsx26(Skeleton4, { className: "h-3 w-16" }),
1850
+ /* @__PURE__ */ jsx26(Skeleton4, { className: "h-4 w-24" }),
1851
+ /* @__PURE__ */ jsx26(Skeleton4, { className: "h-16 mt-2.5" }),
1852
+ /* @__PURE__ */ jsx26(Skeleton4, { className: "h-16" })
1783
1853
  ] })
1784
1854
  };
1785
1855
  case "not-found":
1786
1856
  return {
1787
1857
  ...panel,
1788
1858
  title: "Artist not found",
1789
- content: /* @__PURE__ */ jsx25("p", { className: "px-6 py-2.5 text-[var(--color-text-muted)]", children: "This artist does not exist." })
1859
+ content: /* @__PURE__ */ jsx26("p", { className: "px-6 py-2.5 text-[var(--color-text-muted)]", children: "This artist does not exist." })
1790
1860
  };
1791
1861
  case "error":
1792
1862
  return {
1793
1863
  ...panel,
1794
1864
  title: "Artist",
1795
- content: /* @__PURE__ */ jsx25("p", { className: "px-6 py-2.5 text-red-600", children: "Could not load this artist." })
1865
+ content: /* @__PURE__ */ jsx26("p", { className: "px-6 py-2.5 text-red-600", children: "Could not load this artist." })
1796
1866
  };
1797
1867
  case "success": {
1798
1868
  const { artist } = result;
@@ -1801,13 +1871,13 @@ function useArtistPanel({
1801
1871
  return {
1802
1872
  ...panel,
1803
1873
  title: artist.name,
1804
- content: /* @__PURE__ */ jsxs19(Fragment7, { children: [
1805
- shareUrl && /* @__PURE__ */ jsx25("div", { className: "genre-action-band border-b border-[var(--color-border)]", children: /* @__PURE__ */ jsx25("div", { className: "flex flex-wrap gap-1 px-3.5 py-3", children: shareTarget && /* @__PURE__ */ jsx25(ShareActionButton, { target: shareTarget, url: shareUrl }) }) }),
1806
- artist.image && /* @__PURE__ */ jsx25(ArtistPhoto, { image: artist.image, name: artist.name }),
1807
- /* @__PURE__ */ jsx25(ArtistHeader, { artist }),
1808
- /* @__PURE__ */ jsx25(ArtistMembers, { title: "Members", memberships: artist.members, renderArtistLink }),
1809
- /* @__PURE__ */ jsx25(ArtistMembers, { title: "Member of", memberships: artist.memberOf, renderArtistLink }),
1810
- artist.tracksCount > 0 && /* @__PURE__ */ jsx25(
1874
+ content: /* @__PURE__ */ jsxs20(Fragment8, { children: [
1875
+ shareUrl && /* @__PURE__ */ jsx26("div", { className: "genre-action-band border-b border-[var(--color-border)]", children: /* @__PURE__ */ jsx26("div", { className: "flex flex-wrap gap-1 px-3.5 py-3", children: shareTarget && /* @__PURE__ */ jsx26(ShareActionButton, { target: shareTarget, url: shareUrl }) }) }),
1876
+ artist.image && /* @__PURE__ */ jsx26(ArtistPhoto, { image: artist.image, name: artist.name }),
1877
+ /* @__PURE__ */ jsx26(ArtistHeader, { artist }),
1878
+ /* @__PURE__ */ jsx26(ArtistMembers, { title: "Members", memberships: artist.members, renderArtistLink }),
1879
+ /* @__PURE__ */ jsx26(ArtistMembers, { title: "Member of", memberships: artist.memberOf, renderArtistLink }),
1880
+ artist.tracksCount > 0 && /* @__PURE__ */ jsx26(
1811
1881
  ArtistSongs,
1812
1882
  {
1813
1883
  artist: { uuid: artist.uuid, name: artist.name },
@@ -1816,7 +1886,7 @@ function useArtistPanel({
1816
1886
  }
1817
1887
  )
1818
1888
  ] }),
1819
- extraContent: artist.albums.length > 0 ? /* @__PURE__ */ jsx25(ArtistDiscography, { albums: artist.albums }) : void 0
1889
+ extraContent: artist.albums.length > 0 ? /* @__PURE__ */ jsx26(ArtistDiscography, { albums: artist.albums }) : void 0
1820
1890
  };
1821
1891
  }
1822
1892
  }
@@ -1825,27 +1895,27 @@ function useArtistPanel({
1825
1895
  // src/genre-tree/SizeMetricToggle.tsx
1826
1896
  import { MdMusicNote, MdVisibility } from "react-icons/md";
1827
1897
  import { SegmentedControl } from "@behindthemusictree/ui";
1828
- import { jsx as jsx26 } from "react/jsx-runtime";
1898
+ import { jsx as jsx27 } from "react/jsx-runtime";
1829
1899
  function SizeMetricToggle({
1830
1900
  value,
1831
1901
  onChange
1832
1902
  }) {
1833
- return /* @__PURE__ */ jsx26(
1903
+ return /* @__PURE__ */ jsx27(
1834
1904
  SegmentedControl,
1835
1905
  {
1836
1906
  "aria-label": "Size nodes by",
1837
1907
  value,
1838
1908
  onChange,
1839
1909
  options: [
1840
- { value: "songs", label: /* @__PURE__ */ jsx26(MdMusicNote, { size: 18, "aria-hidden": true }), "aria-label": "Size by songs", title: "Node size by song count" },
1841
- { value: "views", label: /* @__PURE__ */ jsx26(MdVisibility, { size: 18, "aria-hidden": true }), "aria-label": "Size by views", title: "Node size by song views" }
1910
+ { value: "songs", label: /* @__PURE__ */ jsx27(MdMusicNote, { size: 18, "aria-hidden": true }), "aria-label": "Size by songs", title: "Node size by song count" },
1911
+ { value: "views", label: /* @__PURE__ */ jsx27(MdVisibility, { size: 18, "aria-hidden": true }), "aria-label": "Size by views", title: "Node size by song views" }
1842
1912
  ]
1843
1913
  }
1844
1914
  );
1845
1915
  }
1846
1916
 
1847
1917
  // src/genre-tree/GenreTreeView.tsx
1848
- import { Fragment as Fragment8, jsx as jsx27, jsxs as jsxs20 } from "react/jsx-runtime";
1918
+ import { Fragment as Fragment9, jsx as jsx28, jsxs as jsxs21 } from "react/jsx-runtime";
1849
1919
  var PREVIOUS_BUTTON_CSS = `
1850
1920
  .genre-tree-view .search-overlay .gtv-info-panel-previous { display: none; }
1851
1921
  .genre-tree-view:has(.search-overlay .gtv-info-panel-previous) .genre-search input { padding-left: 2.75rem; }
@@ -1855,27 +1925,6 @@ var PREVIOUS_BUTTON_CSS = `
1855
1925
  .genre-search-previous { animation: genre-tree-view-previous-in 200ms ease-out; }
1856
1926
  }
1857
1927
  `;
1858
- function GenreNodeActionBand({ genrePlaylist, scope, playback, ...bandProps }) {
1859
- const { isPlaying, setIsPlaying } = usePlayer8();
1860
- const isCurrentTrackList = playback.playingNodeId === genrePlaylist.uuid;
1861
- const handlePlayPause = () => {
1862
- if (isCurrentTrackList) {
1863
- setIsPlaying(!isPlaying);
1864
- return;
1865
- }
1866
- playback.play(genrePlaylist).catch((error) => {
1867
- console.error("Failed to load genre playlist tracks:", error);
1868
- });
1869
- };
1870
- return /* @__PURE__ */ jsx27(
1871
- GenreActionBand,
1872
- {
1873
- ...bandProps,
1874
- onPlayPause: genrePlaylist.tracksCount > 0 ? handlePlayPause : void 0,
1875
- isPlaying: isCurrentTrackList && isPlaying
1876
- }
1877
- );
1878
- }
1879
1928
  var NO_POP_ROOT_TITLE = "This genre tree has no 'Mainstream Pop' root yet";
1880
1929
  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]";
1881
1930
  function GenreTreeView({
@@ -1889,6 +1938,7 @@ function GenreTreeView({
1889
1938
  selectedNodeId: controlledSelectedNodeId,
1890
1939
  onSelectedNodeIdChange,
1891
1940
  defaultViewport,
1941
+ viewportRestore,
1892
1942
  onViewportChange,
1893
1943
  readOnly = false,
1894
1944
  className: className2,
@@ -1897,7 +1947,6 @@ function GenreTreeView({
1897
1947
  allowWheelRotation: controlledAllowWheelRotation,
1898
1948
  criteriaOverviewSchema,
1899
1949
  renderGenreDetailExtras,
1900
- controlsOrientation = "horizontal",
1901
1950
  floatingControlsLeading,
1902
1951
  sizeMetric: controlledSizeMetric,
1903
1952
  onSizeMetricChange,
@@ -1967,6 +2016,16 @@ function GenreTreeView({
1967
2016
  criteriaOverviewSchema,
1968
2017
  artistUuid
1969
2018
  });
2019
+ const restoreRevision = viewportRestore?.revision;
2020
+ const [stackedRestoreOwner, setStackedRestoreOwner] = useState5({ revision: restoreRevision, rootUuid: selectedGenrePlaylist?.root.uuid });
2021
+ let restoreRootUuid = stackedRestoreOwner.rootUuid;
2022
+ if (stackedRestoreOwner.revision !== restoreRevision) {
2023
+ restoreRootUuid = selectedGenrePlaylist?.root.uuid;
2024
+ setStackedRestoreOwner({ revision: restoreRevision, rootUuid: restoreRootUuid });
2025
+ } else if (restoreRootUuid === void 0 && selectedGenrePlaylist) {
2026
+ restoreRootUuid = selectedGenrePlaylist.root.uuid;
2027
+ setStackedRestoreOwner({ revision: restoreRevision, rootUuid: restoreRootUuid });
2028
+ }
1970
2029
  const isGenreUpdating = useIsMutating({ mutationKey: genreUpdateMutationKey }) > 0;
1971
2030
  const { mutate: updateGenre } = useUpdateGenre2(scope, getBackendBaseUrl);
1972
2031
  const busy = deferredSizeMetric !== sizeMetric || isTreeFetching || isGenreUpdating;
@@ -1991,7 +2050,7 @@ function GenreTreeView({
1991
2050
  scope,
1992
2051
  getBackendBaseUrl: artistBaseUrl,
1993
2052
  getShareUrl: getShareUrlRef.current,
1994
- renderArtistLink: renderArtistLink ?? ((artist) => artist.isSpecialPurpose ? artist.name : /* @__PURE__ */ jsx27("button", { type: "button", onClick: () => openArtist(artist.uuid), className: "hover:underline", children: artist.name })),
2053
+ renderArtistLink: renderArtistLink ?? ((artist) => artist.isSpecialPurpose ? artist.name : /* @__PURE__ */ jsx28("button", { type: "button", onClick: () => openArtist(artist.uuid), className: "hover:underline", children: artist.name })),
1995
2054
  expanded: controlledArtistExpanded ?? (artistUuid !== null && expandedArtistUuid === artistUuid),
1996
2055
  onExpandedChange: (on) => {
1997
2056
  if (controlledArtistExpanded === void 0) setExpandedArtistUuid(on ? artistUuid : null);
@@ -2013,18 +2072,11 @@ function GenreTreeView({
2013
2072
  setUncontrolledExpanded(next);
2014
2073
  onExpandedChange?.(next);
2015
2074
  };
2016
- useEffect5(() => {
2075
+ useEffect6(() => {
2017
2076
  setUncontrolledCollapsed(false);
2018
2077
  }, [selectedNodeId, artistUuid]);
2019
2078
  const isDetailHidden = isCollapsed || !hasSelectedGenreNode && !activeOverlayPanel;
2020
- useEffect5(() => {
2021
- if (placement !== "regions" || !isDetailHidden) return void 0;
2022
- const root = document.documentElement;
2023
- root.style.setProperty("--layout-left-w", "0px");
2024
- return () => {
2025
- root.style.removeProperty("--layout-left-w");
2026
- };
2027
- }, [placement, isDetailHidden]);
2079
+ useLeftDetailPanelLayout(!isDetailHidden, placement === "regions");
2028
2080
  const detailOverlay = placement === "regions" && activeOverlayPanel?.onExpandedChange ? activeOverlayPanel : void 0;
2029
2081
  const detailExpanded = detailOverlay ? !!detailOverlay.expanded : isExpanded;
2030
2082
  const onDetailExpandedChange = detailOverlay ? detailOverlay.onExpandedChange : changeExpanded;
@@ -2043,7 +2095,7 @@ function GenreTreeView({
2043
2095
  const handlePrevious = () => {
2044
2096
  rootRef.current?.querySelector(".search-overlay .gtv-info-panel-previous")?.click();
2045
2097
  };
2046
- useEffect5(() => {
2098
+ useEffect6(() => {
2047
2099
  if (!isTreeReady || viewMode !== "pop-core" || !hasSelectedGenreNode || !selectedGenrePlaylist) return;
2048
2100
  const root = rootRef.current;
2049
2101
  const centerChip = root?.querySelector(".gtv-wheel-center-node .gtv-wheel-chip");
@@ -2059,15 +2111,15 @@ function GenreTreeView({
2059
2111
  label: "Artists",
2060
2112
  items: artistSearchResult.artists.map((artist) => ({
2061
2113
  key: artist.uuid,
2062
- content: /* @__PURE__ */ jsxs20(Fragment8, { children: [
2063
- /* @__PURE__ */ jsxs20("span", { className: "block truncate font-medium", children: [
2114
+ content: /* @__PURE__ */ jsxs21(Fragment9, { children: [
2115
+ /* @__PURE__ */ jsxs21("span", { className: "block truncate font-medium", children: [
2064
2116
  artist.name,
2065
- artist.disambiguation && /* @__PURE__ */ jsxs20("span", { className: "font-normal text-[var(--color-neutral-500)]", children: [
2117
+ artist.disambiguation && /* @__PURE__ */ jsxs21("span", { className: "font-normal text-[var(--color-neutral-500)]", children: [
2066
2118
  " \xB7 ",
2067
2119
  artist.disambiguation
2068
2120
  ] })
2069
2121
  ] }),
2070
- artist.matchedAlias && /* @__PURE__ */ jsxs20("span", { className: "block truncate text-xs text-[var(--color-neutral-500)]", children: [
2122
+ artist.matchedAlias && /* @__PURE__ */ jsxs21("span", { className: "block truncate text-xs text-[var(--color-neutral-500)]", children: [
2071
2123
  "also known as ",
2072
2124
  artist.matchedAlias
2073
2125
  ] })
@@ -2099,7 +2151,7 @@ function GenreTreeView({
2099
2151
  (node, { toolbar, ancestors, family }) => {
2100
2152
  const genrePlaylist = genrePlaylistByNodeId.get(node.id);
2101
2153
  const criteria = genrePlaylist?.criteria;
2102
- if (!genrePlaylist || !criteria) return { primary: /* @__PURE__ */ jsxs20(Fragment8, { children: [
2154
+ if (!genrePlaylist || !criteria) return { primary: /* @__PURE__ */ jsxs21(Fragment9, { children: [
2103
2155
  toolbar,
2104
2156
  family
2105
2157
  ] }), aside: ancestors };
@@ -2119,21 +2171,26 @@ function GenreTreeView({
2119
2171
  (sibling) => sibling.parent?.uuid === parentPlaylist.uuid && sibling.uuid !== genrePlaylist.uuid && sibling.criteria?.side === "pop"
2120
2172
  ) : void 0;
2121
2173
  return {
2122
- primary: /* @__PURE__ */ jsxs20(Fragment8, { children: [
2123
- toolbar,
2124
- /* @__PURE__ */ jsx27(
2125
- GenreNodeActionBand,
2126
- {
2174
+ primary: /* @__PURE__ */ jsx28(
2175
+ GenreInfoPanelBody,
2176
+ {
2177
+ toolbar,
2178
+ family,
2179
+ genreUuid,
2180
+ genrePlaylist,
2181
+ wikidataId: criteria.wikidataId,
2182
+ scope,
2183
+ getBackendBaseUrl,
2184
+ playback,
2185
+ band: {
2127
2186
  node,
2128
- genrePlaylist,
2129
- scope,
2130
2187
  shareTarget: shareUrl ? shareTarget : void 0,
2131
2188
  shareUrl,
2132
2189
  onZoomIn: zoomInRef.current,
2133
2190
  onAddChild: readOnly ? void 0 : () => handleGenreCreationAction(criteria),
2134
2191
  onRename: readOnly ? void 0 : () => handleGenreRenameAction(criteria),
2135
2192
  onChangeParent: readOnly ? void 0 : () => showPopup(
2136
- /* @__PURE__ */ jsx27(
2193
+ /* @__PURE__ */ jsx28(
2137
2194
  ChangeParentPopup,
2138
2195
  {
2139
2196
  genrePlaylist: { ...genrePlaylist, criteria },
@@ -2148,19 +2205,9 @@ function GenreTreeView({
2148
2205
  side,
2149
2206
  onToggleSide: canToggleSide ? () => updateGenre({ uuid: genreUuid, data: { side: side === "pop" ? "core" : "pop" } }) : void 0,
2150
2207
  toggleSideDisabledReason: popSibling ? `${popSibling.name} is already the pop side of ${parentPlaylist?.name}` : void 0,
2151
- additionalActions,
2152
- playback
2153
- }
2154
- ),
2155
- /* @__PURE__ */ jsx27(
2156
- GenreDetailExtras,
2157
- {
2158
- family,
2159
- genreUuid,
2160
- genrePlaylist,
2161
- wikidataId: criteria.wikidataId,
2162
- scope,
2163
- getBackendBaseUrl,
2208
+ additionalActions
2209
+ },
2210
+ extras: {
2164
2211
  criteriaOverviewSchema,
2165
2212
  renderGenreDetailExtras,
2166
2213
  renderArtistLink,
@@ -2170,10 +2217,9 @@ function GenreTreeView({
2170
2217
  renderSongLeading,
2171
2218
  renderSongActions,
2172
2219
  renderTracksToolbar
2173
- },
2174
- genreUuid
2175
- )
2176
- ] }),
2220
+ }
2221
+ }
2222
+ ),
2177
2223
  aside: ancestors
2178
2224
  };
2179
2225
  },
@@ -2204,7 +2250,7 @@ function GenreTreeView({
2204
2250
  );
2205
2251
  const isRotationControlled = controlledAllowWheelRotation !== void 0;
2206
2252
  const allowWheelRotation = controlledAllowWheelRotation ?? internalAllowWheelRotation;
2207
- const detailTab = placement === "regions" && (hasSelectedGenreNode || detailOverlay) ? /* @__PURE__ */ jsx27(
2253
+ const detailTab = placement === "regions" && (hasSelectedGenreNode || detailOverlay) ? /* @__PURE__ */ jsx28(
2208
2254
  PanelEdgeTab,
2209
2255
  {
2210
2256
  side: "left",
@@ -2214,8 +2260,8 @@ function GenreTreeView({
2214
2260
  ) : null;
2215
2261
  const renderArtistFilter = () => {
2216
2262
  if (isLoading || !filterArtist) return null;
2217
- return /* @__PURE__ */ jsxs20("span", { className: "inline-flex items-center overflow-hidden rounded-full bg-[var(--color-neutral-100)] text-sm", children: [
2218
- /* @__PURE__ */ jsx27(
2263
+ return /* @__PURE__ */ jsxs21("span", { className: "inline-flex items-center overflow-hidden rounded-full bg-[var(--color-neutral-100)] text-sm", children: [
2264
+ /* @__PURE__ */ jsx28(
2219
2265
  "button",
2220
2266
  {
2221
2267
  type: "button",
@@ -2227,7 +2273,7 @@ function GenreTreeView({
2227
2273
  children: filterArtist.name
2228
2274
  }
2229
2275
  ),
2230
- /* @__PURE__ */ jsx27(
2276
+ /* @__PURE__ */ jsx28(
2231
2277
  "button",
2232
2278
  {
2233
2279
  type: "button",
@@ -2239,8 +2285,8 @@ function GenreTreeView({
2239
2285
  )
2240
2286
  ] });
2241
2287
  };
2242
- const actions = /* @__PURE__ */ jsxs20(Fragment8, { children: [
2243
- !isLoading && /* @__PURE__ */ jsx27(
2288
+ const actions = /* @__PURE__ */ jsxs21(Fragment9, { children: [
2289
+ !isLoading && /* @__PURE__ */ jsx28(
2244
2290
  GenreSearch,
2245
2291
  {
2246
2292
  genrePlaylists: visibleGenrePlaylists,
@@ -2252,9 +2298,9 @@ function GenreTreeView({
2252
2298
  extraResults: artistExtraResults
2253
2299
  }
2254
2300
  ),
2255
- !isLoading && filterArtist && /* @__PURE__ */ jsx27("div", { className: "hidden w-full justify-center max-md:flex", children: renderArtistFilter() }),
2256
- /* @__PURE__ */ jsxs20("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: [
2257
- !isLoading && !isControlled && /* @__PURE__ */ jsx27(
2301
+ !isLoading && filterArtist && /* @__PURE__ */ jsx28("div", { className: "hidden w-full justify-center max-md:flex", children: renderArtistFilter() }),
2302
+ /* @__PURE__ */ jsxs21("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: [
2303
+ !isLoading && !isControlled && /* @__PURE__ */ jsx28(
2258
2304
  SegmentedControl2,
2259
2305
  {
2260
2306
  "aria-label": "Tree view mode",
@@ -2268,13 +2314,13 @@ function GenreTreeView({
2268
2314
  ]
2269
2315
  }
2270
2316
  ),
2271
- !isLoading && showDisplayOptions && /* @__PURE__ */ jsx27(
2317
+ !isLoading && showDisplayOptions && /* @__PURE__ */ jsx28(
2272
2318
  "div",
2273
2319
  {
2274
2320
  className: "flex items-center gap-1",
2275
2321
  role: "group",
2276
2322
  "aria-label": "Tree display options",
2277
- children: !isRotationControlled && viewMode !== "stacked" && viewMode !== "outline" && /* @__PURE__ */ jsx27(
2323
+ children: !isRotationControlled && viewMode !== "stacked" && viewMode !== "outline" && /* @__PURE__ */ jsx28(
2278
2324
  Button,
2279
2325
  {
2280
2326
  variant: allowWheelRotation ? "default" : "outline",
@@ -2285,7 +2331,7 @@ function GenreTreeView({
2285
2331
  )
2286
2332
  }
2287
2333
  ),
2288
- !isLoading && !readOnly && showAddRoot && /* @__PURE__ */ jsx27(
2334
+ !isLoading && !readOnly && showAddRoot && /* @__PURE__ */ jsx28(
2289
2335
  IconTextButton,
2290
2336
  {
2291
2337
  icon: Plus2,
@@ -2295,42 +2341,42 @@ function GenreTreeView({
2295
2341
  )
2296
2342
  ] })
2297
2343
  ] });
2298
- return /* @__PURE__ */ jsxs20(
2344
+ return /* @__PURE__ */ jsxs21(
2299
2345
  "div",
2300
2346
  {
2301
2347
  ref: rootRef,
2302
2348
  className: ["genre-tree-view relative flex flex-col h-full", placement === "regions" && REGIONS_CLASS, className2].filter(Boolean).join(" "),
2303
2349
  children: [
2304
- /* @__PURE__ */ jsx27("style", { children: PREVIOUS_BUTTON_CSS }),
2305
- /* @__PURE__ */ jsx27(
2350
+ /* @__PURE__ */ jsx28("style", { children: PREVIOUS_BUTTON_CSS }),
2351
+ /* @__PURE__ */ jsx28(
2306
2352
  "div",
2307
2353
  {
2308
2354
  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"}`,
2309
2355
  children: actions
2310
2356
  }
2311
2357
  ),
2312
- !isLoading && filterArtist && /* @__PURE__ */ jsx27(
2358
+ !isLoading && filterArtist && /* @__PURE__ */ jsx28(
2313
2359
  "div",
2314
2360
  {
2315
2361
  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"}`,
2316
2362
  children: renderArtistFilter()
2317
2363
  }
2318
2364
  ),
2319
- detailTab && /* @__PURE__ */ jsx27(
2365
+ detailTab && /* @__PURE__ */ jsx28(
2320
2366
  "div",
2321
2367
  {
2322
2368
  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))]"}`,
2323
2369
  children: detailTab
2324
2370
  }
2325
2371
  ),
2326
- /* @__PURE__ */ jsx27("div", { className: "content-container flex-1 min-h-0 flex flex-row gap-4", children: /* @__PURE__ */ jsxs20("div", { className: "tree-view-container relative flex-1 min-w-0 flex flex-col h-full", children: [
2327
- !isLoading && viewMode === "outline" && floatingControlsLeading && /* @__PURE__ */ jsx27("div", { className: "absolute bottom-3 right-3 z-10", children: floatingControlsLeading }),
2328
- isLoading ? /* @__PURE__ */ jsx27(GenreTreeViewSkeleton, { viewMode }) : viewMode === "wheel" ? (
2372
+ /* @__PURE__ */ jsx28("div", { className: "content-container flex-1 min-h-0 flex flex-row gap-4", children: /* @__PURE__ */ jsxs21("div", { className: "tree-view-container relative flex-1 min-w-0 flex flex-col h-full", children: [
2373
+ !isLoading && viewMode === "outline" && floatingControlsLeading && /* @__PURE__ */ jsx28("div", { className: "absolute bottom-3 right-3 z-10", children: floatingControlsLeading }),
2374
+ isLoading ? /* @__PURE__ */ jsx28(GenreTreeViewSkeleton, { viewMode }) : viewMode === "wheel" ? (
2329
2375
  // The info panel's top edge aligns with the tree's; the search bar floats `--layout-panel-inset` below it,
2330
2376
  // and the header inset (the bar's height plus that inset) extends the header's fill behind the bar,
2331
2377
  // Google Maps style; the 8px bottom padding mirrors the inset above the bar, and the hidden title's height is zeroed.
2332
2378
  // The panel's own Zoom in button is hidden: the search bar carries it.
2333
- /* @__PURE__ */ jsx27("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__ */ jsx27(GenreTreeWheelHandoff, { skeleton: /* @__PURE__ */ jsx27(GenreTreeWheelSkeleton, {}), onRevealedChange: setIsWheelRevealed, children: /* @__PURE__ */ jsx27(
2379
+ /* @__PURE__ */ jsx28("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__ */ jsx28(GenreTreeWheelHandoff, { skeleton: /* @__PURE__ */ jsx28(GenreTreeWheelSkeleton, {}), onRevealedChange: setIsWheelRevealed, children: /* @__PURE__ */ jsx28(
2334
2380
  GenrePlaylistTreeWheel,
2335
2381
  {
2336
2382
  scope,
@@ -2347,21 +2393,21 @@ function GenreTreeView({
2347
2393
  selectedNodeId,
2348
2394
  readOnly,
2349
2395
  allowWheelRotation,
2350
- controlsOrientation,
2351
2396
  sizeMetric: isArtistFiltered || !canSizeByViews ? "songs" : deferredSizeMetric,
2352
2397
  busy,
2353
- floatingControlsLeading: floatingControlsLeading || canSizeByViews ? /* @__PURE__ */ jsxs20("div", { className: "flex items-center gap-2", children: [
2398
+ floatingControlsLeading: floatingControlsLeading || canSizeByViews ? /* @__PURE__ */ jsxs21("div", { className: "flex items-center gap-2", children: [
2354
2399
  floatingControlsLeading,
2355
- canSizeByViews && /* @__PURE__ */ jsx27(SizeMetricToggle, { value: sizeMetric, onChange: changeSizeMetric })
2400
+ canSizeByViews && /* @__PURE__ */ jsx28(SizeMetricToggle, { value: sizeMetric, onChange: changeSizeMetric })
2356
2401
  ] }) : void 0,
2357
2402
  defaultViewport,
2403
+ viewportRestore,
2358
2404
  onViewportChange,
2359
2405
  ...infoPanelCuration
2360
2406
  }
2361
2407
  ) }) })
2362
2408
  ) : viewMode === "pop-core" ? (
2363
2409
  // Same as the wheel: the header inset extends its fill behind the floating search bar, Google Maps style.
2364
- /* @__PURE__ */ jsx27("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__ */ jsx27(GenreTreeWheelHandoff, { skeleton: /* @__PURE__ */ jsx27(GenreTreeWheelSkeleton, {}), onRevealedChange: setIsWheelRevealed, children: /* @__PURE__ */ jsx27(
2410
+ /* @__PURE__ */ jsx28("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__ */ jsx28(GenreTreeWheelHandoff, { skeleton: /* @__PURE__ */ jsx28(GenreTreeWheelSkeleton, {}), onRevealedChange: setIsWheelRevealed, children: /* @__PURE__ */ jsx28(
2365
2411
  GenrePlaylistTreeWheelRadialPopCore,
2366
2412
  {
2367
2413
  scope,
@@ -2378,20 +2424,20 @@ function GenreTreeView({
2378
2424
  selectedNodeId,
2379
2425
  readOnly,
2380
2426
  allowWheelRotation,
2381
- controlsOrientation,
2382
2427
  sizeMetric: isArtistFiltered || !canSizeByViews ? "songs" : deferredSizeMetric,
2383
2428
  busy,
2384
- floatingControlsLeading: floatingControlsLeading || canSizeByViews ? /* @__PURE__ */ jsxs20("div", { className: "flex items-center gap-2", children: [
2429
+ floatingControlsLeading: floatingControlsLeading || canSizeByViews ? /* @__PURE__ */ jsxs21("div", { className: "flex items-center gap-2", children: [
2385
2430
  floatingControlsLeading,
2386
- canSizeByViews && /* @__PURE__ */ jsx27(SizeMetricToggle, { value: sizeMetric, onChange: changeSizeMetric })
2431
+ canSizeByViews && /* @__PURE__ */ jsx28(SizeMetricToggle, { value: sizeMetric, onChange: changeSizeMetric })
2387
2432
  ] }) : void 0,
2388
2433
  defaultViewport,
2434
+ viewportRestore,
2389
2435
  onViewportChange,
2390
2436
  ...infoPanelCuration,
2391
2437
  ...focus
2392
2438
  }
2393
2439
  ) }) })
2394
- ) : viewMode === "outline" ? /* @__PURE__ */ jsx27("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__ */ jsx27(
2440
+ ) : viewMode === "outline" ? /* @__PURE__ */ jsx28("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__ */ jsx28(
2395
2441
  GenrePlaylistTreeWheelRadialPopCore,
2396
2442
  {
2397
2443
  outline: true,
@@ -2410,13 +2456,13 @@ function GenreTreeView({
2410
2456
  readOnly,
2411
2457
  ...infoPanelCuration
2412
2458
  }
2413
- ) }) : /* @__PURE__ */ jsx27("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(
2459
+ ) }) : /* @__PURE__ */ jsx28("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(
2414
2460
  ([uuid, genrePlaylistTreePerRoot], rootIndex) => {
2415
- return /* @__PURE__ */ jsx27(
2461
+ return /* @__PURE__ */ jsx28(
2416
2462
  "div",
2417
2463
  {
2418
2464
  className: "tree-per-root-container relative shrink-0 h-[28rem] mt-2 mr-16 p-2 bg-[var(--color-neutral-100)] rounded-lg",
2419
- children: /* @__PURE__ */ jsx27("div", { className: "graph-container relative z-10 h-full", children: /* @__PURE__ */ jsx27(
2465
+ children: /* @__PURE__ */ jsx28("div", { className: "graph-container relative z-10 h-full", children: /* @__PURE__ */ jsx28(
2420
2466
  GenrePlaylistTreePerRoot,
2421
2467
  {
2422
2468
  scope,
@@ -2434,8 +2480,8 @@ function GenreTreeView({
2434
2480
  selectedNodeId: uuid === selectedGenrePlaylist?.root.uuid ? selectedNodeId : null,
2435
2481
  readOnly,
2436
2482
  wheelZoom: "modifier",
2437
- controlsOrientation,
2438
2483
  defaultViewport: uuid === selectedGenrePlaylist?.root.uuid ? defaultViewport : void 0,
2484
+ viewportRestore: uuid === selectedGenrePlaylist?.root.uuid && uuid === restoreRootUuid ? viewportRestore : void 0,
2439
2485
  onViewportChange: uuid === selectedGenrePlaylist?.root.uuid ? onViewportChange : void 0,
2440
2486
  ...infoPanelCuration,
2441
2487
  overlayPanel: rootIndex === 0 ? activeOverlayPanel : void 0
@@ -2452,6 +2498,123 @@ function GenreTreeView({
2452
2498
  );
2453
2499
  }
2454
2500
 
2501
+ // src/genre-tree/GenreDetailPanel.tsx
2502
+ import { useMemo as useMemo8 } from "react";
2503
+ import {
2504
+ ACCENT_TEXT_COLOR,
2505
+ getGenreTreeColor as getGenreTreeColor2,
2506
+ InfoPanel,
2507
+ TEXT_COLOR,
2508
+ tintSurface
2509
+ } from "@behindthemusictree/genre-tree-view";
2510
+ import { Skeleton as Skeleton5 } from "@behindthemusictree/ui";
2511
+ import {
2512
+ useFetchGenreDetail
2513
+ } from "@behindthemusictree/app-kit/genre-tree";
2514
+ import { jsx as jsx29, jsxs as jsxs22 } from "react/jsx-runtime";
2515
+ var ITEM_COUNT_LABEL = "Songs";
2516
+ var RELATIVE_ITEM_COUNT = 1;
2517
+ function toNode(genre, parentId) {
2518
+ return {
2519
+ id: genre.uuid,
2520
+ parentId,
2521
+ name: genre.name,
2522
+ itemCount: RELATIVE_ITEM_COUNT,
2523
+ side: genre.side ?? void 0,
2524
+ wikidataId: genre.wikidataId
2525
+ };
2526
+ }
2527
+ function GenreDetailPanel({
2528
+ genreUuid,
2529
+ scope,
2530
+ getBackendBaseUrl,
2531
+ onClose,
2532
+ onSelectGenre,
2533
+ onZoomIn,
2534
+ shareUrl,
2535
+ shareTarget,
2536
+ collapsed,
2537
+ expanded,
2538
+ onExpandedChange,
2539
+ extras
2540
+ }) {
2541
+ const { data: detail, isPending, isError } = useFetchGenreDetail(genreUuid, scope, getBackendBaseUrl);
2542
+ const playback = useGenrePlaylistPlayback({ scope, genrePlaylists: [] });
2543
+ const view = useMemo8(() => {
2544
+ if (!detail) return null;
2545
+ const rootColor = getGenreTreeColor2(detail.root.wikidataId);
2546
+ const relative = (genre, parentId) => ({
2547
+ node: toNode(genre, parentId),
2548
+ fill: tintSurface(rootColor),
2549
+ textColor: TEXT_COLOR
2550
+ });
2551
+ const ancestors = [...detail.ascendants].sort((a, b) => b.degree - a.degree);
2552
+ return {
2553
+ node: {
2554
+ id: detail.uuid,
2555
+ parentId: detail.parent?.uuid ?? null,
2556
+ name: detail.name,
2557
+ itemCount: detail.tracksCount,
2558
+ wikidataId: detail.wikidataId
2559
+ },
2560
+ fill: rootColor,
2561
+ parentNode: detail.parent ? relative(detail.parent, null) : null,
2562
+ childNodes: detail.children.map((child) => relative(child, detail.uuid)),
2563
+ ancestorNodes: ancestors.map(
2564
+ ({ ascendant }, index) => relative(ascendant, index === 0 ? null : ancestors[index - 1].ascendant.uuid)
2565
+ )
2566
+ };
2567
+ }, [detail]);
2568
+ if (isError) {
2569
+ return /* @__PURE__ */ jsx29("div", { role: "alert", className: "p-4 text-sm text-[var(--color-text)]", children: "This genre could not be loaded." });
2570
+ }
2571
+ if (isPending || !detail || !view) {
2572
+ return /* @__PURE__ */ jsxs22("div", { className: "flex flex-col gap-3 p-4", "aria-busy": "true", "data-testid": "genre-detail-panel-skeleton", children: [
2573
+ /* @__PURE__ */ jsx29(Skeleton5, { className: "h-6 w-2/3" }),
2574
+ /* @__PURE__ */ jsx29(Skeleton5, { className: "h-4 w-full" }),
2575
+ /* @__PURE__ */ jsx29(Skeleton5, { className: "h-4 w-3/4" })
2576
+ ] });
2577
+ }
2578
+ const renderBody = (node, sections) => ({
2579
+ primary: /* @__PURE__ */ jsx29(
2580
+ GenreInfoPanelBody,
2581
+ {
2582
+ toolbar: sections.toolbar,
2583
+ family: sections.family,
2584
+ genreUuid: detail.uuid,
2585
+ genrePlaylist: { ...detail.criteriaPlaylist, tracksCount: detail.tracksCount },
2586
+ wikidataId: detail.wikidataId,
2587
+ scope,
2588
+ getBackendBaseUrl,
2589
+ playback,
2590
+ band: { node, onZoomIn, shareUrl, shareTarget: shareUrl ? shareTarget : void 0 },
2591
+ extras
2592
+ }
2593
+ ),
2594
+ aside: null
2595
+ });
2596
+ return /* @__PURE__ */ jsx29(
2597
+ InfoPanel,
2598
+ {
2599
+ node: view.node,
2600
+ fill: view.fill,
2601
+ textColor: ACCENT_TEXT_COLOR,
2602
+ parentNode: view.parentNode,
2603
+ childNodes: view.childNodes,
2604
+ ancestorNodes: view.ancestorNodes,
2605
+ side: "left",
2606
+ onClose,
2607
+ onSelectNode: (nodeId) => onSelectGenre(nodeId),
2608
+ itemCountLabel: ITEM_COUNT_LABEL,
2609
+ renderInfoPanelBody: renderBody,
2610
+ expanded,
2611
+ onExpandedChange,
2612
+ collapsed,
2613
+ hideExpandButton: onExpandedChange === void 0
2614
+ }
2615
+ );
2616
+ }
2617
+
2455
2618
  // src/genre-tree/index.ts
2456
2619
  import {
2457
2620
  GenreTreeSkeleton,
@@ -2461,13 +2624,13 @@ import {
2461
2624
  } from "@behindthemusictree/genre-tree-view";
2462
2625
 
2463
2626
  // src/genre-tree/track-list-sidebar/TrackListSidebar.tsx
2464
- import { useEffect as useEffect6, useLayoutEffect as useLayoutEffect2, useRef as useRef5 } from "react";
2627
+ import { useEffect as useEffect7, useLayoutEffect as useLayoutEffect2, useRef as useRef5 } from "react";
2465
2628
  import { ListMusic } from "lucide-react";
2466
2629
  import { useTrackList as useTrackList5 } from "@behindthemusictree/app-kit/genre-tree";
2467
2630
  import { TrackListOriginType as TrackListOriginType2 } from "@behindthemusictree/app-kit/genre-tree";
2468
2631
  import { useTrackNoun as useTrackNoun6 } from "@behindthemusictree/app-kit/track-noun";
2469
- import { jsx as jsx28, jsxs as jsxs21 } from "react/jsx-runtime";
2470
- var useBrowserLayoutEffect = typeof window === "undefined" ? useEffect6 : useLayoutEffect2;
2632
+ import { jsx as jsx30, jsxs as jsxs23 } from "react/jsx-runtime";
2633
+ var useBrowserLayoutEffect = typeof window === "undefined" ? useEffect7 : useLayoutEffect2;
2471
2634
  function TrackListSidebar({
2472
2635
  className: className2 = "",
2473
2636
  renderDuration,
@@ -2489,7 +2652,7 @@ function TrackListSidebar({
2489
2652
  const userScrollIntentTimerRef = useRef5(null);
2490
2653
  const loadMoreSentinelRef = useRef5(null);
2491
2654
  const hasMore = !!trackList && trackList.nextPage !== null;
2492
- useEffect6(() => {
2655
+ useEffect7(() => {
2493
2656
  const sentinel = loadMoreSentinelRef.current;
2494
2657
  if (!hasMore || !sentinel) return;
2495
2658
  const observer = new IntersectionObserver((entries) => {
@@ -2509,11 +2672,11 @@ function TrackListSidebar({
2509
2672
  programmaticScrollTopRef.current = null;
2510
2673
  if (trackListRef.current) trackListRef.current.scrollTop = 0;
2511
2674
  }, [trackList?.origin]);
2512
- useEffect6(() => {
2675
+ useEffect7(() => {
2513
2676
  const list = trackListRef.current;
2514
2677
  if (!isCollapsed && list?.scrollTop === 0) shouldFollowPlaybackRef.current = true;
2515
2678
  }, [isCollapsed]);
2516
- useEffect6(
2679
+ useEffect7(
2517
2680
  () => () => {
2518
2681
  if (userScrollIntentTimerRef.current !== null) clearTimeout(userScrollIntentTimerRef.current);
2519
2682
  },
@@ -2567,25 +2730,25 @@ function TrackListSidebar({
2567
2730
  }
2568
2731
  };
2569
2732
  const positionClasses = layout === "inline" ? "relative w-full flex h-full flex-col" : "fixed right-0 w-[50rem] flex flex-col";
2570
- return trackList ? /* @__PURE__ */ jsxs21(
2733
+ return trackList ? /* @__PURE__ */ jsxs23(
2571
2734
  "div",
2572
2735
  {
2573
2736
  "data-theme": "dark",
2574
2737
  className: `track-list-sidebar ${positionClasses} rounded-2xl bg-[var(--color-surface)] pb-1 ${className2}`,
2575
2738
  style: layout === "fixed" ? { top: "63.5px", maxHeight: "calc(100vh - 63.5px - 79px)" } : void 0,
2576
2739
  children: [
2577
- /* @__PURE__ */ jsxs21("div", { className: "header flex h-16 items-center px-4 py-2 text-[var(--color-text)]", children: [
2578
- /* @__PURE__ */ jsxs21("div", { className: "min-w-0 flex flex-1 flex-col justify-center", children: [
2579
- /* @__PURE__ */ jsxs21("div", { className: "origin flex text-xl", children: [
2580
- /* @__PURE__ */ jsx28("div", { className: "from h-auto flex flex-col justify-center items-center mr-2", children: "From" }),
2581
- /* @__PURE__ */ jsx28("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__ */ jsx28("div", { className: "name max-w-full truncate", children: trackList.origin.label }) })
2740
+ /* @__PURE__ */ jsxs23("div", { className: "header flex h-16 items-center px-4 py-2 text-[var(--color-text)]", children: [
2741
+ /* @__PURE__ */ jsxs23("div", { className: "min-w-0 flex flex-1 flex-col justify-center", children: [
2742
+ /* @__PURE__ */ jsxs23("div", { className: "origin flex text-xl", children: [
2743
+ /* @__PURE__ */ jsx30("div", { className: "from h-auto flex flex-col justify-center items-center mr-2", children: "From" }),
2744
+ /* @__PURE__ */ jsx30("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__ */ jsx30("div", { className: "name max-w-full truncate", children: trackList.origin.label }) })
2582
2745
  ] }),
2583
- /* @__PURE__ */ jsxs21("div", { className: "info flex items-center text-sm text-[var(--color-text-muted)]", children: [
2746
+ /* @__PURE__ */ jsxs23("div", { className: "info flex items-center text-sm text-[var(--color-text-muted)]", children: [
2584
2747
  trackList.origin.type === TrackListOriginType2.GENRE_PLAYLIST ? "Genre \u2022 " : trackList.origin.type === TrackListOriginType2.ARTIST ? "Artist \u2022 " : `${trackNoun.singular} playlist \u2022 `,
2585
2748
  `${trackList.total} ${trackList.total > 1 ? trackNoun.plural : trackNoun.singular}`
2586
2749
  ] })
2587
2750
  ] }),
2588
- (isCollapsed && onExpand || !isCollapsed && onMinimize) && /* @__PURE__ */ jsx28(
2751
+ (isCollapsed && onExpand || !isCollapsed && onMinimize) && /* @__PURE__ */ jsx30(
2589
2752
  "button",
2590
2753
  {
2591
2754
  type: "button",
@@ -2594,11 +2757,11 @@ function TrackListSidebar({
2594
2757
  "aria-expanded": !isCollapsed,
2595
2758
  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"}`,
2596
2759
  onClick: isCollapsed ? onExpand : onMinimize,
2597
- children: /* @__PURE__ */ jsx28(ListMusic, { "aria-hidden": "true", className: "h-5 w-5" })
2760
+ children: /* @__PURE__ */ jsx30(ListMusic, { "aria-hidden": "true", className: "h-5 w-5" })
2598
2761
  }
2599
2762
  )
2600
2763
  ] }),
2601
- !isCollapsed && /* @__PURE__ */ jsxs21(
2764
+ !isCollapsed && /* @__PURE__ */ jsxs23(
2602
2765
  "ul",
2603
2766
  {
2604
2767
  ref: trackListRef,
@@ -2608,7 +2771,7 @@ function TrackListSidebar({
2608
2771
  onPointerDown: markUserScrollIntent,
2609
2772
  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",
2610
2773
  children: [
2611
- trackList.tracks.map((track, index) => /* @__PURE__ */ jsx28("li", { "data-track-uuid": track.uuid, children: /* @__PURE__ */ jsx28(
2774
+ trackList.tracks.map((track, index) => /* @__PURE__ */ jsx30("li", { "data-track-uuid": track.uuid, children: /* @__PURE__ */ jsx30(
2612
2775
  TrackItem,
2613
2776
  {
2614
2777
  track,
@@ -2618,7 +2781,7 @@ function TrackListSidebar({
2618
2781
  renderArtistLink
2619
2782
  }
2620
2783
  ) }, track.uuid)),
2621
- hasMore && /* @__PURE__ */ jsx28("li", { ref: loadMoreSentinelRef, "aria-hidden": "true", className: "h-px" })
2784
+ hasMore && /* @__PURE__ */ jsx30("li", { ref: loadMoreSentinelRef, "aria-hidden": "true", className: "h-px" })
2622
2785
  ]
2623
2786
  }
2624
2787
  )
@@ -2628,7 +2791,7 @@ function TrackListSidebar({
2628
2791
  }
2629
2792
 
2630
2793
  // src/genre-tree/PlayerPanel.tsx
2631
- import { useEffect as useEffect7, useState as useState6 } from "react";
2794
+ import { useEffect as useEffect8, useState as useState6 } from "react";
2632
2795
  import { twMerge as twMerge2 } from "tailwind-merge";
2633
2796
  import {
2634
2797
  usePlayer as usePlayer10,
@@ -2642,7 +2805,7 @@ import {
2642
2805
  import { Pause as Pause2, Play as Play3, SkipBack, SkipForward, Volume2, VolumeX } from "lucide-react";
2643
2806
  import { formatTime, useTrackList as useTrackList6 } from "@behindthemusictree/app-kit/genre-tree";
2644
2807
  import { PlayStates as PlayStates3, useCurrentTime, usePlayer as usePlayer9 } from "@behindthemusictree/app-kit/player";
2645
- import { jsx as jsx29, jsxs as jsxs22 } from "react/jsx-runtime";
2808
+ import { jsx as jsx31, jsxs as jsxs24 } from "react/jsx-runtime";
2646
2809
  function PlayerControls() {
2647
2810
  const {
2648
2811
  duration,
@@ -2661,15 +2824,15 @@ function PlayerControls() {
2661
2824
  const safeDuration = Number.isFinite(duration) && duration > 0 ? duration : 0;
2662
2825
  const safeCurrentTime = Number.isFinite(currentTime) ? Math.max(0, Math.min(currentTime, safeDuration)) : 0;
2663
2826
  const isPlaying = playState === PlayStates3.PLAYING;
2664
- return /* @__PURE__ */ jsxs22(
2827
+ return /* @__PURE__ */ jsxs24(
2665
2828
  "section",
2666
2829
  {
2667
2830
  "aria-label": "Player controls",
2668
2831
  className: "player-controls flex shrink-0 flex-col gap-2 border-b border-zinc-800 px-3 py-2 text-zinc-200",
2669
2832
  children: [
2670
- /* @__PURE__ */ jsxs22("div", { className: "flex min-w-0 items-center gap-2 text-xs tabular-nums text-zinc-400", children: [
2671
- /* @__PURE__ */ jsx29("span", { "aria-label": "Elapsed time", children: formatTime(safeCurrentTime) }),
2672
- /* @__PURE__ */ jsx29(
2833
+ /* @__PURE__ */ jsxs24("div", { className: "flex min-w-0 items-center gap-2 text-xs tabular-nums text-zinc-400", children: [
2834
+ /* @__PURE__ */ jsx31("span", { "aria-label": "Elapsed time", children: formatTime(safeCurrentTime) }),
2835
+ /* @__PURE__ */ jsx31(
2673
2836
  "input",
2674
2837
  {
2675
2838
  "aria-label": "Seek",
@@ -2683,11 +2846,11 @@ function PlayerControls() {
2683
2846
  onChange: (event) => seekTo(Number(event.currentTarget.value))
2684
2847
  }
2685
2848
  ),
2686
- /* @__PURE__ */ jsx29("span", { "aria-label": "Duration", children: formatTime(safeDuration) })
2849
+ /* @__PURE__ */ jsx31("span", { "aria-label": "Duration", children: formatTime(safeDuration) })
2687
2850
  ] }),
2688
- /* @__PURE__ */ jsxs22("div", { className: "grid min-h-9 grid-cols-[1fr_auto_1fr] items-center gap-3", children: [
2689
- /* @__PURE__ */ jsxs22("div", { className: "col-start-2 flex items-center justify-center gap-2", children: [
2690
- /* @__PURE__ */ jsx29(
2851
+ /* @__PURE__ */ jsxs24("div", { className: "grid min-h-9 grid-cols-[1fr_auto_1fr] items-center gap-3", children: [
2852
+ /* @__PURE__ */ jsxs24("div", { className: "col-start-2 flex items-center justify-center gap-2", children: [
2853
+ /* @__PURE__ */ jsx31(
2691
2854
  "button",
2692
2855
  {
2693
2856
  type: "button",
@@ -2696,10 +2859,10 @@ function PlayerControls() {
2696
2859
  disabled: !isReady || !hasPreviousTrack,
2697
2860
  onClick: playPreviousTrack,
2698
2861
  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",
2699
- children: /* @__PURE__ */ jsx29(SkipBack, { "aria-hidden": "true", className: "h-4 w-4" })
2862
+ children: /* @__PURE__ */ jsx31(SkipBack, { "aria-hidden": "true", className: "h-4 w-4" })
2700
2863
  }
2701
2864
  ),
2702
- /* @__PURE__ */ jsx29(
2865
+ /* @__PURE__ */ jsx31(
2703
2866
  "button",
2704
2867
  {
2705
2868
  type: "button",
@@ -2708,10 +2871,10 @@ function PlayerControls() {
2708
2871
  disabled: !isReady,
2709
2872
  onClick: handlePlayPauseAction,
2710
2873
  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",
2711
- children: isPlaying ? /* @__PURE__ */ jsx29(Pause2, { "aria-hidden": "true", className: "h-4 w-4", fill: "currentColor" }) : /* @__PURE__ */ jsx29(Play3, { "aria-hidden": "true", className: "h-4 w-4", fill: "currentColor" })
2874
+ children: isPlaying ? /* @__PURE__ */ jsx31(Pause2, { "aria-hidden": "true", className: "h-4 w-4", fill: "currentColor" }) : /* @__PURE__ */ jsx31(Play3, { "aria-hidden": "true", className: "h-4 w-4", fill: "currentColor" })
2712
2875
  }
2713
2876
  ),
2714
- /* @__PURE__ */ jsx29(
2877
+ /* @__PURE__ */ jsx31(
2715
2878
  "button",
2716
2879
  {
2717
2880
  type: "button",
@@ -2720,14 +2883,14 @@ function PlayerControls() {
2720
2883
  disabled: !isReady || !hasNextTrack,
2721
2884
  onClick: playNextTrack,
2722
2885
  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",
2723
- children: /* @__PURE__ */ jsx29(SkipForward, { "aria-hidden": "true", className: "h-4 w-4" })
2886
+ children: /* @__PURE__ */ jsx31(SkipForward, { "aria-hidden": "true", className: "h-4 w-4" })
2724
2887
  }
2725
2888
  )
2726
2889
  ] }),
2727
- /* @__PURE__ */ jsxs22("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: [
2728
- volume === 0 ? /* @__PURE__ */ jsx29(VolumeX, { "aria-hidden": "true", className: "h-[0.7rem] w-[0.7rem] shrink-0" }) : /* @__PURE__ */ jsx29(Volume2, { "aria-hidden": "true", className: "h-[0.7rem] w-[0.7rem] shrink-0" }),
2729
- /* @__PURE__ */ jsx29("span", { className: "sr-only", children: "Volume" }),
2730
- /* @__PURE__ */ jsx29(
2890
+ /* @__PURE__ */ jsxs24("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: [
2891
+ volume === 0 ? /* @__PURE__ */ jsx31(VolumeX, { "aria-hidden": "true", className: "h-[0.7rem] w-[0.7rem] shrink-0" }) : /* @__PURE__ */ jsx31(Volume2, { "aria-hidden": "true", className: "h-[0.7rem] w-[0.7rem] shrink-0" }),
2892
+ /* @__PURE__ */ jsx31("span", { className: "sr-only", children: "Volume" }),
2893
+ /* @__PURE__ */ jsx31(
2731
2894
  "input",
2732
2895
  {
2733
2896
  "aria-label": "Volume",
@@ -2749,7 +2912,7 @@ function PlayerControls() {
2749
2912
  }
2750
2913
 
2751
2914
  // src/genre-tree/PlayerPanel.tsx
2752
- import { jsx as jsx30, jsxs as jsxs23 } from "react/jsx-runtime";
2915
+ import { jsx as jsx32, jsxs as jsxs25 } from "react/jsx-runtime";
2753
2916
  var PANEL_ID = "player-panel";
2754
2917
  var DEFAULT_EXTENDED_WIDTH = "calc(100vw - var(--layout-center-left) - var(--layout-gutter))";
2755
2918
  function PlayerPanel({
@@ -2764,7 +2927,7 @@ function PlayerPanel({
2764
2927
  const { toggleTrackListSidebar, isTrackListSidebarVisible } = useTrackListSidebarVisibility();
2765
2928
  const [lastLoadedTrackTitle, setLastLoadedTrackTitle] = useState6("");
2766
2929
  const loadedTrackTitle = playerTrackObject?.track.title;
2767
- useEffect7(() => {
2930
+ useEffect8(() => {
2768
2931
  if (loadedTrackTitle) setLastLoadedTrackTitle(loadedTrackTitle);
2769
2932
  }, [loadedTrackTitle]);
2770
2933
  const [uncontrolledExtended, setUncontrolledExtended] = useState6(false);
@@ -2788,7 +2951,7 @@ function PlayerPanel({
2788
2951
  onClick: () => setIsExtendRequested(true),
2789
2952
  expanded: false
2790
2953
  } : null;
2791
- return /* @__PURE__ */ jsxs23(
2954
+ return /* @__PURE__ */ jsxs25(
2792
2955
  "div",
2793
2956
  {
2794
2957
  id: PANEL_ID,
@@ -2801,7 +2964,7 @@ function PlayerPanel({
2801
2964
  ),
2802
2965
  style: isExtended ? { width: extendedWidth } : void 0,
2803
2966
  children: [
2804
- /* @__PURE__ */ jsx30(
2967
+ /* @__PURE__ */ jsx32(
2805
2968
  PanelEdgeTab,
2806
2969
  {
2807
2970
  side: "right",
@@ -2811,7 +2974,7 @@ function PlayerPanel({
2811
2974
  className: "absolute right-full top-1/2 -translate-y-1/2"
2812
2975
  }
2813
2976
  ),
2814
- /* @__PURE__ */ jsxs23(
2977
+ /* @__PURE__ */ jsxs25(
2815
2978
  "div",
2816
2979
  {
2817
2980
  className: twMerge2(
@@ -2819,15 +2982,15 @@ function PlayerPanel({
2819
2982
  isExtended ? "flex-row" : "flex-col"
2820
2983
  ),
2821
2984
  children: [
2822
- /* @__PURE__ */ jsxs23("div", { className: "flex w-[var(--layout-right-w)] shrink-0 flex-col", children: [
2823
- /* @__PURE__ */ jsx30("div", { className: "flex shrink-0 items-center justify-between gap-2 border-b border-zinc-800 p-2", children: /* @__PURE__ */ jsx30("span", { className: "min-w-0 truncate text-sm font-semibold", children: loadedTrackTitle || lastLoadedTrackTitle }) }),
2824
- /* @__PURE__ */ jsxs23("div", { className: "relative w-full shrink-0", children: [
2825
- /* @__PURE__ */ jsx30(PlayerVideoSurface, { className: "aspect-video w-full bg-black" }),
2826
- playerTrackObject?.loadError && /* @__PURE__ */ jsx30("span", { className: "absolute bottom-2 left-2 right-2 text-xs text-red-400", children: playerTrackObject.loadError })
2985
+ /* @__PURE__ */ jsxs25("div", { className: "flex w-[var(--layout-right-w)] shrink-0 flex-col", children: [
2986
+ /* @__PURE__ */ jsx32("div", { className: "flex shrink-0 items-center justify-between gap-2 border-b border-zinc-800 p-2", children: /* @__PURE__ */ jsx32("span", { className: "min-w-0 truncate text-sm font-semibold", children: loadedTrackTitle || lastLoadedTrackTitle }) }),
2987
+ /* @__PURE__ */ jsxs25("div", { className: "relative w-full shrink-0", children: [
2988
+ /* @__PURE__ */ jsx32(PlayerVideoSurface, { className: "aspect-video w-full bg-black" }),
2989
+ playerTrackObject?.loadError && /* @__PURE__ */ jsx32("span", { className: "absolute bottom-2 left-2 right-2 text-xs text-red-400", children: playerTrackObject.loadError })
2827
2990
  ] }),
2828
- /* @__PURE__ */ jsx30(PlayerControls, {})
2991
+ /* @__PURE__ */ jsx32(PlayerControls, {})
2829
2992
  ] }),
2830
- /* @__PURE__ */ jsx30(
2993
+ /* @__PURE__ */ jsx32(
2831
2994
  TrackListSidebar,
2832
2995
  {
2833
2996
  layout: "inline",
@@ -2853,11 +3016,11 @@ function PlayerPanel({
2853
3016
  import { v4 as uuidv4 } from "uuid";
2854
3017
  import { AiFillStar, AiOutlineStar } from "react-icons/ai";
2855
3018
  import { FORM_RATING_NULL_VALUE } from "@behindthemusictree/app-kit/genre-tree";
2856
- import { jsx as jsx31, jsxs as jsxs24 } from "react/jsx-runtime";
3019
+ import { jsx as jsx33, jsxs as jsxs26 } from "react/jsx-runtime";
2857
3020
  function Rating({ rating, handleChange }) {
2858
3021
  const instanceId = uuidv4();
2859
- return /* @__PURE__ */ jsx31("div", { className: "flex items-center", children: [...Array(6)].map((_, i) => /* @__PURE__ */ jsxs24("div", { className: `relative ${i === 0 ? "mr-4" : "mr-1"}`, children: [
2860
- /* @__PURE__ */ jsx31(
3022
+ return /* @__PURE__ */ jsx33("div", { className: "flex items-center", children: [...Array(6)].map((_, i) => /* @__PURE__ */ jsxs26("div", { className: `relative ${i === 0 ? "mr-4" : "mr-1"}`, children: [
3023
+ /* @__PURE__ */ jsx33(
2861
3024
  "input",
2862
3025
  {
2863
3026
  id: `star-${instanceId}-${i}`,
@@ -2869,16 +3032,16 @@ function Rating({ rating, handleChange }) {
2869
3032
  onChange: handleChange
2870
3033
  }
2871
3034
  ),
2872
- /* @__PURE__ */ jsx31("label", { htmlFor: `star-${instanceId}-${i}`, className: "cursor-pointer", children: i === 0 ? /* @__PURE__ */ jsxs24("div", { className: "flex", children: [
2873
- /* @__PURE__ */ jsx31(
3035
+ /* @__PURE__ */ jsx33("label", { htmlFor: `star-${instanceId}-${i}`, className: "cursor-pointer", children: i === 0 ? /* @__PURE__ */ jsxs26("div", { className: "flex", children: [
3036
+ /* @__PURE__ */ jsx33(
2874
3037
  AiOutlineStar,
2875
3038
  {
2876
3039
  className: `w-7 h-7 absolute top-0 left-0 ${rating === FORM_RATING_NULL_VALUE ? "text-[var(--color-accent-rating)]" : "text-[var(--color-text-muted)]"}`
2877
3040
  }
2878
3041
  ),
2879
- /* @__PURE__ */ jsx31("span", { className: "flex-grow w-8" }),
3042
+ /* @__PURE__ */ jsx33("span", { className: "flex-grow w-8" }),
2880
3043
  " "
2881
- ] }) : /* @__PURE__ */ jsx31(
3044
+ ] }) : /* @__PURE__ */ jsx33(
2882
3045
  AiFillStar,
2883
3046
  {
2884
3047
  className: `w-7 h-7 absolute top-0 left-0 ${(rating ?? -1) >= i ? "text-[var(--color-accent-rating)]" : "text-[var(--color-text-muted)]"}`
@@ -2888,12 +3051,17 @@ function Rating({ rating, handleChange }) {
2888
3051
  }
2889
3052
 
2890
3053
  export {
3054
+ useGenrePlaylistPlayback,
2891
3055
  GenrePlaylistTreePerRoot,
2892
3056
  GenreSearch,
3057
+ GenreNodeActionBand,
3058
+ GenreInfoPanelBody,
2893
3059
  PanelEdgeTab,
3060
+ useLeftDetailPanelLayout,
2894
3061
  TrackPositionPlayPause,
2895
3062
  TrackItem,
2896
3063
  GenreTreeView,
3064
+ GenreDetailPanel,
2897
3065
  TrackListSidebar,
2898
3066
  PlayerPanel,
2899
3067
  Rating,
@@ -2902,4 +3070,4 @@ export {
2902
3070
  GenreTreeViewSkeleton2 as GenreTreeViewSkeleton,
2903
3071
  GenreTreeOutlineSkeleton
2904
3072
  };
2905
- //# sourceMappingURL=chunk-5PQWJRDZ.js.map
3073
+ //# sourceMappingURL=chunk-TFLX6C5Q.js.map