@behindthemusictree/app-kit-ui 0.0.0-stage → 24.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.
@@ -0,0 +1,2297 @@
1
+ import {
2
+ BasePopup_default,
3
+ InternalErrorPopup
4
+ } from "./chunk-VDXUW7PP.js";
5
+
6
+ // src/genre-tree/GenreTreeView.tsx
7
+ import { useCallback as useCallback5, useEffect as useEffect4, useMemo as useMemo6, useRef as useRef4, useState as useState4 } from "react";
8
+ import { Plus as Plus2 } from "lucide-react";
9
+ import { IconTextButton, Button, SegmentedControl } from "@behindthemusictree/ui";
10
+ import {
11
+ GenreTreeWheelSkeleton,
12
+ GenreTreeViewSkeleton
13
+ } from "@behindthemusictree/genre-tree-view";
14
+ import {
15
+ useArtistSearch
16
+ } from "@behindthemusictree/app-kit/genre-tree";
17
+ import { useGenreTreeViewState, useArtist as useArtist2 } from "@behindthemusictree/app-kit/genre-tree";
18
+ import { usePlayer as usePlayer8 } from "@behindthemusictree/app-kit/player";
19
+ import { usePopup as usePopup4 } from "@behindthemusictree/app-kit/popup";
20
+
21
+ // src/genre-tree/GenreDetailExtras.tsx
22
+ import { Skeleton as Skeleton2 } from "@behindthemusictree/ui";
23
+ import { useFetchGenreOverview } from "@behindthemusictree/app-kit/genre-tree";
24
+
25
+ // src/genre-tree/ClampedText.tsx
26
+ import { useLayoutEffect, useRef, useState } from "react";
27
+ import { Fragment, jsx, jsxs } from "react/jsx-runtime";
28
+ function ClampedText({ children, className = "" }) {
29
+ const ref = useRef(null);
30
+ const [expanded, setExpanded] = useState(false);
31
+ const [overflows, setOverflows] = useState(false);
32
+ useLayoutEffect(() => {
33
+ const element = ref.current;
34
+ if (!element || expanded) return;
35
+ setOverflows(element.scrollHeight > element.clientHeight);
36
+ }, [children, expanded]);
37
+ return /* @__PURE__ */ jsxs(Fragment, { children: [
38
+ /* @__PURE__ */ jsx("p", { ref, className: `${className} ${expanded ? "" : "line-clamp-3"}`, children }),
39
+ (overflows || expanded) && /* @__PURE__ */ jsx(
40
+ "button",
41
+ {
42
+ type: "button",
43
+ "aria-expanded": expanded,
44
+ onClick: () => setExpanded((value) => !value),
45
+ className: "mt-1 text-xs text-[var(--color-text-muted)] underline hover:text-[var(--color-text)]",
46
+ children: expanded ? "Show less" : "Show more"
47
+ }
48
+ )
49
+ ] });
50
+ }
51
+
52
+ // src/genre-tree/GenrePlaylistTracks.tsx
53
+ import { useEffect, useRef as useRef2 } from "react";
54
+ import { useInfiniteQuery } from "@tanstack/react-query";
55
+ import { z } from "zod";
56
+ import { FaPlay, FaPause } from "react-icons/fa";
57
+ import { RingLoader, Skeleton } from "@behindthemusictree/ui";
58
+ import { useSession } from "@behindthemusictree/app-kit/auth";
59
+ import { useFetchWrapper } from "@behindthemusictree/app-kit/transport";
60
+ import { PaginatedResponseSchema } from "@behindthemusictree/app-kit/transport";
61
+ import { parseWithLog } from "@behindthemusictree/app-kit/transport";
62
+ import { usePlayer } from "@behindthemusictree/app-kit/player";
63
+ import { PlayStates } from "@behindthemusictree/app-kit/player";
64
+ import { usePopup } from "@behindthemusictree/app-kit/popup";
65
+
66
+ // src/genre-tree/UnplayableReason.tsx
67
+ import { FaYoutube } from "react-icons/fa";
68
+ import { NO_VIDEO_LABEL } from "@behindthemusictree/app-kit/genre-tree";
69
+ import { Fragment as Fragment2, jsx as jsx2, jsxs as jsxs2 } from "react/jsx-runtime";
70
+ function UnplayableReason({ reason }) {
71
+ if (reason !== NO_VIDEO_LABEL) return /* @__PURE__ */ jsx2(Fragment2, { children: reason });
72
+ return /* @__PURE__ */ jsxs2("span", { role: "img", "aria-label": reason, className: "relative inline-flex shrink-0 items-center", children: [
73
+ /* @__PURE__ */ jsx2(FaYoutube, { size: 16, "aria-hidden": "true" }),
74
+ /* @__PURE__ */ jsx2("span", { "aria-hidden": "true", className: "absolute left-1/2 top-1/2 h-0.5 w-[130%] -translate-x-1/2 -translate-y-1/2 -rotate-45 rounded bg-current" })
75
+ ] });
76
+ }
77
+
78
+ // src/genre-tree/GenrePlaylistTracks.tsx
79
+ import { ErrorCode } from "@behindthemusictree/app-kit/transport";
80
+ import { genrePlaylistQueryKeys } from "@behindthemusictree/app-kit/genre-tree";
81
+ import { libraryEndpoints, libraryQueryKeys } from "@behindthemusictree/app-kit/genre-tree";
82
+ import { isSong, songUnplayableReason, SongDetailedSchema } from "@behindthemusictree/app-kit/genre-tree";
83
+ import { useTrackList } from "@behindthemusictree/app-kit/genre-tree";
84
+
85
+ // src/genre-tree/MusicBrainzRecordingLink.tsx
86
+ import { FaExternalLinkAlt } from "react-icons/fa";
87
+ import { musicBrainzRecordingUrl } from "@behindthemusictree/app-kit/genre-tree";
88
+ import { jsx as jsx3 } from "react/jsx-runtime";
89
+ function MusicBrainzRecordingLink({ song }) {
90
+ const url = musicBrainzRecordingUrl(song);
91
+ if (!url) return null;
92
+ return /* @__PURE__ */ jsx3(
93
+ "a",
94
+ {
95
+ href: url,
96
+ target: "_blank",
97
+ rel: "noopener noreferrer",
98
+ "aria-label": `View ${song.title} on MusicBrainz`,
99
+ title: "View on MusicBrainz",
100
+ className: "inline-flex shrink-0 items-center opacity-50 hover:opacity-100 focus-visible:opacity-100",
101
+ onClick: (event) => event.stopPropagation(),
102
+ children: /* @__PURE__ */ jsx3(FaExternalLinkAlt, { size: 10 })
103
+ }
104
+ );
105
+ }
106
+
107
+ // src/genre-tree/GenrePlaylistTracks.tsx
108
+ import { ArtistNames } from "@behindthemusictree/app-kit/genre-tree";
109
+ import { capitalize, useTrackNoun } from "@behindthemusictree/app-kit/track-noun";
110
+ import { jsx as jsx4, jsxs as jsxs3 } from "react/jsx-runtime";
111
+ var CATALOGUE_PAGE_SIZE = 100;
112
+ var SongPageSchema = PaginatedResponseSchema(SongDetailedSchema).extend({
113
+ overallTotal: z.number().nullable(),
114
+ page: z.number().nullable(),
115
+ totalPages: z.number().nullable()
116
+ });
117
+ var TEXT_CELL = "min-w-0 flex-1 truncate [@container(min-width:40rem)]:flex [@container(min-width:40rem)]:gap-2";
118
+ var TITLE_CELL = "[@container(min-width:40rem)]:flex-[2] [@container(min-width:40rem)]:truncate";
119
+ var COLUMN_CELL = "[@container(min-width:40rem)]:flex-1 [@container(min-width:40rem)]:truncate";
120
+ var ALBUM_CELL = `hidden [@container(min-width:40rem)]:block ${COLUMN_CELL}`;
121
+ var SEPARATOR = "[@container(min-width:40rem)]:hidden";
122
+ var nextCursor = (next) => {
123
+ const cursor = new URL(next).searchParams.get("cursor");
124
+ if (!cursor) throw new Error(`Songs next link has no cursor: ${next}`);
125
+ return cursor;
126
+ };
127
+ function GenrePlaylistTracks({
128
+ genreUuid,
129
+ genrePlaylist,
130
+ scope,
131
+ getBackendBaseUrl,
132
+ renderArtistLink,
133
+ renderSongLeading,
134
+ renderSongActions,
135
+ renderTracksToolbar,
136
+ artist = null
137
+ }) {
138
+ const { fetchGenrePlaylistTracksPage, playNewTrackListFromGenrePlaylist, playNewTrackListFromArtist } = useTrackList();
139
+ const { session, sessionRestored } = useSession();
140
+ const { playerTrackObject, playState, handlePlayPauseAction } = usePlayer();
141
+ const { showPopup } = usePopup();
142
+ const trackNoun = useTrackNoun();
143
+ const { uuid } = genrePlaylist;
144
+ const { fetch } = useFetchWrapper(getBackendBaseUrl);
145
+ const { data, isPending, isError, refetch, hasNextPage, isFetchingNextPage, fetchNextPage } = useInfiniteQuery({
146
+ queryKey: scope === "me" ? genrePlaylistQueryKeys.me.tracks(uuid) : artist ? [...libraryQueryKeys.reference.songs.byGenre(getBackendBaseUrl(), genreUuid), "artist", artist.uuid] : libraryQueryKeys.reference.songs.byGenre(getBackendBaseUrl(), genreUuid),
147
+ queryFn: async ({ pageParam }) => {
148
+ if (scope === "me") return fetchGenrePlaylistTracksPage(uuid, scope, Number(pageParam));
149
+ const response = await fetch(
150
+ libraryEndpoints.reference.songs.list(),
151
+ true,
152
+ false,
153
+ {},
154
+ { genre: genreUuid, ...artist && { artist: artist.uuid }, pageSize: CATALOGUE_PAGE_SIZE, withTotal: false, ...pageParam && { cursor: pageParam } }
155
+ );
156
+ const parsed = parseWithLog(SongPageSchema, response, "GenrePlaylistTracks");
157
+ return { tracks: parsed.results, total: parsed.overallTotal, nextPage: parsed.next ? nextCursor(parsed.next) : null };
158
+ },
159
+ initialPageParam: scope === "me" ? 1 : "",
160
+ getNextPageParam: (lastPage) => lastPage.nextPage ?? void 0,
161
+ enabled: scope !== "me" || sessionRestored && !!session?.accessToken
162
+ });
163
+ const sentinelRef = useRef2(null);
164
+ useEffect(() => {
165
+ const sentinel = sentinelRef.current;
166
+ if (!hasNextPage || isFetchingNextPage || !sentinel) return;
167
+ const observer = new IntersectionObserver((entries) => {
168
+ if (entries.some((entry) => entry.isIntersecting)) {
169
+ void fetchNextPage();
170
+ }
171
+ });
172
+ observer.observe(sentinel);
173
+ return () => observer.disconnect();
174
+ }, [hasNextPage, isFetchingNextPage, fetchNextPage]);
175
+ const pages = data?.pages ?? [];
176
+ const tracks = pages.flatMap((page) => page.tracks);
177
+ const total = pages[0]?.total ?? void 0;
178
+ const play = (startSongUuid) => {
179
+ const started = artist ? playNewTrackListFromArtist(artist, scope, { startSongUuid, ordering: "title", genreUuid }) : playNewTrackListFromGenrePlaylist(genrePlaylist, scope, { startSongUuid });
180
+ started.catch((error) => {
181
+ console.error("Failed to play genre playlist:", error);
182
+ showPopup(/* @__PURE__ */ jsx4(InternalErrorPopup, { errorCode: ErrorCode.CLIENT_UNKNOWN }));
183
+ });
184
+ };
185
+ return /* @__PURE__ */ jsxs3("div", { className: "gtv-info-panel-children", children: [
186
+ /* @__PURE__ */ jsxs3("span", { className: "gtv-info-panel-children-title", children: [
187
+ capitalize(trackNoun.plural),
188
+ total !== void 0 && ` (${total})`
189
+ ] }),
190
+ renderTracksToolbar?.(),
191
+ isPending ? /* @__PURE__ */ jsx4(Skeleton, { className: "h-4 w-3/4", "aria-busy": "true" }) : isError && tracks.length === 0 ? /* @__PURE__ */ jsxs3("p", { role: "alert", children: [
192
+ "Couldn't load ",
193
+ trackNoun.plural,
194
+ ".",
195
+ " ",
196
+ /* @__PURE__ */ jsx4("button", { type: "button", className: "underline", onClick: () => void refetch(), children: "Retry" })
197
+ ] }) : tracks.length === 0 ? /* @__PURE__ */ jsx4("p", { children: "\u2014" }) : /* @__PURE__ */ jsxs3("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: [
198
+ tracks.map((track, index) => {
199
+ const label = track.artists?.length ? `${track.title} \u2014 ${track.artists.map((artist2) => artist2.name).join(", ")}` : track.title;
200
+ const isCurrent = playerTrackObject?.track.id === track.uuid;
201
+ const isPlaying = isCurrent && playState === PlayStates.PLAYING;
202
+ const text = /* @__PURE__ */ jsxs3("span", { className: TEXT_CELL, children: [
203
+ /* @__PURE__ */ jsx4("span", { className: TITLE_CELL, children: track.title }),
204
+ track.artists?.length ? /* @__PURE__ */ jsx4("span", { className: SEPARATOR, children: " \u2014 " }) : null,
205
+ /* @__PURE__ */ jsx4("span", { className: COLUMN_CELL, children: track.artists?.length ? /* @__PURE__ */ jsx4(ArtistNames, { artists: track.artists, renderArtistLink }) : null }),
206
+ track.album && /* @__PURE__ */ jsx4("span", { className: ALBUM_CELL, children: track.album.name })
207
+ ] });
208
+ const leading = renderSongLeading?.(track, index);
209
+ const actions = renderSongActions?.(track);
210
+ const unplayableReason = isSong(track) ? songUnplayableReason(track) : null;
211
+ if (unplayableReason) {
212
+ return /* @__PURE__ */ jsxs3("li", { className: "flex min-h-8 items-center gap-1 px-1 opacity-50", title: unplayableReason, children: [
213
+ leading,
214
+ /* @__PURE__ */ jsx4(
215
+ "button",
216
+ {
217
+ type: "button",
218
+ disabled: true,
219
+ "aria-disabled": "true",
220
+ "aria-label": `${label} \u2014 ${unplayableReason}`,
221
+ className: "flex h-6 w-6 shrink-0 items-center justify-center rounded-full opacity-0 cursor-not-allowed",
222
+ children: /* @__PURE__ */ jsx4(FaPlay, { size: 10 })
223
+ }
224
+ ),
225
+ text,
226
+ isSong(track) && /* @__PURE__ */ jsx4(MusicBrainzRecordingLink, { song: track }),
227
+ /* @__PURE__ */ jsx4("span", { className: "shrink-0 text-xs", children: /* @__PURE__ */ jsx4(UnplayableReason, { reason: unplayableReason }) }),
228
+ actions
229
+ ] }, track.uuid);
230
+ }
231
+ return /* @__PURE__ */ jsxs3(
232
+ "li",
233
+ {
234
+ 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)]" : ""}`,
235
+ children: [
236
+ leading,
237
+ /* @__PURE__ */ jsx4(
238
+ "button",
239
+ {
240
+ type: "button",
241
+ "aria-label": `${isPlaying ? "Pause" : "Play"} ${label}`,
242
+ 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"}`,
243
+ onClick: () => isCurrent ? handlePlayPauseAction() : play(track.uuid),
244
+ children: isCurrent && playState === PlayStates.LOADING ? /* @__PURE__ */ jsx4(RingLoader, { size: 10 }) : isPlaying ? /* @__PURE__ */ jsx4(FaPause, { size: 10 }) : /* @__PURE__ */ jsx4(FaPlay, { size: 10 })
245
+ }
246
+ ),
247
+ text,
248
+ isSong(track) && /* @__PURE__ */ jsx4(MusicBrainzRecordingLink, { song: track }),
249
+ actions
250
+ ]
251
+ },
252
+ track.uuid
253
+ );
254
+ }),
255
+ hasNextPage && /* @__PURE__ */ jsx4("li", { ref: sentinelRef, "aria-hidden": "true", className: "h-4" })
256
+ ] })
257
+ ] });
258
+ }
259
+
260
+ // src/genre-tree/GenreDetailExtras.tsx
261
+ import { useTrackNoun as useTrackNoun2 } from "@behindthemusictree/app-kit/track-noun";
262
+ import { Fragment as Fragment3, jsx as jsx5, jsxs as jsxs4 } from "react/jsx-runtime";
263
+ function GenreDetailExtras({
264
+ genreUuid,
265
+ genrePlaylist,
266
+ scope,
267
+ getBackendBaseUrl,
268
+ criteriaOverviewSchema,
269
+ renderGenreDetailExtras,
270
+ renderArtistLink,
271
+ renderSummary,
272
+ renderEssentialTracks,
273
+ family,
274
+ ...trackSlots
275
+ }) {
276
+ const trackNoun = useTrackNoun2();
277
+ const { data: overview, isPending } = useFetchGenreOverview(
278
+ genreUuid,
279
+ scope,
280
+ getBackendBaseUrl,
281
+ criteriaOverviewSchema
282
+ );
283
+ if (isPending) {
284
+ return /* @__PURE__ */ jsxs4(Fragment3, { children: [
285
+ /* @__PURE__ */ jsxs4("div", { className: "gtv-info-panel-children", "aria-busy": "true", "data-testid": "genre-detail-extras-skeleton", children: [
286
+ /* @__PURE__ */ jsx5("span", { className: "gtv-info-panel-children-title", children: "Summary" }),
287
+ /* @__PURE__ */ jsx5(Skeleton2, { className: "h-4 w-3/4" })
288
+ ] }),
289
+ family
290
+ ] });
291
+ }
292
+ if (!overview) return /* @__PURE__ */ jsx5(Fragment3, { children: family });
293
+ const { summary, description, summarySourceUrl, essentialSongs } = overview;
294
+ return /* @__PURE__ */ jsxs4(Fragment3, { children: [
295
+ description && /* @__PURE__ */ jsx5("div", { className: "px-3.5 py-2.5", children: /* @__PURE__ */ jsx5(ClampedText, { className: "text-[0.8125rem] leading-[1.45] text-[var(--color-text-muted)]", children: description }, genreUuid) }),
296
+ renderSummary ? renderSummary(overview) : /* @__PURE__ */ jsxs4("div", { className: "gtv-info-panel-children text-[0.8125rem] leading-[1.45] text-[var(--color-text)]", children: [
297
+ /* @__PURE__ */ jsx5("span", { className: "gtv-info-panel-children-title", children: "Summary" }),
298
+ /* @__PURE__ */ jsx5(ClampedText, { className: "mt-1", children: summary ?? "\u2014" }, genreUuid),
299
+ summarySourceUrl && /* @__PURE__ */ jsx5(
300
+ "a",
301
+ {
302
+ href: summarySourceUrl,
303
+ target: "_blank",
304
+ rel: "noopener noreferrer",
305
+ className: "mt-1 inline-block text-xs text-[var(--color-text-muted)] underline hover:text-[var(--color-text)]",
306
+ children: "Source: Wikipedia"
307
+ }
308
+ )
309
+ ] }),
310
+ renderSummary && summarySourceUrl && /* @__PURE__ */ jsx5(
311
+ "a",
312
+ {
313
+ href: summarySourceUrl,
314
+ target: "_blank",
315
+ rel: "noopener noreferrer",
316
+ className: "mx-3.5 mb-2.5 inline-block text-xs text-[var(--color-text-muted)] underline hover:text-[var(--color-text)]",
317
+ children: "Source: Wikipedia"
318
+ }
319
+ ),
320
+ family,
321
+ renderGenreDetailExtras?.(overview),
322
+ renderEssentialTracks ? renderEssentialTracks(overview) : essentialSongs && /* @__PURE__ */ jsxs4("div", { className: "gtv-info-panel-children text-[0.8125rem] leading-[1.45] text-[var(--color-text)]", children: [
323
+ /* @__PURE__ */ jsxs4("span", { className: "gtv-info-panel-children-title", children: [
324
+ "Essential ",
325
+ trackNoun.plural
326
+ ] }),
327
+ essentialSongs.length > 0 ? /* @__PURE__ */ jsx5("ul", { className: "mt-1 divide-y divide-[var(--color-border)]", children: essentialSongs.map((track) => /* @__PURE__ */ jsx5("li", { className: "py-1", children: track.title }, track.uuid)) }) : /* @__PURE__ */ jsx5("p", { className: "mt-1", children: "\u2014" })
328
+ ] }),
329
+ /* @__PURE__ */ jsx5(
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 jsx6 } from "react/jsx-runtime";
387
+ function GenrePlaylistTreePerRoot({
388
+ scope,
389
+ className,
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;
436
+ }
437
+ if (genrePlaylist.tracksCount === 0) {
438
+ return;
439
+ }
440
+ play(genrePlaylist).catch((error) => {
441
+ console.error("Failed to load genre playlist tracks:", error);
442
+ showPopup(/* @__PURE__ */ jsx6(InternalErrorPopup, { errorCode: ErrorCode2.CLIENT_UNKNOWN }));
443
+ });
444
+ },
445
+ [
446
+ genrePlaylistTreePerRoot,
447
+ playingNodeId,
448
+ isPlaying,
449
+ setIsPlaying,
450
+ play,
451
+ showPopup
452
+ ]
453
+ );
454
+ const handleAddChild = useCallback2(
455
+ (parentId) => {
456
+ const genrePlaylist = genrePlaylistTreePerRoot.find((g) => g.uuid === parentId);
457
+ if (!genrePlaylist?.criteria) return;
458
+ handleGenreCreationAction(genrePlaylist.criteria);
459
+ },
460
+ [genrePlaylistTreePerRoot, handleGenreCreationAction]
461
+ );
462
+ const handleRenameRequest = useCallback2(
463
+ (node) => {
464
+ const genrePlaylist = genrePlaylistTreePerRoot.find((g) => g.uuid === node.id);
465
+ if (!genrePlaylist?.criteria) return;
466
+ handleGenreRenameAction(genrePlaylist.criteria);
467
+ },
468
+ [genrePlaylistTreePerRoot, handleGenreRenameAction]
469
+ );
470
+ const handleDeleteRequest = useCallback2((node) => {
471
+ if (confirm(`Are you sure you want to delete "${node.name}"?`)) {
472
+ }
473
+ }, []);
474
+ return /* @__PURE__ */ jsx6(
475
+ GenreTree,
476
+ {
477
+ className,
478
+ nodes,
479
+ itemCountLabel: `${capitalize2(trackNoun.singular)} count`,
480
+ rootColor: getGenreTreeColor(rootUuid),
481
+ playingNodeId,
482
+ playState: isPlaying ? "playing" : "paused",
483
+ onPlayPause: handlePlayPause,
484
+ onAddChild: readOnly ? void 0 : handleAddChild,
485
+ onRenameRequest: readOnly ? void 0 : handleRenameRequest,
486
+ onDeleteRequest: readOnly ? void 0 : handleDeleteRequest,
487
+ additionalActions,
488
+ onNodeClick,
489
+ onNodeHover,
490
+ renderInfoPanelBody,
491
+ selectedNodeId,
492
+ onSelectedNodeChange,
493
+ hideInfoPanelClose,
494
+ hideInfoPanelTitle,
495
+ showToolbar,
496
+ wheelZoom,
497
+ controlsOrientation,
498
+ defaultViewport,
499
+ onViewportChange,
500
+ ...infoPanelCuration,
501
+ readOnly
502
+ }
503
+ );
504
+ }
505
+
506
+ // src/genre-tree/playlist-tree/TreeWheel.tsx
507
+ import { useCallback as useCallback3, useMemo as useMemo3 } from "react";
508
+ import {
509
+ GenreTreeWheel
510
+ } 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";
514
+ import { capitalize as capitalize3, useTrackNoun as useTrackNoun4 } from "@behindthemusictree/app-kit/track-noun";
515
+ import { jsx as jsx7 } from "react/jsx-runtime";
516
+ function GenrePlaylistTreeWheel({
517
+ scope,
518
+ className,
519
+ genrePlaylists,
520
+ handleGenreCreationAction,
521
+ handleGenreRenameAction,
522
+ additionalActions,
523
+ onNodeClick,
524
+ onNodeHover,
525
+ renderInfoPanelBody,
526
+ selectedNodeId,
527
+ onSelectedNodeChange,
528
+ hideInfoPanelClose,
529
+ hideInfoPanelTitle,
530
+ readOnly = false,
531
+ allowWheelRotation,
532
+ showToolbar,
533
+ controlsOrientation,
534
+ defaultViewport,
535
+ onViewportChange,
536
+ playback,
537
+ ...infoPanelCuration
538
+ }) {
539
+ const { isPlaying, setIsPlaying } = usePlayer3();
540
+ const defaultPlayback = useGenrePlaylistPlayback({ scope, genrePlaylists });
541
+ const { playingNodeId, play } = playback ?? defaultPlayback;
542
+ const trackNoun = useTrackNoun4();
543
+ const { showPopup } = usePopup3();
544
+ const nodes = useMemo3(
545
+ () => genrePlaylists.map((genrePlaylist) => ({
546
+ id: genrePlaylist.uuid,
547
+ parentId: genrePlaylist.parent?.uuid ?? null,
548
+ name: genrePlaylist.name,
549
+ itemCount: genrePlaylist.tracksCount,
550
+ actionable: Boolean(genrePlaylist.criteria),
551
+ side: genrePlaylist.criteria?.side ?? void 0,
552
+ wikidataId: genrePlaylist.criteria?.wikidataId ?? null
553
+ })),
554
+ [genrePlaylists]
555
+ );
556
+ const handlePlayPause = useCallback3(
557
+ (nodeId) => {
558
+ const genrePlaylist = genrePlaylists.find((g) => g.uuid === nodeId);
559
+ if (!genrePlaylist) return;
560
+ if (nodeId === playingNodeId) {
561
+ setIsPlaying(!isPlaying);
562
+ return;
563
+ }
564
+ if (genrePlaylist.tracksCount === 0) {
565
+ return;
566
+ }
567
+ play(genrePlaylist).catch((error) => {
568
+ console.error("Failed to load genre playlist tracks:", error);
569
+ showPopup(/* @__PURE__ */ jsx7(InternalErrorPopup, { errorCode: ErrorCode3.CLIENT_UNKNOWN }));
570
+ });
571
+ },
572
+ [
573
+ genrePlaylists,
574
+ playingNodeId,
575
+ isPlaying,
576
+ setIsPlaying,
577
+ play,
578
+ showPopup
579
+ ]
580
+ );
581
+ const handleAddChild = useCallback3(
582
+ (parentId) => {
583
+ const genrePlaylist = genrePlaylists.find((g) => g.uuid === parentId);
584
+ if (!genrePlaylist?.criteria) return;
585
+ handleGenreCreationAction(genrePlaylist.criteria);
586
+ },
587
+ [genrePlaylists, handleGenreCreationAction]
588
+ );
589
+ const handleRenameRequest = useCallback3(
590
+ (node) => {
591
+ const genrePlaylist = genrePlaylists.find((g) => g.uuid === node.id);
592
+ if (!genrePlaylist?.criteria) return;
593
+ handleGenreRenameAction(genrePlaylist.criteria);
594
+ },
595
+ [genrePlaylists, handleGenreRenameAction]
596
+ );
597
+ const handleDeleteRequest = useCallback3((node) => {
598
+ if (confirm(`Are you sure you want to delete "${node.name}"?`)) {
599
+ }
600
+ }, []);
601
+ return /* @__PURE__ */ jsx7(
602
+ GenreTreeWheel,
603
+ {
604
+ className,
605
+ nodes,
606
+ itemCountLabel: `${capitalize3(trackNoun.singular)} count`,
607
+ playingNodeId,
608
+ playState: isPlaying ? "playing" : "paused",
609
+ onPlayPause: handlePlayPause,
610
+ onAddChild: readOnly ? void 0 : handleAddChild,
611
+ onRenameRequest: readOnly ? void 0 : handleRenameRequest,
612
+ onDeleteRequest: readOnly ? void 0 : handleDeleteRequest,
613
+ additionalActions,
614
+ onNodeClick,
615
+ onNodeHover,
616
+ renderInfoPanelBody,
617
+ selectedNodeId,
618
+ onSelectedNodeChange,
619
+ hideInfoPanelClose,
620
+ hideInfoPanelTitle,
621
+ allowWheelRotation,
622
+ showToolbar,
623
+ controlsOrientation,
624
+ defaultViewport,
625
+ onViewportChange,
626
+ ...infoPanelCuration,
627
+ readOnly
628
+ }
629
+ );
630
+ }
631
+
632
+ // src/genre-tree/playlist-tree/TreeWheelRadialPopCore.tsx
633
+ import { useCallback as useCallback4, useMemo as useMemo4 } from "react";
634
+ import {
635
+ GenreTreeOutline,
636
+ GenreTreeWheelRadialPopCore
637
+ } from "@behindthemusictree/genre-tree-view";
638
+ import { usePlayer as usePlayer4 } from "@behindthemusictree/app-kit/player";
639
+ import { capitalize as capitalize4, useTrackNoun as useTrackNoun5 } from "@behindthemusictree/app-kit/track-noun";
640
+ import { jsx as jsx8 } from "react/jsx-runtime";
641
+ function GenrePlaylistTreeWheelRadialPopCore(props) {
642
+ const {
643
+ scope,
644
+ className,
645
+ genrePlaylists,
646
+ handleGenreCreationAction,
647
+ handleGenreRenameAction,
648
+ additionalActions,
649
+ onNodeClick,
650
+ onNodeHover,
651
+ renderInfoPanelBody,
652
+ selectedNodeId,
653
+ onSelectedNodeChange,
654
+ hideInfoPanelClose,
655
+ hideInfoPanelTitle,
656
+ readOnly = false,
657
+ allowWheelRotation,
658
+ showToolbar,
659
+ controlsOrientation,
660
+ outline = false,
661
+ defaultViewport,
662
+ onViewportChange,
663
+ playback
664
+ } = props;
665
+ const focus = props;
666
+ const infoPanelCuration = {
667
+ editMode: props.editMode,
668
+ onEditModeChange: props.onEditModeChange,
669
+ renderTitleEditor: props.renderTitleEditor,
670
+ renderParentsEditor: props.renderParentsEditor,
671
+ renderDangerZone: props.renderDangerZone,
672
+ expanded: props.expanded,
673
+ onExpandedChange: props.onExpandedChange,
674
+ collapsed: props.collapsed,
675
+ hideExpandButton: props.hideExpandButton,
676
+ overlayPanel: props.overlayPanel
677
+ };
678
+ const { isPlaying, setIsPlaying } = usePlayer4();
679
+ const defaultPlayback = useGenrePlaylistPlayback({ scope, genrePlaylists });
680
+ const { playingNodeId, play } = playback ?? defaultPlayback;
681
+ const trackNoun = useTrackNoun5();
682
+ const nodes = useMemo4(
683
+ () => genrePlaylists.map((genrePlaylist) => ({
684
+ id: genrePlaylist.uuid,
685
+ parentId: genrePlaylist.parent?.uuid ?? null,
686
+ name: genrePlaylist.name,
687
+ itemCount: genrePlaylist.tracksCount,
688
+ actionable: Boolean(genrePlaylist.criteria),
689
+ detached: !genrePlaylist.criteria,
690
+ side: genrePlaylist.criteria?.side ?? void 0,
691
+ wikidataId: genrePlaylist.criteria?.wikidataId ?? null,
692
+ umbrella: genrePlaylist.isUmbrella
693
+ })),
694
+ [genrePlaylists]
695
+ );
696
+ const handlePlayPause = useCallback4(
697
+ (nodeId) => {
698
+ const genrePlaylist = genrePlaylists.find((g) => g.uuid === nodeId);
699
+ if (!genrePlaylist) return;
700
+ if (nodeId === playingNodeId) {
701
+ setIsPlaying(!isPlaying);
702
+ return;
703
+ }
704
+ if (genrePlaylist.tracksCount === 0) {
705
+ return;
706
+ }
707
+ play(genrePlaylist).catch((error) => {
708
+ console.error("Failed to load genre playlist tracks:", error);
709
+ });
710
+ },
711
+ [genrePlaylists, playingNodeId, isPlaying, setIsPlaying, play]
712
+ );
713
+ const handleAddChild = useCallback4(
714
+ (parentId) => {
715
+ const genrePlaylist = genrePlaylists.find((g) => g.uuid === parentId);
716
+ if (!genrePlaylist?.criteria) return;
717
+ handleGenreCreationAction(genrePlaylist.criteria);
718
+ },
719
+ [genrePlaylists, handleGenreCreationAction]
720
+ );
721
+ const handleRenameRequest = useCallback4(
722
+ (node) => {
723
+ const genrePlaylist = genrePlaylists.find((g) => g.uuid === node.id);
724
+ if (!genrePlaylist?.criteria) return;
725
+ handleGenreRenameAction(genrePlaylist.criteria);
726
+ },
727
+ [genrePlaylists, handleGenreRenameAction]
728
+ );
729
+ const handleDeleteRequest = useCallback4((node) => {
730
+ if (confirm(`Are you sure you want to delete "${node.name}"?`)) {
731
+ }
732
+ }, []);
733
+ const treeProps = {
734
+ className,
735
+ nodes,
736
+ itemCountLabel: `${capitalize4(trackNoun.singular)} count`,
737
+ playingNodeId,
738
+ playState: isPlaying ? "playing" : "paused",
739
+ onPlayPause: handlePlayPause,
740
+ onAddChild: readOnly ? void 0 : handleAddChild,
741
+ onRenameRequest: readOnly ? void 0 : handleRenameRequest,
742
+ onDeleteRequest: readOnly ? void 0 : handleDeleteRequest,
743
+ additionalActions,
744
+ onNodeClick,
745
+ onNodeHover,
746
+ renderInfoPanelBody,
747
+ selectedNodeId,
748
+ onSelectedNodeChange,
749
+ hideInfoPanelClose,
750
+ hideInfoPanelTitle,
751
+ showToolbar,
752
+ ...infoPanelCuration,
753
+ readOnly
754
+ };
755
+ return outline ? /* @__PURE__ */ jsx8(GenreTreeOutline, { ...treeProps }) : /* @__PURE__ */ jsx8(
756
+ GenreTreeWheelRadialPopCore,
757
+ {
758
+ ...treeProps,
759
+ allowWheelRotation,
760
+ controlsOrientation,
761
+ defaultViewport,
762
+ onViewportChange,
763
+ ...focus.onFocusGenreChange ? { focusNodeId: focus.focusGenreUuid, onFocusChange: focus.onFocusGenreChange } : { focusNodeId: void 0, onFocusChange: void 0 }
764
+ }
765
+ );
766
+ }
767
+
768
+ // src/genre-tree/GenreTreeWheelHandoff.tsx
769
+ import { useEffect as useEffect2, useRef as useRef3, useState as useState2 } from "react";
770
+ import { jsx as jsx9, jsxs as jsxs5 } from "react/jsx-runtime";
771
+ function GenreTreeWheelHandoff({ skeleton, children, onRevealedChange }) {
772
+ const [revealed, setRevealed] = useState2(false);
773
+ const onRevealedChangeRef = useRef3(onRevealedChange);
774
+ onRevealedChangeRef.current = onRevealedChange;
775
+ useEffect2(() => {
776
+ let innerFrame = 0;
777
+ const outerFrame = requestAnimationFrame(() => {
778
+ innerFrame = requestAnimationFrame(() => {
779
+ setRevealed(true);
780
+ onRevealedChangeRef.current?.(true);
781
+ });
782
+ });
783
+ return () => {
784
+ onRevealedChangeRef.current?.(false);
785
+ cancelAnimationFrame(outerFrame);
786
+ cancelAnimationFrame(innerFrame);
787
+ };
788
+ }, []);
789
+ return /* @__PURE__ */ jsxs5("div", { className: "relative w-full h-full", children: [
790
+ /* @__PURE__ */ jsx9(
791
+ "div",
792
+ {
793
+ className: "absolute inset-0 transition-opacity duration-150 ease-out",
794
+ style: { opacity: revealed ? 1 : 0, pointerEvents: revealed ? "auto" : "none" },
795
+ "aria-hidden": !revealed,
796
+ children
797
+ }
798
+ ),
799
+ !revealed && /* @__PURE__ */ jsx9("div", { className: "absolute inset-0", "aria-hidden": "true", children: skeleton })
800
+ ] });
801
+ }
802
+
803
+ // src/genre-tree/GenreSearch.tsx
804
+ import { ArrowLeft, Search, X } from "lucide-react";
805
+ import { Input } from "@behindthemusictree/ui";
806
+ import { useGenreSearch } from "@behindthemusictree/app-kit/genre-tree";
807
+ import { jsx as jsx10, jsxs as jsxs6 } from "react/jsx-runtime";
808
+ function GenreSearch({
809
+ genrePlaylists,
810
+ onSelect,
811
+ selectedName = null,
812
+ onClear,
813
+ onPrevious,
814
+ placeholder,
815
+ onQueryChange,
816
+ extraResults
817
+ }) {
818
+ const { query, setQuery, results, canClear, clear, select } = useGenreSearch({
819
+ genrePlaylists,
820
+ onSelect,
821
+ selectedName,
822
+ onClear
823
+ });
824
+ const extraItems = query !== "" && query !== selectedName ? extraResults?.items ?? [] : [];
825
+ const label = extraResults ? "Search" : "Search a genre";
826
+ const showList = results.length > 0 || extraItems.length > 0;
827
+ const heading = "px-3 pt-2 pb-1 text-xs font-medium uppercase text-[var(--color-text-muted)]";
828
+ return /* @__PURE__ */ jsxs6("div", { className: "genre-search relative w-[calc(var(--layout-detail-panel-w,25.5rem)-2*var(--layout-panel-inset,1rem))] shrink-0", children: [
829
+ onPrevious && /* @__PURE__ */ jsx10(
830
+ "button",
831
+ {
832
+ type: "button",
833
+ "aria-label": "Previous",
834
+ title: "Previous",
835
+ className: "genre-search-previous absolute left-2 top-1/2 z-10 hidden p-1.5 text-[var(--color-text-muted)] rounded-full -translate-y-1/2 hover:bg-[var(--color-surface-muted)] hover:text-[var(--color-text)]",
836
+ onClick: onPrevious,
837
+ children: /* @__PURE__ */ jsx10(ArrowLeft, { "aria-hidden": "true", className: "w-4 h-4" })
838
+ }
839
+ ),
840
+ /* @__PURE__ */ jsx10(
841
+ Input,
842
+ {
843
+ type: "text",
844
+ value: query,
845
+ onChange: (event) => {
846
+ setQuery(event.target.value);
847
+ onQueryChange?.(event.target.value);
848
+ },
849
+ placeholder: placeholder ?? `${label}\u2026`,
850
+ "aria-label": label,
851
+ className: "h-[var(--layout-search-h,3rem)] pl-6 pr-20 text-base transition-[padding] duration-200 ease-out motion-reduce:transition-none bg-[var(--color-surface)] border-[var(--color-border)] rounded-full shadow-md"
852
+ }
853
+ ),
854
+ /* @__PURE__ */ jsxs6("div", { className: "absolute right-3 top-1/2 flex items-center -translate-y-1/2", children: [
855
+ /* @__PURE__ */ jsx10("span", { className: "p-2 text-[var(--color-text-muted)] pointer-events-none", children: /* @__PURE__ */ jsx10(Search, { "aria-hidden": "true", className: "w-6 h-6" }) }),
856
+ canClear && /* @__PURE__ */ jsx10(
857
+ "button",
858
+ {
859
+ type: "button",
860
+ "aria-label": "Clear search",
861
+ className: "p-2 text-[var(--color-text-muted)] rounded-full hover:bg-[var(--color-surface-muted)] hover:text-[var(--color-text)]",
862
+ onClick: () => {
863
+ clear();
864
+ onQueryChange?.("");
865
+ },
866
+ children: /* @__PURE__ */ jsx10(X, { "aria-hidden": "true", className: "w-6 h-6" })
867
+ }
868
+ )
869
+ ] }),
870
+ extraResults?.status && /* @__PURE__ */ jsx10("p", { role: "status", "aria-live": "polite", className: "sr-only", children: extraResults.status }),
871
+ showList && /* @__PURE__ */ jsxs6("ul", { className: "genre-search-results absolute z-40 w-full max-h-64 mt-1 overflow-y-auto bg-[var(--color-surface)] border border-[var(--color-border)] rounded-xl shadow-md", children: [
872
+ extraResults && results.length > 0 && /* @__PURE__ */ jsx10("li", { className: heading, children: "Genres" }),
873
+ results.map((genrePlaylist) => /* @__PURE__ */ jsx10("li", { children: /* @__PURE__ */ jsxs6(
874
+ "button",
875
+ {
876
+ type: "button",
877
+ className: "w-full px-3 py-1.5 text-left hover:bg-[var(--color-surface-muted)]",
878
+ onClick: () => {
879
+ select(genrePlaylist);
880
+ onQueryChange?.("");
881
+ },
882
+ children: [
883
+ genrePlaylist.name,
884
+ genrePlaylist.isUmbrella && /* @__PURE__ */ jsx10("span", { className: "ml-2 text-xs italic text-[var(--color-text-muted)]", children: "umbrella" })
885
+ ]
886
+ }
887
+ ) }, genrePlaylist.uuid)),
888
+ extraResults && extraItems.length > 0 && /* @__PURE__ */ jsx10("li", { className: heading, children: extraResults.label }),
889
+ extraItems.map((item) => /* @__PURE__ */ jsx10("li", { children: /* @__PURE__ */ jsx10(
890
+ "button",
891
+ {
892
+ type: "button",
893
+ className: "w-full px-3 py-1.5 text-left hover:bg-[var(--color-surface-muted)]",
894
+ onClick: () => {
895
+ setQuery("");
896
+ onQueryChange?.("");
897
+ item.onSelect();
898
+ },
899
+ children: item.content
900
+ }
901
+ ) }, item.key))
902
+ ] })
903
+ ] });
904
+ }
905
+
906
+ // src/genre-tree/GenreActionBand.tsx
907
+ import { MoveUpRight, Pause, Pencil, Play, Plus, ZoomIn } from "lucide-react";
908
+ import { jsx as jsx11, jsxs as jsxs7 } from "react/jsx-runtime";
909
+ function GenreActionBand({
910
+ node,
911
+ onPlayPause,
912
+ isPlaying = false,
913
+ onZoomIn,
914
+ onAddChild,
915
+ onRename,
916
+ onChangeParent,
917
+ additionalActions
918
+ }) {
919
+ const items = [];
920
+ const adminItems = [];
921
+ if (onPlayPause) {
922
+ items.push({
923
+ key: "play-pause",
924
+ label: isPlaying ? "Pause" : "Play",
925
+ icon: isPlaying ? /* @__PURE__ */ jsx11(Pause, { "aria-hidden": "true", className: "w-5 h-5" }) : /* @__PURE__ */ jsx11(Play, { "aria-hidden": "true", className: "w-5 h-5" }),
926
+ onClick: onPlayPause
927
+ });
928
+ }
929
+ if (onZoomIn) {
930
+ items.push({
931
+ key: "zoom-in",
932
+ label: "Focus",
933
+ icon: /* @__PURE__ */ jsx11(ZoomIn, { "aria-hidden": "true", className: "w-5 h-5" }),
934
+ onClick: onZoomIn
935
+ });
936
+ }
937
+ if (onAddChild) {
938
+ adminItems.push({ key: "add-child", label: "Add", icon: /* @__PURE__ */ jsx11(Plus, { "aria-hidden": "true", className: "w-5 h-5" }), onClick: onAddChild });
939
+ }
940
+ if (onRename) {
941
+ adminItems.push({ key: "rename", label: "Rename", icon: /* @__PURE__ */ jsx11(Pencil, { "aria-hidden": "true", className: "w-5 h-5" }), onClick: onRename });
942
+ }
943
+ if (onChangeParent) {
944
+ adminItems.push({ key: "change-parent", label: "Parent", icon: /* @__PURE__ */ jsx11(MoveUpRight, { "aria-hidden": "true", className: "w-5 h-5" }), onClick: onChangeParent });
945
+ }
946
+ for (const action of additionalActions?.(node) ?? []) {
947
+ items.push({
948
+ key: action.key,
949
+ label: action.label(node),
950
+ icon: action.icon(node),
951
+ onClick: (event) => action.onClick(event, node),
952
+ disabled: action.enabled ? !action.enabled(node) : false
953
+ });
954
+ }
955
+ if (items.length === 0 && adminItems.length === 0) return null;
956
+ const renderItems = (list) => list.map((item) => /* @__PURE__ */ jsxs7(
957
+ "button",
958
+ {
959
+ type: "button",
960
+ disabled: item.disabled,
961
+ onClick: item.onClick,
962
+ 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",
963
+ children: [
964
+ /* @__PURE__ */ jsx11("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 }),
965
+ item.label
966
+ ]
967
+ },
968
+ item.key
969
+ ));
970
+ return /* @__PURE__ */ jsxs7("div", { className: "genre-action-band border-b border-[var(--color-border)]", children: [
971
+ items.length > 0 && /* @__PURE__ */ jsx11("div", { className: "flex flex-wrap gap-1 px-3.5 py-3", children: renderItems(items) }),
972
+ adminItems.length > 0 && /* @__PURE__ */ jsxs7(
973
+ "section",
974
+ {
975
+ "aria-label": "Admin actions",
976
+ className: `px-3.5 py-3 ${items.length > 0 ? "border-t border-[var(--color-border)]" : ""}`,
977
+ children: [
978
+ /* @__PURE__ */ jsx11("h3", { className: "mb-2 text-xs font-semibold uppercase tracking-wide text-[var(--color-text-muted)]", children: "Admin" }),
979
+ /* @__PURE__ */ jsx11("div", { className: "flex flex-wrap gap-1", children: renderItems(adminItems) })
980
+ ]
981
+ }
982
+ )
983
+ ] });
984
+ }
985
+
986
+ // src/genre-tree/PanelEdgeTab.tsx
987
+ import { ChevronLeft, ChevronRight } from "lucide-react";
988
+ import { twMerge } from "tailwind-merge";
989
+ import { jsx as jsx12, jsxs as jsxs8 } from "react/jsx-runtime";
990
+ 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)]";
991
+ function PanelEdgeTab({ side, edge, center, controls, className }) {
992
+ if (!edge && !center) return null;
993
+ const isLeft = side === "left";
994
+ const EdgeIcon = isLeft ? ChevronLeft : ChevronRight;
995
+ const CenterIcon = isLeft ? ChevronRight : ChevronLeft;
996
+ const renderButton = (action, icon) => /* @__PURE__ */ jsx12(
997
+ "button",
998
+ {
999
+ type: "button",
1000
+ "aria-label": action.label,
1001
+ title: action.label,
1002
+ "aria-controls": controls,
1003
+ "aria-expanded": action.expanded,
1004
+ className: buttonClassName,
1005
+ onClick: action.onClick,
1006
+ children: icon
1007
+ }
1008
+ );
1009
+ const edgeButton = edge && renderButton(edge, /* @__PURE__ */ jsx12(EdgeIcon, { "aria-hidden": "true", className: "h-5 w-5" }));
1010
+ const centerButton = center && renderButton(center, /* @__PURE__ */ jsx12(CenterIcon, { "aria-hidden": "true", className: "h-5 w-5" }));
1011
+ const buttons = isLeft ? [edgeButton, centerButton] : [centerButton, edgeButton];
1012
+ return /* @__PURE__ */ jsxs8(
1013
+ "div",
1014
+ {
1015
+ className: twMerge(
1016
+ "panel-edge-tab flex w-[var(--layout-edge-tab-w)] flex-col overflow-hidden bg-[var(--color-surface)] shadow-[0_1px_2px_rgba(60,64,67,0.3),0_2px_6px_2px_rgba(60,64,67,0.15)] [clip-path:inset(-0.5rem_-0.5rem_-0.5rem_0)]",
1017
+ isLeft ? "rounded-r-lg" : "rounded-l-lg [clip-path:inset(-0.5rem_0_-0.5rem_-0.5rem)]",
1018
+ className
1019
+ ),
1020
+ children: [
1021
+ buttons[0],
1022
+ buttons[0] && buttons[1] && /* @__PURE__ */ jsx12("div", { role: "presentation", className: "h-px shrink-0 bg-[var(--color-border)]" }),
1023
+ buttons[1]
1024
+ ]
1025
+ }
1026
+ );
1027
+ }
1028
+
1029
+ // src/genre-tree/ChangeParentPopup.tsx
1030
+ import { useEffect as useEffect3, useMemo as useMemo5 } from "react";
1031
+ import {
1032
+ getReparentCandidates,
1033
+ useUpdateGenre
1034
+ } from "@behindthemusictree/app-kit/genre-tree";
1035
+ import { jsx as jsx13, jsxs as jsxs9 } from "react/jsx-runtime";
1036
+ function ChangeParentPopup({
1037
+ genrePlaylist,
1038
+ genrePlaylists,
1039
+ scope,
1040
+ getBackendBaseUrl,
1041
+ onClose
1042
+ }) {
1043
+ const { mutate, isPending, formErrors } = useUpdateGenre(scope, getBackendBaseUrl);
1044
+ const candidates = useMemo5(
1045
+ () => getReparentCandidates(genrePlaylists, genrePlaylist.uuid),
1046
+ [genrePlaylists, genrePlaylist.uuid]
1047
+ );
1048
+ useEffect3(() => {
1049
+ const handleKeyDown = (event) => {
1050
+ if (event.key === "Escape") onClose();
1051
+ };
1052
+ window.addEventListener("keydown", handleKeyDown);
1053
+ return () => window.removeEventListener("keydown", handleKeyDown);
1054
+ }, [onClose]);
1055
+ return /* @__PURE__ */ jsxs9(BasePopup_default, { title: `Change parent of ${genrePlaylist.name}`, onClose, showCancelButton: true, fitContent: true, children: [
1056
+ /* @__PURE__ */ jsxs9("p", { className: "mb-3 text-sm text-[var(--color-text-muted)]", children: [
1057
+ "Current parent: ",
1058
+ genrePlaylist.parent?.name ?? "none"
1059
+ ] }),
1060
+ /* @__PURE__ */ jsx13("div", { "aria-busy": isPending, className: isPending ? "pointer-events-none opacity-60" : void 0, children: /* @__PURE__ */ jsx13(
1061
+ GenreSearch,
1062
+ {
1063
+ genrePlaylists: candidates,
1064
+ onSelect: (newParent) => mutate({ uuid: genrePlaylist.criteria.uuid, data: { parent: newParent.criteria.uuid } }, { onSuccess: onClose }),
1065
+ placeholder: "Search a new parent\u2026"
1066
+ }
1067
+ ) }),
1068
+ formErrors.length > 0 && /* @__PURE__ */ jsx13("p", { role: "alert", className: "mt-3 text-sm text-[var(--color-red-500)]", children: formErrors.map(({ message }) => message).join(" ") })
1069
+ ] });
1070
+ }
1071
+
1072
+ // src/genre-tree/artist/useArtistPanel.tsx
1073
+ import { Skeleton as Skeleton4 } from "@behindthemusictree/ui";
1074
+ import { useArtist } from "@behindthemusictree/app-kit/genre-tree";
1075
+
1076
+ // src/genre-tree/artist/ArtistHeader.tsx
1077
+ import { FieldList } from "@behindthemusictree/ui";
1078
+
1079
+ // src/genre-tree/artist/ArtistLinks.tsx
1080
+ import { Globe, Share2 } from "lucide-react";
1081
+ import {
1082
+ SiBandcamp,
1083
+ SiDiscogs,
1084
+ SiMusicbrainz,
1085
+ SiSoundcloud,
1086
+ SiSpotify,
1087
+ SiWikidata,
1088
+ SiWikipedia,
1089
+ SiYoutube
1090
+ } from "react-icons/si";
1091
+ import { Chip } from "@behindthemusictree/ui";
1092
+ import { jsx as jsx14 } from "react/jsx-runtime";
1093
+ var LINK_KINDS = {
1094
+ official: { label: "Official website", Icon: Globe },
1095
+ wikipedia: { label: "Wikipedia", Icon: SiWikipedia },
1096
+ wikidata: { label: "Wikidata", Icon: SiWikidata },
1097
+ spotify: { label: "Spotify", Icon: SiSpotify },
1098
+ youtube: { label: "YouTube", Icon: SiYoutube },
1099
+ bandcamp: { label: "Bandcamp", Icon: SiBandcamp },
1100
+ soundcloud: { label: "SoundCloud", Icon: SiSoundcloud },
1101
+ discogs: { label: "Discogs", Icon: SiDiscogs },
1102
+ social: { label: "Social", Icon: Share2 }
1103
+ };
1104
+ function socialLabel(url) {
1105
+ try {
1106
+ return new URL(url).hostname.replace(/^www\./, "");
1107
+ } catch {
1108
+ return LINK_KINDS.social.label;
1109
+ }
1110
+ }
1111
+ function artistLinks(artist) {
1112
+ const links = artist.links.map(({ kind, url }) => ({
1113
+ key: `${kind}-${url}`,
1114
+ url,
1115
+ Icon: LINK_KINDS[kind].Icon,
1116
+ label: kind === "social" ? socialLabel(url) : LINK_KINDS[kind].label
1117
+ }));
1118
+ if (artist.musicbrainzArtistId) {
1119
+ links.push({
1120
+ key: "musicbrainz",
1121
+ url: `https://musicbrainz.org/artist/${artist.musicbrainzArtistId}`,
1122
+ Icon: SiMusicbrainz,
1123
+ label: "MusicBrainz"
1124
+ });
1125
+ }
1126
+ return links;
1127
+ }
1128
+ function ArtistLinks({ links }) {
1129
+ return /* @__PURE__ */ jsx14("ul", { className: "flex flex-wrap gap-1.5", children: links.map(({ key, url, Icon, label }) => /* @__PURE__ */ jsx14("li", { children: /* @__PURE__ */ jsx14(Chip, { href: url, icon: /* @__PURE__ */ jsx14(Icon, { className: "h-3.5 w-3.5 shrink-0", "aria-hidden": "true" }), children: label }) }, key)) });
1130
+ }
1131
+
1132
+ // src/genre-tree/artist/formatYears.ts
1133
+ function formatYears(beginYear, endYear, ended) {
1134
+ if (beginYear === null && endYear === null) return ended ? "ended" : null;
1135
+ const end = endYear ?? (ended ? "?" : "present");
1136
+ return `${beginYear ?? "?"}\u2013${end}`;
1137
+ }
1138
+
1139
+ // src/genre-tree/artist/ArtistHeader.tsx
1140
+ import { jsx as jsx15 } from "react/jsx-runtime";
1141
+ var countryNames = new Intl.DisplayNames(["en"], { type: "region" });
1142
+ function ArtistHeader({ artist }) {
1143
+ const fields = [];
1144
+ if (artist.type) fields.push({ label: "Type", value: artist.type.charAt(0).toUpperCase() + artist.type.slice(1) });
1145
+ const country = artist.country && countryNames.of(artist.country);
1146
+ if (country) fields.push({ label: "Country", value: country });
1147
+ const years = formatYears(artist.beginYear, artist.endYear, artist.ended);
1148
+ if (years) fields.push({ label: "Active", value: years });
1149
+ const links = artistLinks(artist);
1150
+ if (links.length > 0) fields.push({ label: "Links", value: /* @__PURE__ */ jsx15(ArtistLinks, { links }) });
1151
+ if (fields.length === 0) return null;
1152
+ return /* @__PURE__ */ jsx15(FieldList, { items: fields });
1153
+ }
1154
+
1155
+ // src/genre-tree/artist/ArtistPhoto.tsx
1156
+ import { jsx as jsx16, jsxs as jsxs10 } from "react/jsx-runtime";
1157
+ function ExternalLink({ href, children }) {
1158
+ return /* @__PURE__ */ jsx16("a", { href, target: "_blank", rel: "noopener noreferrer", className: "underline-offset-2 hover:underline", children });
1159
+ }
1160
+ function ArtistPhoto({ image, name }) {
1161
+ return /* @__PURE__ */ jsxs10("figure", { className: "flex flex-col gap-1.5 px-6 py-2.5", children: [
1162
+ /* @__PURE__ */ jsx16("img", { src: image.url, alt: name, className: "h-64 w-full object-contain bg-[var(--color-surface-muted)] rounded" }),
1163
+ /* @__PURE__ */ jsxs10("figcaption", { className: "text-xs text-[var(--color-text-muted)]", children: [
1164
+ "Photo: ",
1165
+ /* @__PURE__ */ jsx16(ExternalLink, { href: image.pageUrl, children: image.author ?? "Wikimedia Commons" }),
1166
+ ",",
1167
+ " ",
1168
+ image.licenseUrl ? /* @__PURE__ */ jsx16(ExternalLink, { href: image.licenseUrl, children: image.license }) : image.license
1169
+ ] })
1170
+ ] });
1171
+ }
1172
+
1173
+ // src/genre-tree/artist/ArtistMembers.tsx
1174
+ import { PanelGroup, PanelSection } from "@behindthemusictree/ui";
1175
+ import { jsx as jsx17, jsxs as jsxs11 } from "react/jsx-runtime";
1176
+ function MembershipList({ title, memberships, renderArtistLink }) {
1177
+ if (memberships.length === 0) return null;
1178
+ return /* @__PURE__ */ jsx17(PanelGroup, { title, children: /* @__PURE__ */ jsx17("ul", { children: memberships.map((membership, index) => {
1179
+ const details = [
1180
+ formatYears(membership.beginYear, membership.endYear, membership.ended),
1181
+ ...membership.roles
1182
+ ];
1183
+ const detailsText = details.filter(Boolean).join(" \xB7 ");
1184
+ return (
1185
+ // A member can have several periods in the same group.
1186
+ /* @__PURE__ */ jsxs11("li", { className: "truncate py-0.5", title: detailsText || void 0, children: [
1187
+ /* @__PURE__ */ jsx17("span", { className: "font-medium text-[var(--color-text)]", children: renderArtistLink(membership.artist) }),
1188
+ detailsText && /* @__PURE__ */ jsxs11("span", { className: "text-[var(--color-text-muted)]", children: [
1189
+ " \u2014 ",
1190
+ detailsText
1191
+ ] })
1192
+ ] }, `${membership.artist.uuid}-${index}`)
1193
+ );
1194
+ }) }) });
1195
+ }
1196
+ function ArtistMembers({ title, memberships, renderArtistLink }) {
1197
+ if (memberships.length === 0) return null;
1198
+ const isCurrent = (membership) => !membership.ended && membership.endYear === null;
1199
+ return /* @__PURE__ */ jsxs11(PanelSection, { title, children: [
1200
+ /* @__PURE__ */ jsx17(MembershipList, { title: "Current", memberships: memberships.filter(isCurrent), renderArtistLink }),
1201
+ /* @__PURE__ */ jsx17(
1202
+ MembershipList,
1203
+ {
1204
+ title: "Past",
1205
+ memberships: memberships.filter((membership) => !isCurrent(membership)),
1206
+ renderArtistLink
1207
+ }
1208
+ )
1209
+ ] });
1210
+ }
1211
+
1212
+ // src/genre-tree/artist/ArtistDiscography.tsx
1213
+ import { PanelGroup as PanelGroup2, PanelSection as PanelSection2 } from "@behindthemusictree/ui";
1214
+ import { jsx as jsx18, jsxs as jsxs12 } from "react/jsx-runtime";
1215
+ var GROUPS = ["Albums", "EPs", "Singles", "Live", "Compilations", "Other"];
1216
+ var PRIMARY_GROUPS = { album: "Albums", ep: "EPs", single: "Singles" };
1217
+ function discographyGroup({ primaryType, secondaryTypes }) {
1218
+ const secondary = secondaryTypes.map((type) => type.toLowerCase());
1219
+ if (secondary.includes("live")) return "Live";
1220
+ if (secondary.includes("compilation")) return "Compilations";
1221
+ return PRIMARY_GROUPS[primaryType?.toLowerCase() ?? ""] ?? "Other";
1222
+ }
1223
+ function byYear(a, b) {
1224
+ if (a.year !== b.year) {
1225
+ if (a.year === null) return 1;
1226
+ if (b.year === null) return -1;
1227
+ return a.year.localeCompare(b.year);
1228
+ }
1229
+ return a.name.localeCompare(b.name);
1230
+ }
1231
+ function ArtistDiscography({ albums }) {
1232
+ if (albums.length === 0) return null;
1233
+ const sorted = [...albums].sort(byYear);
1234
+ return /* @__PURE__ */ jsx18(PanelSection2, { title: "Discography", children: GROUPS.map((group) => {
1235
+ const groupAlbums = sorted.filter((album) => discographyGroup(album) === group);
1236
+ if (groupAlbums.length === 0) return null;
1237
+ return /* @__PURE__ */ jsx18(PanelGroup2, { title: group, children: /* @__PURE__ */ jsx18("ul", { children: groupAlbums.map((album) => /* @__PURE__ */ jsxs12("li", { className: "grid grid-cols-[2.5rem_minmax(0,1fr)] gap-2 py-0.5", children: [
1238
+ /* @__PURE__ */ jsx18("span", { className: "tabular-nums text-[var(--color-text-muted)]", children: album.year ?? "\u2014" }),
1239
+ /* @__PURE__ */ jsx18("span", { className: "truncate text-[var(--color-text)]", title: album.name, children: album.name })
1240
+ ] }, album.uuid)) }) }, group);
1241
+ }) });
1242
+ }
1243
+
1244
+ // src/genre-tree/artist/ArtistSongs.tsx
1245
+ import { useState as useState3 } from "react";
1246
+ import { keepPreviousData } from "@tanstack/react-query";
1247
+ import { Play as Play2 } from "lucide-react";
1248
+ import { Pagination, PanelSection as PanelSection3, Skeleton as Skeleton3 } from "@behindthemusictree/ui";
1249
+ import { usePlayer as usePlayer7 } from "@behindthemusictree/app-kit/player";
1250
+ import {
1251
+ SongDetailedSchema as SongDetailedSchema2,
1252
+ libraryEndpoints as libraryEndpoints2,
1253
+ songUnplayableReason as songUnplayableReason3,
1254
+ useTrackList as useTrackList4
1255
+ } from "@behindthemusictree/app-kit/genre-tree";
1256
+ import { PaginatedResponseSchema as PaginatedResponseSchema2, useFetchWrapper as useFetchWrapper2, useQueryWithParse } from "@behindthemusictree/app-kit/transport";
1257
+
1258
+ // src/genre-tree/track-list-sidebar/TrackItem.tsx
1259
+ import { usePlayer as usePlayer6 } from "@behindthemusictree/app-kit/player";
1260
+ import { useTrackList as useTrackList3 } from "@behindthemusictree/app-kit/genre-tree";
1261
+
1262
+ // src/genre-tree/TrackPositionPlayPause.tsx
1263
+ import { FaPlay as FaPlay2, FaPause as FaPause2 } from "react-icons/fa";
1264
+ import { usePlayer as usePlayer5 } from "@behindthemusictree/app-kit/player";
1265
+ import { PlayStates as PlayStates2 } from "@behindthemusictree/app-kit/player";
1266
+ import { RingLoader as RingLoader2 } from "@behindthemusictree/ui";
1267
+ import { jsx as jsx19, jsxs as jsxs13 } from "react/jsx-runtime";
1268
+ function TrackPositionPlayPause({ position, uuid, handlePlayPauseClick }) {
1269
+ const { playerTrackObject, playState } = usePlayer5();
1270
+ const isThisTrack = playerTrackObject?.track.id === uuid;
1271
+ return /* @__PURE__ */ jsxs13(
1272
+ "div",
1273
+ {
1274
+ className: "track-position-play-pause flex items-center justify-center text-lg w-16 cursor-pointer",
1275
+ style: { minWidth: "4rem", maxWidth: "4rem" },
1276
+ onClick: handlePlayPauseClick,
1277
+ children: [
1278
+ /* @__PURE__ */ jsx19("div", { className: "group-hover:hidden", children: /* @__PURE__ */ jsx19("div", { children: isThisTrack && playState !== PlayStates2.STOPPED ? /* @__PURE__ */ jsxs13("div", { className: "flex space-x-1 items-end", children: [
1279
+ /* @__PURE__ */ jsx19(
1280
+ "div",
1281
+ {
1282
+ className: `w-playingbar bg-[var(--color-green-500)] h-3 origin-bottom ${playState === PlayStates2.PLAYING ? "animate-scale-pulse" : ""}`
1283
+ }
1284
+ ),
1285
+ /* @__PURE__ */ jsx19(
1286
+ "div",
1287
+ {
1288
+ className: `w-playingbar bg-[var(--color-green-500)] h-4 origin-bottom ${playState === PlayStates2.PLAYING ? "animate-scale-pulse delay-200" : ""}`,
1289
+ style: { animationDelay: "0.2s" }
1290
+ }
1291
+ ),
1292
+ /* @__PURE__ */ jsx19(
1293
+ "div",
1294
+ {
1295
+ className: `w-playingbar bg-[var(--color-green-500)] h-3 origin-bottom ${playState === PlayStates2.PLAYING ? "animate-scale-pulse delay-400" : ""}`,
1296
+ style: { animationDelay: "0.3s" }
1297
+ }
1298
+ )
1299
+ ] }) : position }) }),
1300
+ /* @__PURE__ */ jsx19("div", { className: "hidden group-hover:flex items-center justify-center", children: isThisTrack && playState === PlayStates2.LOADING ? /* @__PURE__ */ jsx19(RingLoader2, { size: 16 }) : isThisTrack && playState === PlayStates2.PLAYING ? /* @__PURE__ */ jsx19(FaPause2, {}) : /* @__PURE__ */ jsx19(FaPlay2, {}) })
1301
+ ]
1302
+ }
1303
+ );
1304
+ }
1305
+
1306
+ // src/genre-tree/track-list-sidebar/TrackItem.tsx
1307
+ import { isSong as isSong2, songUnplayableReason as songUnplayableReason2 } from "@behindthemusictree/app-kit/genre-tree";
1308
+ import { ArtistNames as ArtistNames2 } from "@behindthemusictree/app-kit/genre-tree";
1309
+ import { jsx as jsx20, jsxs as jsxs14 } from "react/jsx-runtime";
1310
+ function TrackItem({
1311
+ track,
1312
+ position,
1313
+ renderDuration,
1314
+ renderActions,
1315
+ renderArtistLink
1316
+ }) {
1317
+ const { handlePlayPauseAction, playerTrackObject, loadTrackForPlayer } = usePlayer6();
1318
+ const { trackList, toTrackAtPosition } = useTrackList3();
1319
+ const scope = trackList?.origin?.scope ?? null;
1320
+ const unplayableReason = isSong2(track) ? songUnplayableReason2(track) : null;
1321
+ const handlePlayPauseClick = (event) => {
1322
+ event.stopPropagation();
1323
+ if (playerTrackObject && playerTrackObject.track.id === track.uuid) {
1324
+ handlePlayPauseAction();
1325
+ } else if (scope != null) {
1326
+ toTrackAtPosition(position);
1327
+ loadTrackForPlayer(track.uuid);
1328
+ }
1329
+ };
1330
+ return /* @__PURE__ */ jsxs14(
1331
+ "div",
1332
+ {
1333
+ className: `track-item flex h-14 text-[var(--color-text)] ${unplayableReason ? "opacity-50" : "hover:bg-[var(--color-surface-muted)] group"}`,
1334
+ title: unplayableReason ?? void 0,
1335
+ children: [
1336
+ unplayableReason ? /* @__PURE__ */ jsx20(
1337
+ "div",
1338
+ {
1339
+ className: "flex items-center justify-center text-lg w-16",
1340
+ style: { minWidth: "4rem", maxWidth: "4rem" },
1341
+ "aria-disabled": "true",
1342
+ children: position
1343
+ }
1344
+ ) : /* @__PURE__ */ jsx20(TrackPositionPlayPause, { position, uuid: track.uuid, handlePlayPauseClick }),
1345
+ /* @__PURE__ */ jsxs14("div", { className: "title-artist-container flex flex-col items-start justify-center min-w-0 flex-1", children: [
1346
+ /* @__PURE__ */ jsxs14("div", { className: "flex max-w-full items-center gap-2", children: [
1347
+ /* @__PURE__ */ jsx20("div", { className: "title text-lg font-bold text-[var(--color-text)] max-w-full truncate", children: track.title }),
1348
+ isSong2(track) && /* @__PURE__ */ jsx20(MusicBrainzRecordingLink, { song: track })
1349
+ ] }),
1350
+ track.artists && track.artists.length > 0 ? /* @__PURE__ */ jsx20("div", { className: "artist text-base max-w-full truncate text-[var(--color-text-muted)]", children: /* @__PURE__ */ jsx20(ArtistNames2, { artists: track.artists, renderArtistLink }) }) : "",
1351
+ unplayableReason && /* @__PURE__ */ jsx20("div", { className: "text-xs", children: /* @__PURE__ */ jsx20(UnplayableReason, { reason: unplayableReason }) })
1352
+ ] }),
1353
+ /* @__PURE__ */ jsx20("div", { className: "album-name items-start justify-center w-1/3 min-w-0 ml-2 max-w-full truncate text-[var(--color-text-muted)]", children: track.album ? track.album.name : "" }),
1354
+ /* @__PURE__ */ jsx20("div", { className: "genre-name-container flex items-center justify-end w-1/6 min-w-0 shrink-0", children: track.genre ? /* @__PURE__ */ jsx20("div", { className: "genre-name font-bold p-1 border border-[var(--color-text-muted)] text-xs max-w-full truncate", children: track.genre.name }) : "" }),
1355
+ renderDuration && /* @__PURE__ */ jsx20(
1356
+ "div",
1357
+ {
1358
+ className: "duration flex text-base w-16 items-center justify-center",
1359
+ style: { minWidth: "4rem", maxWidth: "4rem" },
1360
+ children: renderDuration(track)
1361
+ }
1362
+ ),
1363
+ renderActions && /* @__PURE__ */ jsx20("div", { className: "edit flex text-base w-6 items-center justify-center mr-2 cursor-pointer", children: renderActions(track) })
1364
+ ]
1365
+ }
1366
+ );
1367
+ }
1368
+
1369
+ // src/genre-tree/artist/ArtistSongs.tsx
1370
+ import { Fragment as Fragment4, jsx as jsx21, jsxs as jsxs15 } from "react/jsx-runtime";
1371
+ var SongPageSchema2 = PaginatedResponseSchema2(SongDetailedSchema2);
1372
+ var SORTS = [
1373
+ { ordering: "title", label: "Title" },
1374
+ { ordering: "-albumYear", label: "Year" }
1375
+ ];
1376
+ function ArtistSongs({ artist, getBackendBaseUrl, renderArtistLink }) {
1377
+ const [page, setPage] = useState3(1);
1378
+ const [ordering, setOrdering] = useState3("title");
1379
+ const { fetch } = useFetchWrapper2(getBackendBaseUrl);
1380
+ const { playNewTrackListFromArtist } = useTrackList4();
1381
+ const { playerTrackObject } = usePlayer7();
1382
+ const { data, isPending, isError } = useQueryWithParse({
1383
+ queryKey: ["referenceSongs", "artist", artist.uuid, ordering, page],
1384
+ queryFn: () => fetch(libraryEndpoints2.reference.songs.list(), true, false, void 0, {
1385
+ artist: artist.uuid,
1386
+ ordering,
1387
+ page
1388
+ }),
1389
+ schema: SongPageSchema2,
1390
+ placeholderData: keepPreviousData,
1391
+ context: "ArtistSongs"
1392
+ });
1393
+ const play = (startSongUuid) => playNewTrackListFromArtist({ uuid: artist.uuid, name: artist.name }, "reference", {
1394
+ ordering,
1395
+ startSongUuid
1396
+ });
1397
+ const handleRowClickCapture = (event, song) => {
1398
+ if (!(event.target instanceof Element) || !event.target.closest(".track-position-play-pause")) return;
1399
+ if (playerTrackObject?.track.id === song.uuid || songUnplayableReason3(song) !== null) return;
1400
+ event.stopPropagation();
1401
+ void play(song.uuid);
1402
+ };
1403
+ return /* @__PURE__ */ jsx21(PanelSection3, { title: "Songs", children: isPending ? /* @__PURE__ */ jsx21(Skeleton3, { className: "h-14", "aria-busy": "true" }) : isError ? /* @__PURE__ */ jsx21("p", { className: "text-red-600", children: "Could not load songs." }) : data.results.length === 0 ? /* @__PURE__ */ jsx21("p", { className: "text-[var(--color-text-muted)]", children: "No songs yet." }) : /* @__PURE__ */ jsxs15(Fragment4, { children: [
1404
+ /* @__PURE__ */ jsxs15("div", { className: "flex flex-wrap items-center justify-between gap-2 mb-2", children: [
1405
+ /* @__PURE__ */ jsxs15(
1406
+ "button",
1407
+ {
1408
+ type: "button",
1409
+ onClick: () => void play(),
1410
+ 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",
1411
+ children: [
1412
+ /* @__PURE__ */ jsx21(Play2, { className: "h-3.5 w-3.5", "aria-hidden": "true" }),
1413
+ "Play all"
1414
+ ]
1415
+ }
1416
+ ),
1417
+ /* @__PURE__ */ jsx21("div", { role: "group", "aria-label": "Sort songs by", className: "flex gap-1 text-xs", children: SORTS.map((sort) => /* @__PURE__ */ jsx21(
1418
+ "button",
1419
+ {
1420
+ type: "button",
1421
+ "aria-pressed": ordering === sort.ordering,
1422
+ onClick: () => {
1423
+ setOrdering(sort.ordering);
1424
+ setPage(1);
1425
+ },
1426
+ className: "px-2.5 py-0.5 text-[var(--color-text)] rounded-full hover:bg-[var(--color-surface-muted)] aria-pressed:bg-gray-900 aria-pressed:text-white",
1427
+ children: sort.label
1428
+ },
1429
+ sort.ordering
1430
+ )) })
1431
+ ] }),
1432
+ /* @__PURE__ */ jsx21("ul", { className: "overflow-hidden mb-2 bg-zinc-950 rounded-lg", children: data.results.map((song, index) => /* @__PURE__ */ jsx21("li", { onClickCapture: (event) => handleRowClickCapture(event, song), children: /* @__PURE__ */ jsx21(
1433
+ TrackItem,
1434
+ {
1435
+ track: song,
1436
+ position: (page - 1) * data.pageSize + index + 1,
1437
+ renderArtistLink
1438
+ }
1439
+ ) }, song.uuid)) }),
1440
+ /* @__PURE__ */ jsx21(Pagination, { currentPage: page, totalPages: data.totalPages, onPageChange: setPage })
1441
+ ] }) });
1442
+ }
1443
+
1444
+ // src/genre-tree/artist/useArtistPanel.tsx
1445
+ import { Fragment as Fragment5, jsx as jsx22, jsxs as jsxs16 } from "react/jsx-runtime";
1446
+ function useArtistPanel({
1447
+ uuid,
1448
+ getBackendBaseUrl,
1449
+ renderArtistLink,
1450
+ expanded,
1451
+ onExpandedChange,
1452
+ onClose
1453
+ }) {
1454
+ const result = useArtist(uuid, getBackendBaseUrl);
1455
+ const panel = { expanded, onExpandedChange, onClose };
1456
+ switch (result.status) {
1457
+ case "idle":
1458
+ return void 0;
1459
+ case "loading":
1460
+ return {
1461
+ ...panel,
1462
+ title: "Artist",
1463
+ content: /* @__PURE__ */ jsxs16("div", { "aria-busy": "true", className: "flex flex-col gap-2.5 px-6 py-2.5", children: [
1464
+ /* @__PURE__ */ jsx22(Skeleton4, { className: "h-3 w-16" }),
1465
+ /* @__PURE__ */ jsx22(Skeleton4, { className: "h-4 w-32" }),
1466
+ /* @__PURE__ */ jsx22(Skeleton4, { className: "h-3 w-16" }),
1467
+ /* @__PURE__ */ jsx22(Skeleton4, { className: "h-4 w-24" }),
1468
+ /* @__PURE__ */ jsx22(Skeleton4, { className: "h-16 mt-2.5" }),
1469
+ /* @__PURE__ */ jsx22(Skeleton4, { className: "h-16" })
1470
+ ] })
1471
+ };
1472
+ case "not-found":
1473
+ return {
1474
+ ...panel,
1475
+ title: "Artist not found",
1476
+ content: /* @__PURE__ */ jsx22("p", { className: "px-6 py-2.5 text-[var(--color-text-muted)]", children: "This artist does not exist." })
1477
+ };
1478
+ case "error":
1479
+ return {
1480
+ ...panel,
1481
+ title: "Artist",
1482
+ content: /* @__PURE__ */ jsx22("p", { className: "px-6 py-2.5 text-red-600", children: "Could not load this artist." })
1483
+ };
1484
+ case "success": {
1485
+ const { artist } = result;
1486
+ return {
1487
+ ...panel,
1488
+ title: artist.name,
1489
+ content: /* @__PURE__ */ jsxs16(Fragment5, { children: [
1490
+ artist.image && /* @__PURE__ */ jsx22(ArtistPhoto, { image: artist.image, name: artist.name }),
1491
+ /* @__PURE__ */ jsx22(ArtistHeader, { artist }),
1492
+ /* @__PURE__ */ jsx22(ArtistMembers, { title: "Members", memberships: artist.members, renderArtistLink }),
1493
+ /* @__PURE__ */ jsx22(ArtistMembers, { title: "Member of", memberships: artist.memberOf, renderArtistLink }),
1494
+ artist.tracksCount > 0 && /* @__PURE__ */ jsx22(
1495
+ ArtistSongs,
1496
+ {
1497
+ artist: { uuid: artist.uuid, name: artist.name },
1498
+ getBackendBaseUrl,
1499
+ renderArtistLink
1500
+ }
1501
+ )
1502
+ ] }),
1503
+ extraContent: artist.albums.length > 0 ? /* @__PURE__ */ jsx22(ArtistDiscography, { albums: artist.albums }) : void 0
1504
+ };
1505
+ }
1506
+ }
1507
+ }
1508
+
1509
+ // src/genre-tree/GenreTreeView.tsx
1510
+ import { Fragment as Fragment6, jsx as jsx23, jsxs as jsxs17 } from "react/jsx-runtime";
1511
+ var PREVIOUS_BUTTON_CSS = `
1512
+ .genre-tree-view .search-overlay .gtv-info-panel-previous { display: none; }
1513
+ .genre-tree-view:has(.search-overlay .gtv-info-panel-previous) .genre-search input { padding-left: 2.75rem; }
1514
+ .genre-tree-view:has(.search-overlay .gtv-info-panel-previous) .genre-search-previous { display: flex; }
1515
+ @keyframes genre-tree-view-previous-in { from { opacity: 0; transform: translate(-0.5rem, -50%); } to { opacity: 1; transform: translate(0, -50%); } }
1516
+ @media (prefers-reduced-motion: no-preference) {
1517
+ .genre-search-previous { animation: genre-tree-view-previous-in 200ms ease-out; }
1518
+ }
1519
+ `;
1520
+ function GenreNodeActionBand({ genrePlaylist, scope, playback, ...bandProps }) {
1521
+ const { isPlaying, setIsPlaying } = usePlayer8();
1522
+ const isCurrentTrackList = playback.playingNodeId === genrePlaylist.uuid;
1523
+ const handlePlayPause = () => {
1524
+ if (isCurrentTrackList) {
1525
+ setIsPlaying(!isPlaying);
1526
+ return;
1527
+ }
1528
+ playback.play(genrePlaylist).catch((error) => {
1529
+ console.error("Failed to load genre playlist tracks:", error);
1530
+ });
1531
+ };
1532
+ return /* @__PURE__ */ jsx23(
1533
+ GenreActionBand,
1534
+ {
1535
+ ...bandProps,
1536
+ onPlayPause: genrePlaylist.tracksCount > 0 ? handlePlayPause : void 0,
1537
+ isPlaying: isCurrentTrackList && isPlaying
1538
+ }
1539
+ );
1540
+ }
1541
+ var NO_POP_ROOT_TITLE = "This genre tree has no 'Mainstream Pop' root yet";
1542
+ 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]";
1543
+ function GenreTreeView({
1544
+ scope,
1545
+ handleGenreCreationAction,
1546
+ handleGenreRenameAction,
1547
+ getBackendBaseUrl,
1548
+ additionalActions,
1549
+ viewMode: controlledViewMode,
1550
+ selectedNodeId: controlledSelectedNodeId,
1551
+ onSelectedNodeIdChange,
1552
+ defaultViewport,
1553
+ onViewportChange,
1554
+ readOnly = false,
1555
+ className,
1556
+ showDisplayOptions = true,
1557
+ showAddRoot = true,
1558
+ allowWheelRotation: controlledAllowWheelRotation,
1559
+ criteriaOverviewSchema,
1560
+ renderGenreDetailExtras,
1561
+ controlsOrientation = "horizontal",
1562
+ renderArtistLink,
1563
+ artistUuid: controlledArtistUuid,
1564
+ onArtistUuidChange,
1565
+ artistExpanded: controlledArtistExpanded,
1566
+ onArtistExpandedChange,
1567
+ artistSearch = true,
1568
+ onArtistSelect,
1569
+ artistSearchBaseUrl,
1570
+ renderSummary,
1571
+ renderEssentialTracks,
1572
+ renderSongLeading,
1573
+ renderSongActions,
1574
+ renderTracksToolbar,
1575
+ editMode,
1576
+ onEditModeChange,
1577
+ renderTitleEditor,
1578
+ renderParentsEditor,
1579
+ renderDangerZone,
1580
+ expanded,
1581
+ onExpandedChange,
1582
+ collapsed,
1583
+ onCollapsedChange,
1584
+ overlayPanel,
1585
+ placement = "default",
1586
+ ...focus
1587
+ }) {
1588
+ const [internalArtistUuid, setInternalArtistUuid] = useState4(null);
1589
+ const isArtistControlled = controlledArtistUuid !== void 0;
1590
+ const artistUuid = isArtistControlled ? controlledArtistUuid : internalArtistUuid;
1591
+ const changeArtist = (uuid) => {
1592
+ if (!isArtistControlled) setInternalArtistUuid(uuid);
1593
+ onArtistUuidChange?.(uuid);
1594
+ };
1595
+ const {
1596
+ isLoading,
1597
+ genrePlaylistByNodeId,
1598
+ visibleGenrePlaylists,
1599
+ groupedGenrePlaylistsByRoot,
1600
+ selectedNodeId,
1601
+ selectedGenrePlaylist,
1602
+ selectNode,
1603
+ selectGenrePlaylist,
1604
+ clearSelection,
1605
+ viewMode,
1606
+ isViewModeControlled: isControlled,
1607
+ setViewMode,
1608
+ canShowPopCore,
1609
+ allowWheelRotation: internalAllowWheelRotation,
1610
+ toggleWheelRotation,
1611
+ onNodeHover: handleNodeHover
1612
+ } = useGenreTreeViewState({
1613
+ scope,
1614
+ getBackendBaseUrl,
1615
+ viewMode: controlledViewMode,
1616
+ selectedNodeId: controlledSelectedNodeId,
1617
+ onSelectedNodeIdChange,
1618
+ criteriaOverviewSchema,
1619
+ artistUuid
1620
+ });
1621
+ const artistResult = useArtist2(artistUuid, artistSearchBaseUrl ?? getBackendBaseUrl);
1622
+ const filterArtistName = artistResult.status === "success" ? artistResult.artist.name : null;
1623
+ const filterArtist = useMemo6(
1624
+ () => artistUuid !== null && filterArtistName !== null ? { uuid: artistUuid, name: filterArtistName } : null,
1625
+ [artistUuid, filterArtistName]
1626
+ );
1627
+ const playback = useGenrePlaylistPlayback({ scope, genrePlaylists: visibleGenrePlaylists, artist: filterArtist });
1628
+ const openArtist = (uuid) => {
1629
+ if (selectedNodeId !== null) clearSelection();
1630
+ changeArtist(uuid);
1631
+ };
1632
+ const [expandedArtistUuid, setExpandedArtistUuid] = useState4(null);
1633
+ const artistBaseUrl = artistSearchBaseUrl ?? getBackendBaseUrl;
1634
+ const artistPanel = useArtistPanel({
1635
+ uuid: artistUuid,
1636
+ getBackendBaseUrl: artistBaseUrl,
1637
+ renderArtistLink: renderArtistLink ?? ((artist) => artist.isSpecialPurpose ? artist.name : /* @__PURE__ */ jsx23("button", { type: "button", onClick: () => openArtist(artist.uuid), className: "hover:underline", children: artist.name })),
1638
+ expanded: controlledArtistExpanded ?? (artistUuid !== null && expandedArtistUuid === artistUuid),
1639
+ onExpandedChange: (on) => {
1640
+ if (controlledArtistExpanded === void 0) setExpandedArtistUuid(on ? artistUuid : null);
1641
+ onArtistExpandedChange?.(on);
1642
+ },
1643
+ onClose: () => changeArtist(null)
1644
+ });
1645
+ const builtInArtistPanel = overlayPanel || selectedNodeId !== null ? void 0 : artistPanel;
1646
+ const activeOverlayPanel = overlayPanel ?? (builtInArtistPanel && { ...builtInArtistPanel, hideTitle: true, hideClose: true });
1647
+ const [uncontrolledCollapsed, setUncontrolledCollapsed] = useState4(false);
1648
+ const [uncontrolledExpanded, setUncontrolledExpanded] = useState4(false);
1649
+ const isCollapsed = collapsed ?? uncontrolledCollapsed;
1650
+ const isExpanded = expanded ?? uncontrolledExpanded;
1651
+ const changeCollapsed = (next) => {
1652
+ setUncontrolledCollapsed(next);
1653
+ onCollapsedChange?.(next);
1654
+ };
1655
+ const changeExpanded = (next) => {
1656
+ setUncontrolledExpanded(next);
1657
+ onExpandedChange?.(next);
1658
+ };
1659
+ useEffect4(() => {
1660
+ setUncontrolledCollapsed(false);
1661
+ }, [selectedNodeId, artistUuid]);
1662
+ const isDetailHidden = isCollapsed || !selectedNodeId && !activeOverlayPanel;
1663
+ useEffect4(() => {
1664
+ if (placement !== "regions" || !isDetailHidden) return void 0;
1665
+ const root = document.documentElement;
1666
+ root.style.setProperty("--layout-left-w", "0px");
1667
+ return () => {
1668
+ root.style.removeProperty("--layout-left-w");
1669
+ };
1670
+ }, [placement, isDetailHidden]);
1671
+ const detailOverlay = placement === "regions" && activeOverlayPanel?.onExpandedChange ? activeOverlayPanel : void 0;
1672
+ const detailExpanded = detailOverlay ? !!detailOverlay.expanded : isExpanded;
1673
+ const onDetailExpandedChange = detailOverlay ? detailOverlay.onExpandedChange : changeExpanded;
1674
+ const infoPanelCuration = {
1675
+ editMode,
1676
+ onEditModeChange,
1677
+ renderTitleEditor,
1678
+ renderParentsEditor,
1679
+ renderDangerZone,
1680
+ expanded: placement === "regions" ? isExpanded : expanded,
1681
+ onExpandedChange: placement === "regions" ? changeExpanded : onExpandedChange,
1682
+ collapsed: placement === "regions" ? isCollapsed : void 0,
1683
+ hideExpandButton: placement === "regions" ? true : void 0,
1684
+ overlayPanel: detailOverlay ? { ...detailOverlay, collapsed: isCollapsed, hideExpandButton: true } : activeOverlayPanel
1685
+ };
1686
+ const [isWheelRevealed, setIsWheelRevealed] = useState4(false);
1687
+ const usesHandoff = viewMode === "wheel" || viewMode === "pop-core";
1688
+ const isTreeReady = !isLoading && (!usesHandoff || isWheelRevealed);
1689
+ const rootRef = useRef4(null);
1690
+ const handlePrevious = () => {
1691
+ rootRef.current?.querySelector(".search-overlay .gtv-info-panel-previous")?.click();
1692
+ };
1693
+ const { showPopup, hidePopup } = usePopup4();
1694
+ const [searchText, setSearchText] = useState4("");
1695
+ const artistSearchResult = useArtistSearch(artistBaseUrl, artistSearch ? searchText : "");
1696
+ const artistExtraResults = artistSearch ? {
1697
+ label: "Artists",
1698
+ items: artistSearchResult.artists.map((artist) => ({
1699
+ key: artist.uuid,
1700
+ content: /* @__PURE__ */ jsxs17(Fragment6, { children: [
1701
+ /* @__PURE__ */ jsxs17("span", { className: "block truncate font-medium", children: [
1702
+ artist.name,
1703
+ artist.disambiguation && /* @__PURE__ */ jsxs17("span", { className: "font-normal text-[var(--color-neutral-500)]", children: [
1704
+ " \xB7 ",
1705
+ artist.disambiguation
1706
+ ] })
1707
+ ] }),
1708
+ artist.matchedAlias && /* @__PURE__ */ jsxs17("span", { className: "block truncate text-xs text-[var(--color-neutral-500)]", children: [
1709
+ "also known as ",
1710
+ artist.matchedAlias
1711
+ ] })
1712
+ ] }),
1713
+ onSelect: () => {
1714
+ openArtist(artist.uuid);
1715
+ onArtistSelect?.(artist);
1716
+ }
1717
+ })),
1718
+ status: artistSearchResult.isError ? "Search failed" : artistSearchResult.settled && artistSearchResult.artists.length === 0 ? "No artists found" : ""
1719
+ } : void 0;
1720
+ const handleSelectedNodeChange = (node) => {
1721
+ selectNode(node);
1722
+ };
1723
+ const handleGenreSearchClear = clearSelection;
1724
+ const handleGenreSearchSelect = (genrePlaylist) => {
1725
+ selectGenrePlaylist(genrePlaylist);
1726
+ };
1727
+ const selectedUuid = selectedGenrePlaylist?.uuid;
1728
+ const selectedChildren = selectedUuid ? visibleGenrePlaylists.filter((genrePlaylist) => genrePlaylist.parent?.uuid === selectedUuid) : [];
1729
+ const canZoomIn = viewMode === "pop-core" && focus.onFocusGenreChange !== void 0 && selectedUuid !== void 0 && focus.focusGenreUuid !== selectedUuid && selectedChildren.length > 0 && selectedChildren.filter((child) => child.criteria?.side === "pop").length <= 1;
1730
+ const handleGenreSearchZoomIn = () => {
1731
+ if (selectedUuid) focus.onFocusGenreChange?.(selectedUuid, "zoom-in");
1732
+ };
1733
+ const zoomInRef = useRef4(void 0);
1734
+ zoomInRef.current = canZoomIn ? handleGenreSearchZoomIn : void 0;
1735
+ const renderInfoPanelBody = useCallback5(
1736
+ (node, { toolbar, ancestors, family }) => {
1737
+ const genrePlaylist = genrePlaylistByNodeId.get(node.id);
1738
+ const criteria = genrePlaylist?.criteria;
1739
+ if (!genrePlaylist || !criteria) return { primary: /* @__PURE__ */ jsxs17(Fragment6, { children: [
1740
+ toolbar,
1741
+ family
1742
+ ] }), aside: ancestors };
1743
+ const genreUuid = criteria.uuid;
1744
+ return {
1745
+ primary: /* @__PURE__ */ jsxs17(Fragment6, { children: [
1746
+ toolbar,
1747
+ /* @__PURE__ */ jsx23(
1748
+ GenreNodeActionBand,
1749
+ {
1750
+ node,
1751
+ genrePlaylist,
1752
+ scope,
1753
+ onZoomIn: zoomInRef.current,
1754
+ onAddChild: readOnly ? void 0 : () => handleGenreCreationAction(criteria),
1755
+ onRename: readOnly ? void 0 : () => handleGenreRenameAction(criteria),
1756
+ onChangeParent: readOnly ? void 0 : () => showPopup(
1757
+ /* @__PURE__ */ jsx23(
1758
+ ChangeParentPopup,
1759
+ {
1760
+ genrePlaylist: { ...genrePlaylist, criteria },
1761
+ genrePlaylists: visibleGenrePlaylists,
1762
+ scope,
1763
+ getBackendBaseUrl,
1764
+ onClose: hidePopup
1765
+ }
1766
+ )
1767
+ ),
1768
+ additionalActions,
1769
+ playback
1770
+ }
1771
+ ),
1772
+ /* @__PURE__ */ jsx23(
1773
+ GenreDetailExtras,
1774
+ {
1775
+ family,
1776
+ genreUuid,
1777
+ genrePlaylist,
1778
+ scope,
1779
+ getBackendBaseUrl,
1780
+ criteriaOverviewSchema,
1781
+ renderGenreDetailExtras,
1782
+ renderArtistLink,
1783
+ artist: filterArtist,
1784
+ renderSummary,
1785
+ renderEssentialTracks,
1786
+ renderSongLeading,
1787
+ renderSongActions,
1788
+ renderTracksToolbar
1789
+ },
1790
+ genreUuid
1791
+ )
1792
+ ] }),
1793
+ aside: ancestors
1794
+ };
1795
+ },
1796
+ [
1797
+ genrePlaylistByNodeId,
1798
+ additionalActions,
1799
+ readOnly,
1800
+ handleGenreCreationAction,
1801
+ handleGenreRenameAction,
1802
+ showPopup,
1803
+ hidePopup,
1804
+ visibleGenrePlaylists,
1805
+ playback,
1806
+ filterArtist,
1807
+ scope,
1808
+ getBackendBaseUrl,
1809
+ criteriaOverviewSchema,
1810
+ renderGenreDetailExtras,
1811
+ renderArtistLink,
1812
+ renderSummary,
1813
+ renderEssentialTracks,
1814
+ renderSongLeading,
1815
+ renderSongActions,
1816
+ renderTracksToolbar
1817
+ ]
1818
+ );
1819
+ const isRotationControlled = controlledAllowWheelRotation !== void 0;
1820
+ const allowWheelRotation = controlledAllowWheelRotation ?? internalAllowWheelRotation;
1821
+ const detailTab = placement === "regions" && (selectedNodeId || detailOverlay) ? /* @__PURE__ */ jsx23(
1822
+ PanelEdgeTab,
1823
+ {
1824
+ side: "left",
1825
+ edge: detailExpanded ? { label: "Shrink panel", onClick: () => onDetailExpandedChange?.(false) } : isCollapsed ? null : { label: "Hide panel", onClick: () => changeCollapsed(true) },
1826
+ center: detailExpanded ? null : isCollapsed ? { label: "Show panel", onClick: () => changeCollapsed(false) } : onDetailExpandedChange ? { label: "Extend panel", onClick: () => onDetailExpandedChange(true) } : null
1827
+ }
1828
+ ) : null;
1829
+ const actions = /* @__PURE__ */ jsxs17(Fragment6, { children: [
1830
+ !isLoading && /* @__PURE__ */ jsx23(
1831
+ GenreSearch,
1832
+ {
1833
+ genrePlaylists: visibleGenrePlaylists,
1834
+ onSelect: handleGenreSearchSelect,
1835
+ selectedName: selectedGenrePlaylist?.name ?? builtInArtistPanel?.title ?? null,
1836
+ onClear: builtInArtistPanel ? () => changeArtist(null) : handleGenreSearchClear,
1837
+ onPrevious: handlePrevious,
1838
+ onQueryChange: artistSearch ? setSearchText : void 0,
1839
+ extraResults: artistExtraResults
1840
+ }
1841
+ ),
1842
+ !isLoading && filterArtist && /* @__PURE__ */ jsxs17("span", { className: "inline-flex items-center overflow-hidden rounded-full bg-[var(--color-neutral-100)] text-sm", children: [
1843
+ /* @__PURE__ */ jsx23(
1844
+ "button",
1845
+ {
1846
+ type: "button",
1847
+ className: "py-1 pl-3 pr-2 hover:underline",
1848
+ "aria-label": `Show artist ${filterArtist.name}`,
1849
+ onClick: () => {
1850
+ if (selectedNodeId !== null) clearSelection();
1851
+ },
1852
+ children: filterArtist.name
1853
+ }
1854
+ ),
1855
+ /* @__PURE__ */ jsx23(
1856
+ "button",
1857
+ {
1858
+ type: "button",
1859
+ className: "py-1 pl-1 pr-3 hover:text-[var(--color-text)]",
1860
+ "aria-label": `Clear artist filter ${filterArtist.name}`,
1861
+ onClick: () => changeArtist(null),
1862
+ children: "\xD7"
1863
+ }
1864
+ )
1865
+ ] }),
1866
+ !isLoading && !isControlled && /* @__PURE__ */ jsx23(
1867
+ SegmentedControl,
1868
+ {
1869
+ "aria-label": "Tree view mode",
1870
+ value: viewMode,
1871
+ onChange: setViewMode,
1872
+ options: [
1873
+ { value: "pop-core", label: "Pop/Core", disabled: !canShowPopCore, title: canShowPopCore ? void 0 : NO_POP_ROOT_TITLE },
1874
+ { value: "wheel", label: "Wheel" },
1875
+ { value: "stacked", label: "Stacked" },
1876
+ { value: "outline", label: "Outline", disabled: !canShowPopCore, title: canShowPopCore ? void 0 : NO_POP_ROOT_TITLE }
1877
+ ]
1878
+ }
1879
+ ),
1880
+ !isLoading && showDisplayOptions && /* @__PURE__ */ jsx23(
1881
+ "div",
1882
+ {
1883
+ className: "flex items-center gap-1",
1884
+ role: "group",
1885
+ "aria-label": "Tree display options",
1886
+ children: !isRotationControlled && viewMode !== "stacked" && viewMode !== "outline" && /* @__PURE__ */ jsx23(
1887
+ Button,
1888
+ {
1889
+ variant: allowWheelRotation ? "default" : "outline",
1890
+ size: "sm",
1891
+ onClick: toggleWheelRotation,
1892
+ children: "Rotation"
1893
+ }
1894
+ )
1895
+ }
1896
+ ),
1897
+ !isLoading && !readOnly && showAddRoot && /* @__PURE__ */ jsx23(
1898
+ IconTextButton,
1899
+ {
1900
+ icon: Plus2,
1901
+ text: "Add root",
1902
+ onClick: () => handleGenreCreationAction(null)
1903
+ }
1904
+ )
1905
+ ] });
1906
+ return /* @__PURE__ */ jsxs17(
1907
+ "div",
1908
+ {
1909
+ ref: rootRef,
1910
+ className: ["genre-tree-view relative flex flex-col h-full", placement === "regions" && REGIONS_CLASS, className].filter(Boolean).join(" "),
1911
+ children: [
1912
+ /* @__PURE__ */ jsx23("style", { children: PREVIOUS_BUTTON_CSS }),
1913
+ /* @__PURE__ */ jsx23(
1914
+ "div",
1915
+ {
1916
+ className: `actions-container absolute z-30 flex items-center gap-2 ${placement === "regions" ? "left-[calc(var(--layout-gutter)+var(--layout-panel-inset))] top-[calc(var(--layout-gutter)+var(--layout-panel-inset))]" : "left-5 top-5"}`,
1917
+ children: actions
1918
+ }
1919
+ ),
1920
+ detailTab && /* @__PURE__ */ jsx23(
1921
+ "div",
1922
+ {
1923
+ className: `absolute top-1/2 z-30 -translate-y-1/2 transition-[opacity,visibility] duration-150 ease-out ${isTreeReady ? "opacity-100" : "pointer-events-none invisible opacity-0"} ${detailExpanded ? "right-[calc(var(--layout-center-right)-var(--layout-edge-tab-w))]" : isCollapsed ? "left-0" : "left-[calc(var(--layout-gutter)+var(--layout-left-w))]"}`,
1924
+ children: detailTab
1925
+ }
1926
+ ),
1927
+ /* @__PURE__ */ jsx23("div", { className: "content-container flex-1 min-h-0 flex flex-row gap-4", children: /* @__PURE__ */ jsx23("div", { className: "tree-view-container flex-1 min-w-0 flex flex-col h-full", children: isLoading ? /* @__PURE__ */ jsx23(GenreTreeViewSkeleton, { viewMode }) : viewMode === "wheel" ? (
1928
+ // The info panel's top edge aligns with the tree's; the search bar floats `--layout-panel-inset` below it,
1929
+ // and the header inset (the bar's height plus that inset) extends the header's fill behind the bar,
1930
+ // Google Maps style; the 8px bottom padding mirrors the inset above the bar, and the hidden title's height is zeroed.
1931
+ // The panel's own Zoom in button is hidden: the search bar carries it.
1932
+ /* @__PURE__ */ jsx23("div", { className: "tree-container search-overlay relative flex-1 w-full min-h-0 [--gtv-info-panel-header-inset:calc(var(--layout-search-h,2.5rem)+var(--layout-panel-inset,0.5rem))] [&_.gtv-info-panel-header]:pb-2 [&_.gtv-info-panel-title--hidden]:h-0 [&_.gtv-toolbar]:hidden", children: /* @__PURE__ */ jsx23(GenreTreeWheelHandoff, { skeleton: /* @__PURE__ */ jsx23(GenreTreeWheelSkeleton, {}), onRevealedChange: setIsWheelRevealed, children: /* @__PURE__ */ jsx23(
1933
+ GenrePlaylistTreeWheel,
1934
+ {
1935
+ scope,
1936
+ genrePlaylists: visibleGenrePlaylists,
1937
+ handleGenreCreationAction,
1938
+ handleGenreRenameAction,
1939
+ additionalActions,
1940
+ onSelectedNodeChange: handleSelectedNodeChange,
1941
+ hideInfoPanelClose: true,
1942
+ hideInfoPanelTitle: true,
1943
+ onNodeHover: handleNodeHover,
1944
+ renderInfoPanelBody,
1945
+ playback,
1946
+ selectedNodeId,
1947
+ readOnly,
1948
+ allowWheelRotation,
1949
+ controlsOrientation,
1950
+ defaultViewport,
1951
+ onViewportChange,
1952
+ ...infoPanelCuration
1953
+ }
1954
+ ) }) })
1955
+ ) : viewMode === "pop-core" ? (
1956
+ // Same as the wheel: the header inset extends its fill behind the floating search bar, Google Maps style.
1957
+ /* @__PURE__ */ jsx23("div", { className: "tree-container search-overlay relative flex-1 w-full min-h-0 [--gtv-info-panel-header-inset:calc(var(--layout-search-h,2.5rem)+var(--layout-panel-inset,0.5rem))] [&_.gtv-info-panel-header]:pb-2 [&_.gtv-info-panel-title--hidden]:h-0 [&_.gtv-toolbar]:hidden [&_.gtv-info-panel-zoom-in]:hidden", children: /* @__PURE__ */ jsx23(GenreTreeWheelHandoff, { skeleton: /* @__PURE__ */ jsx23(GenreTreeWheelSkeleton, {}), onRevealedChange: setIsWheelRevealed, children: /* @__PURE__ */ jsx23(
1958
+ GenrePlaylistTreeWheelRadialPopCore,
1959
+ {
1960
+ scope,
1961
+ genrePlaylists: visibleGenrePlaylists,
1962
+ handleGenreCreationAction,
1963
+ handleGenreRenameAction,
1964
+ additionalActions,
1965
+ onSelectedNodeChange: handleSelectedNodeChange,
1966
+ hideInfoPanelClose: true,
1967
+ hideInfoPanelTitle: true,
1968
+ onNodeHover: handleNodeHover,
1969
+ renderInfoPanelBody,
1970
+ playback,
1971
+ selectedNodeId,
1972
+ readOnly,
1973
+ allowWheelRotation,
1974
+ controlsOrientation,
1975
+ defaultViewport,
1976
+ onViewportChange,
1977
+ ...infoPanelCuration,
1978
+ ...focus
1979
+ }
1980
+ ) }) })
1981
+ ) : viewMode === "outline" ? /* @__PURE__ */ jsx23("div", { className: "tree-container flex-1 min-h-0 w-full overflow-y-auto relative [--gtv-info-panel-header-inset:calc(var(--layout-search-h,2.5rem)+var(--layout-panel-inset,0.5rem)+0.75rem)] [&_.gtv-toolbar]:hidden", children: /* @__PURE__ */ jsx23(
1982
+ GenrePlaylistTreeWheelRadialPopCore,
1983
+ {
1984
+ outline: true,
1985
+ scope,
1986
+ genrePlaylists: visibleGenrePlaylists,
1987
+ handleGenreCreationAction,
1988
+ handleGenreRenameAction,
1989
+ additionalActions,
1990
+ onSelectedNodeChange: handleSelectedNodeChange,
1991
+ hideInfoPanelClose: true,
1992
+ hideInfoPanelTitle: true,
1993
+ onNodeHover: handleNodeHover,
1994
+ renderInfoPanelBody,
1995
+ playback,
1996
+ selectedNodeId,
1997
+ readOnly,
1998
+ ...infoPanelCuration
1999
+ }
2000
+ ) }) : /* @__PURE__ */ jsx23("div", { className: "tree-container relative flex flex-col gap-4 w-full pt-14 overflow-x-auto overflow-y-auto text-[var(--color-text)] [&_.gtv-toolbar]:hidden", children: Object.entries(groupedGenrePlaylistsByRoot).map(
2001
+ ([uuid, genrePlaylistTreePerRoot], rootIndex) => {
2002
+ return /* @__PURE__ */ jsx23(
2003
+ "div",
2004
+ {
2005
+ className: "tree-per-root-container relative shrink-0 h-[28rem] mt-2 mr-16 p-2 bg-[var(--color-neutral-100)] rounded-lg",
2006
+ children: /* @__PURE__ */ jsx23("div", { className: "graph-container relative z-10 h-full", children: /* @__PURE__ */ jsx23(
2007
+ GenrePlaylistTreePerRoot,
2008
+ {
2009
+ scope,
2010
+ rootUuid: uuid,
2011
+ genrePlaylistTreePerRoot,
2012
+ handleGenreCreationAction,
2013
+ handleGenreRenameAction,
2014
+ additionalActions,
2015
+ onSelectedNodeChange: handleSelectedNodeChange,
2016
+ hideInfoPanelClose: true,
2017
+ hideInfoPanelTitle: true,
2018
+ onNodeHover: handleNodeHover,
2019
+ renderInfoPanelBody,
2020
+ playback,
2021
+ selectedNodeId: uuid === selectedGenrePlaylist?.root.uuid ? selectedNodeId : null,
2022
+ readOnly,
2023
+ wheelZoom: "modifier",
2024
+ controlsOrientation,
2025
+ defaultViewport: uuid === selectedGenrePlaylist?.root.uuid ? defaultViewport : void 0,
2026
+ onViewportChange: uuid === selectedGenrePlaylist?.root.uuid ? onViewportChange : void 0,
2027
+ ...infoPanelCuration,
2028
+ overlayPanel: rootIndex === 0 ? activeOverlayPanel : void 0
2029
+ }
2030
+ ) })
2031
+ },
2032
+ uuid
2033
+ );
2034
+ }
2035
+ ) }) }) })
2036
+ ]
2037
+ }
2038
+ );
2039
+ }
2040
+
2041
+ // src/genre-tree/index.ts
2042
+ import {
2043
+ GenreTreeSkeleton,
2044
+ GenreTreeWheelSkeleton as GenreTreeWheelSkeleton2,
2045
+ GenreTreeViewSkeleton as GenreTreeViewSkeleton2,
2046
+ GenreTreeOutlineSkeleton
2047
+ } from "@behindthemusictree/genre-tree-view";
2048
+
2049
+ // src/genre-tree/track-list-sidebar/TrackListSidebar.tsx
2050
+ import { useEffect as useEffect5, useRef as useRef5 } from "react";
2051
+ import { useTrackList as useTrackList5 } from "@behindthemusictree/app-kit/genre-tree";
2052
+ import { useTrackListSidebarVisibility } from "@behindthemusictree/app-kit/genre-tree";
2053
+ import { TrackListOriginType as TrackListOriginType2 } from "@behindthemusictree/app-kit/genre-tree";
2054
+ import { useTrackNoun as useTrackNoun6 } from "@behindthemusictree/app-kit/track-noun";
2055
+ import { jsx as jsx24, jsxs as jsxs18 } from "react/jsx-runtime";
2056
+ function TrackListSidebar({
2057
+ className = "",
2058
+ renderDuration,
2059
+ renderActions,
2060
+ layout = "fixed",
2061
+ renderArtistLink
2062
+ }) {
2063
+ const { trackList, loadMore } = useTrackList5();
2064
+ const trackNoun = useTrackNoun6();
2065
+ const loadMoreSentinelRef = useRef5(null);
2066
+ const hasMore = !!trackList && trackList.nextPage !== null;
2067
+ useEffect5(() => {
2068
+ const sentinel = loadMoreSentinelRef.current;
2069
+ if (!hasMore || !sentinel) return;
2070
+ const observer = new IntersectionObserver((entries) => {
2071
+ if (entries.some((entry) => entry.isIntersecting)) {
2072
+ loadMore().catch((error) => console.error("Failed to load more tracks:", error));
2073
+ }
2074
+ });
2075
+ observer.observe(sentinel);
2076
+ return () => observer.disconnect();
2077
+ }, [hasMore, loadMore]);
2078
+ const { hideTrackListSidebar } = useTrackListSidebarVisibility();
2079
+ const positionClasses = layout === "inline" ? "relative w-full flex h-full flex-col" : "fixed right-0 w-[50rem]";
2080
+ return trackList ? /* @__PURE__ */ jsxs18(
2081
+ "div",
2082
+ {
2083
+ "data-theme": "dark",
2084
+ className: `track-list-sidebar ${positionClasses} rounded-2xl bg-[var(--color-surface)] pb-1 ${className}`,
2085
+ style: layout === "fixed" ? {
2086
+ bottom: "79px"
2087
+ /* Match player height exactly */
2088
+ } : void 0,
2089
+ children: [
2090
+ /* @__PURE__ */ jsxs18("div", { className: "header flex h-16 px-4 py-2 text-[var(--color-text)]", children: [
2091
+ /* @__PURE__ */ jsxs18("div", { className: "origin flex text-xl ", children: [
2092
+ /* @__PURE__ */ jsx24("div", { className: "from h-auto flex flex-col justify-center items-center mr-2", children: "From" }),
2093
+ /* @__PURE__ */ jsx24("div", { className: "name-container flex flex-col justify-center items-center text-[var(--color-text)] font-bold pr-2 max-w-[27.5rem]", children: /* @__PURE__ */ jsx24("div", { className: "name max-w-full truncate", children: trackList.origin.label }) })
2094
+ ] }),
2095
+ /* @__PURE__ */ jsxs18("div", { className: "info flex flex-col justify-center items-center pt-1 mr-2 text-[var(--color-text-muted)]", children: [
2096
+ trackList.origin.type === TrackListOriginType2.GENRE_PLAYLIST ? "\u2022 Genre playlist \u2022 " : trackList.origin.type === TrackListOriginType2.ARTIST ? "\u2022 Artist \u2022 " : `\u2022 ${trackNoun.singular} playlist \u2022 `,
2097
+ `${trackList.total} ${trackList.total > 1 ? trackNoun.plural : trackNoun.singular} \u2022`
2098
+ ] }),
2099
+ /* @__PURE__ */ jsx24(
2100
+ "button",
2101
+ {
2102
+ type: "button",
2103
+ "aria-label": "Close track list",
2104
+ className: "flex-grow flex flex-col items-end justify-center h-full p-3 cursor-pointer text-inherit",
2105
+ onClick: hideTrackListSidebar,
2106
+ children: "\u2715"
2107
+ }
2108
+ )
2109
+ ] }),
2110
+ /* @__PURE__ */ jsxs18(
2111
+ "ul",
2112
+ {
2113
+ className: layout === "inline" ? "track-list flex-1 min-h-0 overflow-auto list-none p-0 m-0" : "track-list overflow-auto max-h-[calc(100vh-63.5px-79px-56px-3.5px)] list-none p-0 m-0",
2114
+ children: [
2115
+ trackList.tracks.map((track, index) => /* @__PURE__ */ jsx24("li", { children: /* @__PURE__ */ jsx24(
2116
+ TrackItem,
2117
+ {
2118
+ track,
2119
+ position: index + 1,
2120
+ renderDuration,
2121
+ renderActions,
2122
+ renderArtistLink
2123
+ }
2124
+ ) }, track.uuid)),
2125
+ hasMore && /* @__PURE__ */ jsx24("li", { ref: loadMoreSentinelRef, "aria-hidden": "true", className: "h-px" })
2126
+ ]
2127
+ }
2128
+ )
2129
+ ]
2130
+ }
2131
+ ) : null;
2132
+ }
2133
+
2134
+ // src/genre-tree/PlayerPanel.tsx
2135
+ import { useState as useState5 } from "react";
2136
+ import { X as X2 } from "lucide-react";
2137
+ import { twMerge as twMerge2 } from "tailwind-merge";
2138
+ import {
2139
+ usePlayer as usePlayer9,
2140
+ PlayerVideoSurface
2141
+ } from "@behindthemusictree/app-kit/player";
2142
+ import {
2143
+ useTrackListSidebarVisibility as useTrackListSidebarVisibility2
2144
+ } from "@behindthemusictree/app-kit/genre-tree";
2145
+ import { jsx as jsx25, jsxs as jsxs19 } from "react/jsx-runtime";
2146
+ var PANEL_ID = "player-panel";
2147
+ var iconButtonClassName = "flex h-6 w-6 shrink-0 items-center justify-center p-0 rounded-md hover:bg-white/10";
2148
+ var DEFAULT_EXTENDED_WIDTH = "calc(100vw - var(--layout-center-left) - var(--layout-gutter))";
2149
+ function PlayerPanel({
2150
+ className,
2151
+ isVisible = true,
2152
+ onClose,
2153
+ expanded,
2154
+ onExpandedChange,
2155
+ extendedWidth = DEFAULT_EXTENDED_WIDTH,
2156
+ renderArtistLink
2157
+ }) {
2158
+ const { playerTrackObject } = usePlayer9();
2159
+ const { toggleTrackListSidebar, isTrackListSidebarVisible } = useTrackListSidebarVisibility2();
2160
+ const [uncontrolledExtended, setUncontrolledExtended] = useState5(false);
2161
+ const isExtendRequested = expanded ?? uncontrolledExtended;
2162
+ const setIsExtendRequested = (next) => {
2163
+ setUncontrolledExtended(next);
2164
+ onExpandedChange?.(next);
2165
+ };
2166
+ const isExtended = isExtendRequested && isTrackListSidebarVisible;
2167
+ const tabEdge = !isTrackListSidebarVisible ? null : isExtended ? {
2168
+ label: "Shrink song list panel",
2169
+ onClick: () => setIsExtendRequested(false)
2170
+ } : { label: "Hide song list", onClick: toggleTrackListSidebar };
2171
+ const tabCenter = !isTrackListSidebarVisible ? { label: "Show song list", onClick: toggleTrackListSidebar } : isExtended ? null : {
2172
+ label: "Extend song list panel",
2173
+ onClick: () => setIsExtendRequested(true)
2174
+ };
2175
+ return /* @__PURE__ */ jsxs19(
2176
+ "div",
2177
+ {
2178
+ id: PANEL_ID,
2179
+ className: twMerge2(
2180
+ "fixed right-[var(--layout-gutter)] top-[var(--layout-band-top)] z-40 max-h-[calc(100vh-var(--layout-band-top)-3.5rem)]",
2181
+ !isExtended && "w-[var(--layout-right-w)]",
2182
+ isTrackListSidebarVisible && "h-[calc(100vh-var(--layout-band-top)-3.5rem)]",
2183
+ !isVisible && "hidden",
2184
+ className
2185
+ ),
2186
+ style: isExtended ? { width: extendedWidth } : void 0,
2187
+ children: [
2188
+ /* @__PURE__ */ jsx25(
2189
+ PanelEdgeTab,
2190
+ {
2191
+ side: "right",
2192
+ edge: tabEdge,
2193
+ center: tabCenter,
2194
+ controls: PANEL_ID,
2195
+ className: "absolute right-full top-1/2 -translate-y-1/2"
2196
+ }
2197
+ ),
2198
+ /* @__PURE__ */ jsxs19(
2199
+ "div",
2200
+ {
2201
+ className: twMerge2(
2202
+ "flex h-full max-h-[inherit] overflow-hidden rounded-[var(--radius-panel,1rem)] bg-zinc-950 text-zinc-100 shadow-[0_1px_2px_rgba(0,0,0,0.5),0_2px_6px_2px_rgba(0,0,0,0.3)]",
2203
+ isExtended ? "flex-row" : "flex-col"
2204
+ ),
2205
+ children: [
2206
+ /* @__PURE__ */ jsxs19("div", { className: "flex w-[var(--layout-right-w)] shrink-0 flex-col", children: [
2207
+ /* @__PURE__ */ jsxs19("div", { className: "flex shrink-0 items-center justify-between gap-2 border-b border-zinc-800 p-2", children: [
2208
+ /* @__PURE__ */ jsx25("span", { className: "min-w-0 truncate text-sm font-semibold", children: playerTrackObject?.track?.title }),
2209
+ /* @__PURE__ */ jsx25("div", { className: "flex shrink-0 items-center gap-1", children: /* @__PURE__ */ jsx25(
2210
+ "button",
2211
+ {
2212
+ type: "button",
2213
+ onClick: onClose,
2214
+ className: iconButtonClassName,
2215
+ "aria-label": "Close player",
2216
+ children: /* @__PURE__ */ jsx25(X2, { size: 16 })
2217
+ }
2218
+ ) })
2219
+ ] }),
2220
+ /* @__PURE__ */ jsxs19("div", { className: "relative w-full shrink-0", children: [
2221
+ /* @__PURE__ */ jsx25(PlayerVideoSurface, { className: "aspect-video w-full bg-black" }),
2222
+ playerTrackObject?.loadError && /* @__PURE__ */ jsx25("span", { className: "absolute bottom-2 left-2 right-2 text-xs text-red-400", children: playerTrackObject.loadError })
2223
+ ] })
2224
+ ] }),
2225
+ isTrackListSidebarVisible && /* @__PURE__ */ jsx25(
2226
+ TrackListSidebar,
2227
+ {
2228
+ layout: "inline",
2229
+ className: twMerge2(
2230
+ "min-h-0 min-w-0 flex-1",
2231
+ isExtended && "border-l border-zinc-800"
2232
+ ),
2233
+ renderArtistLink
2234
+ }
2235
+ )
2236
+ ]
2237
+ }
2238
+ )
2239
+ ]
2240
+ }
2241
+ );
2242
+ }
2243
+
2244
+ // src/genre-tree/Rating.tsx
2245
+ import { v4 as uuidv4 } from "uuid";
2246
+ import { AiFillStar, AiOutlineStar } from "react-icons/ai";
2247
+ import { FORM_RATING_NULL_VALUE } from "@behindthemusictree/app-kit/genre-tree";
2248
+ import { jsx as jsx26, jsxs as jsxs20 } from "react/jsx-runtime";
2249
+ function Rating({ rating, handleChange }) {
2250
+ const instanceId = uuidv4();
2251
+ return /* @__PURE__ */ jsx26("div", { className: "flex items-center", children: [...Array(6)].map((_, i) => /* @__PURE__ */ jsxs20("div", { className: `relative ${i === 0 ? "mr-4" : "mr-1"}`, children: [
2252
+ /* @__PURE__ */ jsx26(
2253
+ "input",
2254
+ {
2255
+ id: `star-${instanceId}-${i}`,
2256
+ className: "w-6 h-6 opacity-0",
2257
+ type: "radio",
2258
+ name: "rating",
2259
+ value: i === 0 ? FORM_RATING_NULL_VALUE : i,
2260
+ checked: rating === (i === 0 ? null : i),
2261
+ onChange: handleChange
2262
+ }
2263
+ ),
2264
+ /* @__PURE__ */ jsx26("label", { htmlFor: `star-${instanceId}-${i}`, className: "cursor-pointer", children: i === 0 ? /* @__PURE__ */ jsxs20("div", { className: "flex", children: [
2265
+ /* @__PURE__ */ jsx26(
2266
+ AiOutlineStar,
2267
+ {
2268
+ className: `w-7 h-7 absolute top-0 left-0 ${rating === FORM_RATING_NULL_VALUE ? "text-[var(--color-accent-rating)]" : "text-[var(--color-text-muted)]"}`
2269
+ }
2270
+ ),
2271
+ /* @__PURE__ */ jsx26("span", { className: "flex-grow w-8" }),
2272
+ " "
2273
+ ] }) : /* @__PURE__ */ jsx26(
2274
+ AiFillStar,
2275
+ {
2276
+ className: `w-7 h-7 absolute top-0 left-0 ${(rating ?? -1) >= i ? "text-[var(--color-accent-rating)]" : "text-[var(--color-text-muted)]"}`
2277
+ }
2278
+ ) })
2279
+ ] }, i)) });
2280
+ }
2281
+
2282
+ export {
2283
+ GenrePlaylistTreePerRoot,
2284
+ GenreSearch,
2285
+ PanelEdgeTab,
2286
+ TrackPositionPlayPause,
2287
+ TrackItem,
2288
+ GenreTreeView,
2289
+ TrackListSidebar,
2290
+ PlayerPanel,
2291
+ Rating,
2292
+ GenreTreeSkeleton,
2293
+ GenreTreeWheelSkeleton2 as GenreTreeWheelSkeleton,
2294
+ GenreTreeViewSkeleton2 as GenreTreeViewSkeleton,
2295
+ GenreTreeOutlineSkeleton
2296
+ };
2297
+ //# sourceMappingURL=chunk-BPCFNBFO.js.map