@behindthemusictree/app-kit-ui 26.1.0 → 28.0.2

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.
@@ -1,10 +1,11 @@
1
1
  import {
2
2
  BasePopup_default,
3
- InternalErrorPopup
4
- } from "./chunk-VDXUW7PP.js";
3
+ InternalErrorPopup,
4
+ Toast
5
+ } from "./chunk-DKQQLLMY.js";
5
6
 
6
7
  // src/genre-tree/GenreTreeView.tsx
7
- import { useCallback as useCallback5, useDeferredValue, useEffect as useEffect4, useMemo as useMemo6, useRef as useRef4, useState as useState4 } from "react";
8
+ import { useCallback as useCallback6, useDeferredValue, useEffect as useEffect5, useMemo as useMemo7, useRef as useRef4, useState as useState5 } from "react";
8
9
  import { useIsMutating } from "@tanstack/react-query";
9
10
  import { Plus as Plus2 } from "lucide-react";
10
11
  import { IconTextButton, Button, SegmentedControl as SegmentedControl2 } from "@behindthemusictree/ui";
@@ -15,9 +16,9 @@ import {
15
16
  import {
16
17
  useArtistSearch
17
18
  } from "@behindthemusictree/app-kit/genre-tree";
18
- import { useGenreTreeViewState, useArtist as useArtist2, genreUpdateMutationKey } from "@behindthemusictree/app-kit/genre-tree";
19
+ import { useGenreTreeViewState, useArtist as useArtist2, useUpdateGenre as useUpdateGenre2, genreUpdateMutationKey } from "@behindthemusictree/app-kit/genre-tree";
19
20
  import { usePlayer as usePlayer8 } from "@behindthemusictree/app-kit/player";
20
- import { usePopup as usePopup4 } from "@behindthemusictree/app-kit/popup";
21
+ import { usePopup as usePopup5 } from "@behindthemusictree/app-kit/popup";
21
22
 
22
23
  // src/genre-tree/GenreDetailExtras.tsx
23
24
  import { Skeleton as Skeleton2 } from "@behindthemusictree/ui";
@@ -26,7 +27,7 @@ import { useFetchGenreOverview } from "@behindthemusictree/app-kit/genre-tree";
26
27
  // src/genre-tree/ClampedText.tsx
27
28
  import { useLayoutEffect, useRef, useState } from "react";
28
29
  import { Fragment, jsx, jsxs } from "react/jsx-runtime";
29
- function ClampedText({ children, className = "" }) {
30
+ function ClampedText({ children, className: className2 = "" }) {
30
31
  const ref = useRef(null);
31
32
  const [expanded, setExpanded] = useState(false);
32
33
  const [overflows, setOverflows] = useState(false);
@@ -36,7 +37,7 @@ function ClampedText({ children, className = "" }) {
36
37
  setOverflows(element.scrollHeight > element.clientHeight);
37
38
  }, [children, expanded]);
38
39
  return /* @__PURE__ */ jsxs(Fragment, { children: [
39
- /* @__PURE__ */ jsx("p", { ref, className: `${className} ${expanded ? "" : "line-clamp-3"}`, children }),
40
+ /* @__PURE__ */ jsx("p", { ref, className: `${className2} ${expanded ? "" : "line-clamp-3"}`, children }),
40
41
  (overflows || expanded) && /* @__PURE__ */ jsx(
41
42
  "button",
42
43
  {
@@ -63,20 +64,6 @@ import { parseWithLog } from "@behindthemusictree/app-kit/transport";
63
64
  import { usePlayer } from "@behindthemusictree/app-kit/player";
64
65
  import { PlayStates } from "@behindthemusictree/app-kit/player";
65
66
  import { usePopup } from "@behindthemusictree/app-kit/popup";
66
-
67
- // src/genre-tree/UnplayableReason.tsx
68
- import { FaYoutube } from "react-icons/fa";
69
- import { NO_VIDEO_LABEL } from "@behindthemusictree/app-kit/genre-tree";
70
- import { Fragment as Fragment2, jsx as jsx2, jsxs as jsxs2 } from "react/jsx-runtime";
71
- function UnplayableReason({ reason }) {
72
- if (reason !== NO_VIDEO_LABEL) return /* @__PURE__ */ jsx2(Fragment2, { children: reason });
73
- return /* @__PURE__ */ jsxs2("span", { role: "img", "aria-label": reason, className: "relative inline-flex shrink-0 items-center", children: [
74
- /* @__PURE__ */ jsx2(FaYoutube, { size: 16, "aria-hidden": "true" }),
75
- /* @__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" })
76
- ] });
77
- }
78
-
79
- // src/genre-tree/GenrePlaylistTracks.tsx
80
67
  import { ErrorCode } from "@behindthemusictree/app-kit/transport";
81
68
  import { genrePlaylistQueryKeys } from "@behindthemusictree/app-kit/genre-tree";
82
69
  import { libraryEndpoints, libraryQueryKeys } from "@behindthemusictree/app-kit/genre-tree";
@@ -84,23 +71,25 @@ import { isSong, songUnplayableReason, SongDetailedSchema } from "@behindthemusi
84
71
  import { useTrackList } from "@behindthemusictree/app-kit/genre-tree";
85
72
 
86
73
  // src/genre-tree/MusicBrainzRecordingLink.tsx
87
- import { FaExternalLinkAlt } from "react-icons/fa";
88
74
  import { musicBrainzRecordingUrl } from "@behindthemusictree/app-kit/genre-tree";
89
- import { jsx as jsx3 } from "react/jsx-runtime";
90
- function MusicBrainzRecordingLink({ song }) {
75
+ import { Fragment as Fragment2, jsx as jsx2 } from "react/jsx-runtime";
76
+ function MusicBrainzRecordingLink({
77
+ song,
78
+ children,
79
+ className: className2 = ""
80
+ }) {
91
81
  const url = musicBrainzRecordingUrl(song);
92
- if (!url) return null;
93
- return /* @__PURE__ */ jsx3(
82
+ if (!url) return /* @__PURE__ */ jsx2(Fragment2, { children });
83
+ return /* @__PURE__ */ jsx2(
94
84
  "a",
95
85
  {
96
86
  href: url,
97
87
  target: "_blank",
98
88
  rel: "noopener noreferrer",
99
89
  "aria-label": `View ${song.title} on MusicBrainz`,
100
- title: "View on MusicBrainz",
101
- className: "inline-flex shrink-0 items-center opacity-50 hover:opacity-100 focus-visible:opacity-100",
90
+ className: className2,
102
91
  onClick: (event) => event.stopPropagation(),
103
- children: /* @__PURE__ */ jsx3(FaExternalLinkAlt, { size: 10 })
92
+ children
104
93
  }
105
94
  );
106
95
  }
@@ -108,7 +97,7 @@ function MusicBrainzRecordingLink({ song }) {
108
97
  // src/genre-tree/GenrePlaylistTracks.tsx
109
98
  import { ArtistNames } from "@behindthemusictree/app-kit/genre-tree";
110
99
  import { capitalize, useTrackNoun } from "@behindthemusictree/app-kit/track-noun";
111
- import { jsx as jsx4, jsxs as jsxs3 } from "react/jsx-runtime";
100
+ import { jsx as jsx3, jsxs as jsxs2 } from "react/jsx-runtime";
112
101
  var CATALOGUE_PAGE_SIZE = 100;
113
102
  var SongPageSchema = PaginatedResponseSchema(SongDetailedSchema).extend({
114
103
  overallTotal: z.number().nullable(),
@@ -180,90 +169,89 @@ function GenrePlaylistTracks({
180
169
  const started = artist ? playNewTrackListFromArtist(artist, scope, { startSongUuid, ordering: "title", genreUuid }) : playNewTrackListFromGenrePlaylist(genrePlaylist, scope, { startSongUuid });
181
170
  started.catch((error) => {
182
171
  console.error("Failed to play genre playlist:", error);
183
- showPopup(/* @__PURE__ */ jsx4(InternalErrorPopup, { errorCode: ErrorCode.CLIENT_UNKNOWN }));
172
+ showPopup(/* @__PURE__ */ jsx3(InternalErrorPopup, { errorCode: ErrorCode.CLIENT_UNKNOWN }));
184
173
  });
185
174
  };
186
- return /* @__PURE__ */ jsxs3("div", { className: "gtv-info-panel-children", children: [
187
- /* @__PURE__ */ jsxs3("span", { className: "gtv-info-panel-children-title", children: [
175
+ return /* @__PURE__ */ jsxs2("div", { className: "gtv-info-panel-children", children: [
176
+ /* @__PURE__ */ jsxs2("span", { className: "gtv-info-panel-children-title", children: [
188
177
  capitalize(trackNoun.plural),
189
178
  total !== void 0 && ` (${total})`
190
179
  ] }),
191
180
  renderTracksToolbar?.(),
192
- isPending ? /* @__PURE__ */ jsx4(Skeleton, { className: "h-4 w-3/4", "aria-busy": "true" }) : isError && tracks.length === 0 ? /* @__PURE__ */ jsxs3("p", { role: "alert", children: [
181
+ isPending ? /* @__PURE__ */ jsx3(Skeleton, { className: "h-4 w-3/4", "aria-busy": "true" }) : isError && tracks.length === 0 ? /* @__PURE__ */ jsxs2("p", { role: "alert", children: [
193
182
  "Couldn't load ",
194
183
  trackNoun.plural,
195
184
  ".",
196
185
  " ",
197
- /* @__PURE__ */ jsx4("button", { type: "button", className: "underline", onClick: () => void refetch(), children: "Retry" })
198
- ] }) : 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: [
186
+ /* @__PURE__ */ jsx3("button", { type: "button", className: "underline", onClick: () => void refetch(), children: "Retry" })
187
+ ] }) : tracks.length === 0 ? /* @__PURE__ */ jsx3("p", { children: "\u2014" }) : /* @__PURE__ */ jsxs2("ul", { className: "mt-1 max-h-64 overflow-y-auto text-[0.8125rem] leading-[1.45] [container-type:inline-size]", "aria-label": `${genrePlaylist.name} ${trackNoun.plural}`, children: [
199
188
  tracks.map((track, index) => {
200
189
  const label = track.artists?.length ? `${track.title} \u2014 ${track.artists.map((artist2) => artist2.name).join(", ")}` : track.title;
201
190
  const isCurrent = playerTrackObject?.track.id === track.uuid;
202
191
  const isPlaying = isCurrent && playState === PlayStates.PLAYING;
203
- const text = /* @__PURE__ */ jsxs3("span", { className: TEXT_CELL, children: [
204
- /* @__PURE__ */ jsx4("span", { className: TITLE_CELL, children: track.title }),
205
- track.artists?.length ? /* @__PURE__ */ jsx4("span", { className: SEPARATOR, children: " \u2014 " }) : null,
206
- /* @__PURE__ */ jsx4("span", { className: COLUMN_CELL, children: track.artists?.length ? /* @__PURE__ */ jsx4(ArtistNames, { artists: track.artists, renderArtistLink }) : null }),
207
- track.album && /* @__PURE__ */ jsx4("span", { className: ALBUM_CELL, children: track.album.name })
192
+ const text = /* @__PURE__ */ jsxs2("span", { className: TEXT_CELL, children: [
193
+ /* @__PURE__ */ jsx3("span", { className: TITLE_CELL, children: isSong(track) ? /* @__PURE__ */ jsx3(MusicBrainzRecordingLink, { song: track, className: "hover:underline focus-visible:underline", children: track.title }) : track.title }),
194
+ track.artists?.length ? /* @__PURE__ */ jsx3("span", { className: SEPARATOR, children: " \u2014 " }) : null,
195
+ /* @__PURE__ */ jsx3("span", { className: COLUMN_CELL, children: track.artists?.length ? /* @__PURE__ */ jsx3(ArtistNames, { artists: track.artists, renderArtistLink }) : null }),
196
+ track.album && /* @__PURE__ */ jsx3("span", { className: ALBUM_CELL, children: track.album.name })
208
197
  ] });
209
198
  const leading = renderSongLeading?.(track, index);
210
199
  const actions = renderSongActions?.(track);
211
- const unplayableReason = isSong(track) ? songUnplayableReason(track) : null;
212
- if (unplayableReason) {
213
- return /* @__PURE__ */ jsxs3("li", { className: "flex min-h-8 items-center gap-1 px-1 opacity-50", title: unplayableReason, children: [
200
+ const isUnplayable = isSong(track) && songUnplayableReason(track) !== null;
201
+ if (isUnplayable) {
202
+ return /* @__PURE__ */ jsxs2("li", { className: "flex min-h-8 items-center gap-1 px-1 opacity-50", children: [
214
203
  leading,
215
- /* @__PURE__ */ jsx4(
204
+ /* @__PURE__ */ jsx3(
216
205
  "button",
217
206
  {
218
207
  type: "button",
219
208
  disabled: true,
220
209
  "aria-disabled": "true",
221
- "aria-label": `${label} \u2014 ${unplayableReason}`,
210
+ "aria-label": label,
222
211
  className: "flex h-6 w-6 shrink-0 items-center justify-center rounded-full opacity-0 cursor-not-allowed",
223
- children: /* @__PURE__ */ jsx4(FaPlay, { size: 10 })
212
+ children: /* @__PURE__ */ jsx3(FaPlay, { size: 10 })
224
213
  }
225
214
  ),
226
215
  text,
227
- isSong(track) && /* @__PURE__ */ jsx4(MusicBrainzRecordingLink, { song: track }),
228
- /* @__PURE__ */ jsx4("span", { className: "shrink-0 text-xs", children: /* @__PURE__ */ jsx4(UnplayableReason, { reason: unplayableReason }) }),
229
216
  actions
230
217
  ] }, track.uuid);
231
218
  }
232
- return /* @__PURE__ */ jsxs3(
219
+ return /* @__PURE__ */ jsxs2(
233
220
  "li",
234
221
  {
235
222
  className: `group flex min-h-8 items-center gap-1 rounded px-1 hover:bg-[var(--color-neutral-100)] ${isCurrent ? "font-medium text-[var(--color-primary)]" : ""}`,
236
223
  children: [
237
224
  leading,
238
- /* @__PURE__ */ jsx4(
225
+ /* @__PURE__ */ jsx3(
239
226
  "button",
240
227
  {
241
228
  type: "button",
242
229
  "aria-label": `${isPlaying ? "Pause" : "Play"} ${label}`,
243
230
  className: `flex h-6 w-6 shrink-0 items-center justify-center rounded-full cursor-pointer group-hover:opacity-100 focus-visible:opacity-100 ${isCurrent && playState !== PlayStates.STOPPED ? "opacity-100" : "opacity-0 [@media(pointer:coarse)]:opacity-100"}`,
244
231
  onClick: () => isCurrent ? handlePlayPauseAction() : play(track.uuid),
245
- children: isCurrent && playState === PlayStates.LOADING ? /* @__PURE__ */ jsx4(RingLoader, { size: 10 }) : isPlaying ? /* @__PURE__ */ jsx4(FaPause, { size: 10 }) : /* @__PURE__ */ jsx4(FaPlay, { size: 10 })
232
+ children: isCurrent && playState === PlayStates.LOADING ? /* @__PURE__ */ jsx3(RingLoader, { size: 10 }) : isPlaying ? /* @__PURE__ */ jsx3(FaPause, { size: 10 }) : /* @__PURE__ */ jsx3(FaPlay, { size: 10 })
246
233
  }
247
234
  ),
248
235
  text,
249
- isSong(track) && /* @__PURE__ */ jsx4(MusicBrainzRecordingLink, { song: track }),
250
236
  actions
251
237
  ]
252
238
  },
253
239
  track.uuid
254
240
  );
255
241
  }),
256
- hasNextPage && /* @__PURE__ */ jsx4("li", { ref: sentinelRef, "aria-hidden": "true", className: "h-4" })
242
+ hasNextPage && /* @__PURE__ */ jsx3("li", { ref: sentinelRef, "aria-hidden": "true", className: "h-4" })
257
243
  ] })
258
244
  ] });
259
245
  }
260
246
 
261
247
  // src/genre-tree/GenreDetailExtras.tsx
262
248
  import { useTrackNoun as useTrackNoun2 } from "@behindthemusictree/app-kit/track-noun";
263
- import { Fragment as Fragment3, jsx as jsx5, jsxs as jsxs4 } from "react/jsx-runtime";
249
+ import { SiWikidata, SiWikipedia } from "react-icons/si";
250
+ import { Fragment as Fragment3, jsx as jsx4, jsxs as jsxs3 } from "react/jsx-runtime";
264
251
  function GenreDetailExtras({
265
252
  genreUuid,
266
253
  genrePlaylist,
254
+ wikidataId,
267
255
  scope,
268
256
  getBackendBaseUrl,
269
257
  criteriaOverviewSchema,
@@ -282,52 +270,63 @@ function GenreDetailExtras({
282
270
  criteriaOverviewSchema
283
271
  );
284
272
  if (isPending) {
285
- return /* @__PURE__ */ jsxs4(Fragment3, { children: [
286
- /* @__PURE__ */ jsxs4("div", { className: "gtv-info-panel-children", "aria-busy": "true", "data-testid": "genre-detail-extras-skeleton", children: [
287
- /* @__PURE__ */ jsx5("span", { className: "gtv-info-panel-children-title", children: "Summary" }),
288
- /* @__PURE__ */ jsx5(Skeleton2, { className: "h-4 w-3/4" })
273
+ return /* @__PURE__ */ jsxs3(Fragment3, { children: [
274
+ /* @__PURE__ */ jsxs3("div", { className: "gtv-info-panel-children", "aria-busy": "true", "data-testid": "genre-detail-extras-skeleton", children: [
275
+ /* @__PURE__ */ jsx4("span", { className: "gtv-info-panel-children-title", children: "Summary" }),
276
+ /* @__PURE__ */ jsx4(Skeleton2, { className: "h-4 w-3/4" })
289
277
  ] }),
290
278
  family
291
279
  ] });
292
280
  }
293
- if (!overview) return /* @__PURE__ */ jsx5(Fragment3, { children: family });
294
- const { summary, description, summarySourceUrl, essentialSongs } = overview;
295
- return /* @__PURE__ */ jsxs4(Fragment3, { children: [
296
- 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) }),
297
- renderSummary ? renderSummary(overview) : /* @__PURE__ */ jsxs4("div", { className: "gtv-info-panel-children text-[0.8125rem] leading-[1.45] text-[var(--color-text)]", children: [
298
- /* @__PURE__ */ jsx5("span", { className: "gtv-info-panel-children-title", children: "Summary" }),
299
- /* @__PURE__ */ jsx5(ClampedText, { className: "mt-1", children: summary ?? "\u2014" }, genreUuid),
300
- summarySourceUrl && /* @__PURE__ */ jsx5(
301
- "a",
302
- {
303
- href: summarySourceUrl,
304
- target: "_blank",
305
- rel: "noopener noreferrer",
306
- className: "mt-1 inline-block text-xs text-[var(--color-text-muted)] underline hover:text-[var(--color-text)]",
307
- children: "Source: Wikipedia"
308
- }
309
- )
281
+ if (!overview) return /* @__PURE__ */ jsx4(Fragment3, { children: family });
282
+ const { summary, summarySourceUrl, essentialSongs } = overview;
283
+ const wikidataUrl = wikidataId ? `https://www.wikidata.org/wiki/${encodeURIComponent(wikidataId)}` : null;
284
+ return /* @__PURE__ */ jsxs3(Fragment3, { children: [
285
+ (summarySourceUrl || wikidataUrl) && /* @__PURE__ */ jsxs3("div", { className: "gtv-info-panel-children text-[0.8125rem] leading-[1.45] text-[var(--color-text)]", children: [
286
+ /* @__PURE__ */ jsx4("span", { className: "gtv-info-panel-children-title", children: "Links" }),
287
+ /* @__PURE__ */ jsxs3("div", { className: "mt-1 flex flex-wrap gap-x-3 gap-y-1 text-xs", children: [
288
+ summarySourceUrl && /* @__PURE__ */ jsxs3(
289
+ "a",
290
+ {
291
+ href: summarySourceUrl,
292
+ target: "_blank",
293
+ rel: "noopener noreferrer",
294
+ className: "inline-flex items-center gap-1.5 text-[var(--color-text-muted)] hover:text-[var(--color-text)]",
295
+ children: [
296
+ /* @__PURE__ */ jsx4(SiWikipedia, { "aria-hidden": "true", className: "h-3.5 w-3.5" }),
297
+ "Wikipedia"
298
+ ]
299
+ }
300
+ ),
301
+ wikidataUrl && /* @__PURE__ */ jsxs3(
302
+ "a",
303
+ {
304
+ href: wikidataUrl,
305
+ target: "_blank",
306
+ rel: "noopener noreferrer",
307
+ className: "inline-flex items-center gap-1.5 text-[var(--color-text-muted)] hover:text-[var(--color-text)]",
308
+ children: [
309
+ /* @__PURE__ */ jsx4(SiWikidata, { "aria-hidden": "true", className: "h-3.5 w-3.5" }),
310
+ "Wikidata"
311
+ ]
312
+ }
313
+ )
314
+ ] })
315
+ ] }),
316
+ renderSummary ? renderSummary(overview) : /* @__PURE__ */ jsxs3("div", { className: "gtv-info-panel-children text-[0.8125rem] leading-[1.45] text-[var(--color-text)]", children: [
317
+ /* @__PURE__ */ jsx4("span", { className: "gtv-info-panel-children-title", children: "Summary" }),
318
+ /* @__PURE__ */ jsx4(ClampedText, { className: "mt-1", children: summary ?? "\u2014" }, genreUuid)
310
319
  ] }),
311
- renderSummary && summarySourceUrl && /* @__PURE__ */ jsx5(
312
- "a",
313
- {
314
- href: summarySourceUrl,
315
- target: "_blank",
316
- rel: "noopener noreferrer",
317
- className: "mx-3.5 mb-2.5 inline-block text-xs text-[var(--color-text-muted)] underline hover:text-[var(--color-text)]",
318
- children: "Source: Wikipedia"
319
- }
320
- ),
321
320
  family,
322
321
  renderGenreDetailExtras?.(overview),
323
- renderEssentialTracks ? renderEssentialTracks(overview) : essentialSongs && /* @__PURE__ */ jsxs4("div", { className: "gtv-info-panel-children text-[0.8125rem] leading-[1.45] text-[var(--color-text)]", children: [
324
- /* @__PURE__ */ jsxs4("span", { className: "gtv-info-panel-children-title", children: [
322
+ renderEssentialTracks ? renderEssentialTracks(overview) : essentialSongs && /* @__PURE__ */ jsxs3("div", { className: "gtv-info-panel-children text-[0.8125rem] leading-[1.45] text-[var(--color-text)]", children: [
323
+ /* @__PURE__ */ jsxs3("span", { className: "gtv-info-panel-children-title", children: [
325
324
  "Essential ",
326
325
  trackNoun.plural
327
326
  ] }),
328
- 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" })
327
+ essentialSongs.length > 0 ? /* @__PURE__ */ jsx4("ul", { className: "mt-1 divide-y divide-[var(--color-border)]", children: essentialSongs.map((track) => /* @__PURE__ */ jsx4("li", { className: "py-1", children: track.title }, track.uuid)) }) : /* @__PURE__ */ jsx4("p", { className: "mt-1", children: "\u2014" })
329
328
  ] }),
330
- /* @__PURE__ */ jsx5(
329
+ /* @__PURE__ */ jsx4(
331
330
  GenrePlaylistTracks,
332
331
  {
333
332
  genreUuid,
@@ -384,10 +383,10 @@ function useGenrePlaylistPlayback({
384
383
 
385
384
  // src/genre-tree/playlist-tree/TreePerRoot.tsx
386
385
  import { capitalize as capitalize2, useTrackNoun as useTrackNoun3 } from "@behindthemusictree/app-kit/track-noun";
387
- import { jsx as jsx6 } from "react/jsx-runtime";
386
+ import { jsx as jsx5 } from "react/jsx-runtime";
388
387
  function GenrePlaylistTreePerRoot({
389
388
  scope,
390
- className,
389
+ className: className2,
391
390
  rootUuid,
392
391
  genrePlaylistTreePerRoot,
393
392
  handleGenreCreationAction,
@@ -440,7 +439,7 @@ function GenrePlaylistTreePerRoot({
440
439
  }
441
440
  play(genrePlaylist).catch((error) => {
442
441
  console.error("Failed to load genre playlist tracks:", error);
443
- showPopup(/* @__PURE__ */ jsx6(InternalErrorPopup, { errorCode: ErrorCode2.CLIENT_UNKNOWN }));
442
+ showPopup(/* @__PURE__ */ jsx5(InternalErrorPopup, { errorCode: ErrorCode2.CLIENT_UNKNOWN }));
444
443
  });
445
444
  },
446
445
  [
@@ -472,10 +471,10 @@ function GenrePlaylistTreePerRoot({
472
471
  if (confirm(`Are you sure you want to delete "${node.name}"?`)) {
473
472
  }
474
473
  }, []);
475
- return /* @__PURE__ */ jsx6(
474
+ return /* @__PURE__ */ jsx5(
476
475
  GenreTree,
477
476
  {
478
- className,
477
+ className: className2,
479
478
  nodes,
480
479
  itemCountLabel: `${capitalize2(trackNoun.singular)} count`,
481
480
  rootColor: getGenreTreeColor(rootUuid),
@@ -514,14 +513,21 @@ import { usePopup as usePopup3 } from "@behindthemusictree/app-kit/popup";
514
513
  import { ErrorCode as ErrorCode3 } from "@behindthemusictree/app-kit/transport";
515
514
 
516
515
  // src/genre-tree/playlist-tree/size-metric.ts
517
- var genrePlaylistSize = (genrePlaylist, metric) => metric === "views" ? genrePlaylist.viewsCount : genrePlaylist.tracksCount;
516
+ var hasViewsCount = (genrePlaylists) => genrePlaylists.every((genrePlaylist) => genrePlaylist.viewsCount !== void 0);
517
+ var genrePlaylistSize = (genrePlaylist, metric) => {
518
+ if (metric === "songs") return genrePlaylist.tracksCount;
519
+ if (genrePlaylist.viewsCount === void 0) {
520
+ throw new Error(`Genre playlist ${genrePlaylist.uuid} has no viewsCount to size by views`);
521
+ }
522
+ return genrePlaylist.viewsCount;
523
+ };
518
524
 
519
525
  // src/genre-tree/playlist-tree/TreeWheel.tsx
520
526
  import { capitalize as capitalize3, useTrackNoun as useTrackNoun4 } from "@behindthemusictree/app-kit/track-noun";
521
- import { jsx as jsx7 } from "react/jsx-runtime";
527
+ import { jsx as jsx6 } from "react/jsx-runtime";
522
528
  function GenrePlaylistTreeWheel({
523
529
  scope,
524
- className,
530
+ className: className2,
525
531
  genrePlaylists,
526
532
  handleGenreCreationAction,
527
533
  handleGenreRenameAction,
@@ -575,7 +581,7 @@ function GenrePlaylistTreeWheel({
575
581
  }
576
582
  play(genrePlaylist).catch((error) => {
577
583
  console.error("Failed to load genre playlist tracks:", error);
578
- showPopup(/* @__PURE__ */ jsx7(InternalErrorPopup, { errorCode: ErrorCode3.CLIENT_UNKNOWN }));
584
+ showPopup(/* @__PURE__ */ jsx6(InternalErrorPopup, { errorCode: ErrorCode3.CLIENT_UNKNOWN }));
579
585
  });
580
586
  },
581
587
  [
@@ -607,10 +613,10 @@ function GenrePlaylistTreeWheel({
607
613
  if (confirm(`Are you sure you want to delete "${node.name}"?`)) {
608
614
  }
609
615
  }, []);
610
- return /* @__PURE__ */ jsx7(
616
+ return /* @__PURE__ */ jsx6(
611
617
  GenreTreeWheel,
612
618
  {
613
- className,
619
+ className: className2,
614
620
  nodes,
615
621
  itemCountLabel: sizeMetric === "views" ? "Views" : `${capitalize3(trackNoun.singular)} count`,
616
622
  playingNodeId,
@@ -648,11 +654,11 @@ import {
648
654
  } from "@behindthemusictree/genre-tree-view";
649
655
  import { usePlayer as usePlayer4 } from "@behindthemusictree/app-kit/player";
650
656
  import { capitalize as capitalize4, useTrackNoun as useTrackNoun5 } from "@behindthemusictree/app-kit/track-noun";
651
- import { jsx as jsx8 } from "react/jsx-runtime";
657
+ import { jsx as jsx7 } from "react/jsx-runtime";
652
658
  function GenrePlaylistTreeWheelRadialPopCore(props) {
653
659
  const {
654
660
  scope,
655
- className,
661
+ className: className2,
656
662
  genrePlaylists,
657
663
  handleGenreCreationAction,
658
664
  handleGenreRenameAction,
@@ -742,7 +748,7 @@ function GenrePlaylistTreeWheelRadialPopCore(props) {
742
748
  }
743
749
  }, []);
744
750
  const treeProps = {
745
- className,
751
+ className: className2,
746
752
  nodes,
747
753
  itemCountLabel: sizeMetric === "views" ? "Views" : `${capitalize4(trackNoun.singular)} count`,
748
754
  playingNodeId,
@@ -763,7 +769,7 @@ function GenrePlaylistTreeWheelRadialPopCore(props) {
763
769
  ...infoPanelCuration,
764
770
  readOnly
765
771
  };
766
- return outline ? /* @__PURE__ */ jsx8(GenreTreeOutline, { ...treeProps }) : /* @__PURE__ */ jsx8(
772
+ return outline ? /* @__PURE__ */ jsx7(GenreTreeOutline, { ...treeProps }) : /* @__PURE__ */ jsx7(
767
773
  GenreTreeWheelRadialPopCore,
768
774
  {
769
775
  ...treeProps,
@@ -780,7 +786,7 @@ function GenrePlaylistTreeWheelRadialPopCore(props) {
780
786
 
781
787
  // src/genre-tree/GenreTreeWheelHandoff.tsx
782
788
  import { useEffect as useEffect2, useRef as useRef3, useState as useState2 } from "react";
783
- import { jsx as jsx9, jsxs as jsxs5 } from "react/jsx-runtime";
789
+ import { jsx as jsx8, jsxs as jsxs4 } from "react/jsx-runtime";
784
790
  function GenreTreeWheelHandoff({ skeleton, children, onRevealedChange }) {
785
791
  const [revealed, setRevealed] = useState2(false);
786
792
  const onRevealedChangeRef = useRef3(onRevealedChange);
@@ -799,8 +805,8 @@ function GenreTreeWheelHandoff({ skeleton, children, onRevealedChange }) {
799
805
  cancelAnimationFrame(innerFrame);
800
806
  };
801
807
  }, []);
802
- return /* @__PURE__ */ jsxs5("div", { className: "relative w-full h-full", children: [
803
- /* @__PURE__ */ jsx9(
808
+ return /* @__PURE__ */ jsxs4("div", { className: "relative w-full h-full", children: [
809
+ /* @__PURE__ */ jsx8(
804
810
  "div",
805
811
  {
806
812
  className: "absolute inset-0 transition-opacity duration-150 ease-out",
@@ -809,7 +815,7 @@ function GenreTreeWheelHandoff({ skeleton, children, onRevealedChange }) {
809
815
  children
810
816
  }
811
817
  ),
812
- !revealed && /* @__PURE__ */ jsx9("div", { className: "absolute inset-0", "aria-hidden": "true", children: skeleton })
818
+ !revealed && /* @__PURE__ */ jsx8("div", { className: "absolute inset-0", "aria-hidden": "true", children: skeleton })
813
819
  ] });
814
820
  }
815
821
 
@@ -817,7 +823,7 @@ function GenreTreeWheelHandoff({ skeleton, children, onRevealedChange }) {
817
823
  import { ArrowLeft, LoaderCircle, Search, X } from "lucide-react";
818
824
  import { Input } from "@behindthemusictree/ui";
819
825
  import { useGenreSearch } from "@behindthemusictree/app-kit/genre-tree";
820
- import { jsx as jsx10, jsxs as jsxs6 } from "react/jsx-runtime";
826
+ import { jsx as jsx9, jsxs as jsxs5 } from "react/jsx-runtime";
821
827
  function GenreSearch({
822
828
  genrePlaylists,
823
829
  onSelect,
@@ -839,19 +845,19 @@ function GenreSearch({
839
845
  const loading = query !== "" && query !== selectedName && (extraResults?.loading ?? false);
840
846
  const showList = results.length > 0 || extraItems.length > 0;
841
847
  const heading = "px-3 pt-2 pb-1 text-xs font-medium uppercase text-[var(--color-text-muted)]";
842
- 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: [
843
- onPrevious && /* @__PURE__ */ jsx10(
848
+ return /* @__PURE__ */ jsxs5("div", { className: "genre-search relative w-[calc(var(--layout-detail-panel-w,25.5rem)-2*var(--layout-panel-inset,1rem))] shrink-0 max-md:w-full", children: [
849
+ onPrevious && /* @__PURE__ */ jsx9(
844
850
  "button",
845
851
  {
846
852
  type: "button",
847
853
  "aria-label": "Previous",
848
854
  title: "Previous",
849
- 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)]",
855
+ className: "genre-search-previous absolute left-2 top-1/2 z-10 hidden items-center justify-center p-1.5 [@media(pointer:coarse)]:min-h-[var(--layout-touch-target)] [@media(pointer:coarse)]:min-w-[var(--layout-touch-target)] text-[var(--color-text-muted)] rounded-full -translate-y-1/2 hover:bg-[var(--color-surface-muted)] hover:text-[var(--color-text)]",
850
856
  onClick: onPrevious,
851
- children: /* @__PURE__ */ jsx10(ArrowLeft, { "aria-hidden": "true", className: "w-4 h-4" })
857
+ children: /* @__PURE__ */ jsx9(ArrowLeft, { "aria-hidden": "true", className: "w-4 h-4" })
852
858
  }
853
859
  ),
854
- /* @__PURE__ */ jsx10(
860
+ /* @__PURE__ */ jsx9(
855
861
  Input,
856
862
  {
857
863
  type: "text",
@@ -863,24 +869,27 @@ function GenreSearch({
863
869
  placeholder: placeholder ?? `${label}\u2026`,
864
870
  "aria-label": label,
865
871
  "aria-busy": loading,
872
+ inputMode: "search",
873
+ enterKeyHint: "search",
874
+ autoComplete: "off",
866
875
  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"
867
876
  }
868
877
  ),
869
- /* @__PURE__ */ jsxs6("div", { className: "absolute right-3 top-1/2 flex items-center -translate-y-1/2", children: [
870
- /* @__PURE__ */ jsxs6(
878
+ /* @__PURE__ */ jsxs5("div", { className: "absolute right-3 top-1/2 flex items-center -translate-y-1/2", children: [
879
+ /* @__PURE__ */ jsxs5(
871
880
  "span",
872
881
  {
873
882
  "data-loading": loading,
874
883
  className: "relative p-2 text-[var(--color-text-muted)] pointer-events-none",
875
884
  children: [
876
- /* @__PURE__ */ jsx10(
885
+ /* @__PURE__ */ jsx9(
877
886
  Search,
878
887
  {
879
888
  "aria-hidden": "true",
880
889
  className: `w-6 h-6 transition-opacity duration-200 motion-reduce:transition-none ${loading ? "opacity-0" : ""}`
881
890
  }
882
891
  ),
883
- /* @__PURE__ */ jsx10(
892
+ /* @__PURE__ */ jsx9(
884
893
  LoaderCircle,
885
894
  {
886
895
  "aria-hidden": "true",
@@ -890,44 +899,44 @@ function GenreSearch({
890
899
  ]
891
900
  }
892
901
  ),
893
- canClear && /* @__PURE__ */ jsx10(
902
+ canClear && /* @__PURE__ */ jsx9(
894
903
  "button",
895
904
  {
896
905
  type: "button",
897
906
  "aria-label": "Clear search",
898
- className: "p-2 text-[var(--color-text-muted)] rounded-full hover:bg-[var(--color-surface-muted)] hover:text-[var(--color-text)]",
907
+ className: "flex items-center justify-center p-2 [@media(pointer:coarse)]:min-h-[var(--layout-touch-target)] [@media(pointer:coarse)]:min-w-[var(--layout-touch-target)] text-[var(--color-text-muted)] rounded-full hover:bg-[var(--color-surface-muted)] hover:text-[var(--color-text)]",
899
908
  onClick: () => {
900
909
  clear();
901
910
  onQueryChange?.("");
902
911
  },
903
- children: /* @__PURE__ */ jsx10(X, { "aria-hidden": "true", className: "w-6 h-6" })
912
+ children: /* @__PURE__ */ jsx9(X, { "aria-hidden": "true", className: "w-6 h-6" })
904
913
  }
905
914
  )
906
915
  ] }),
907
- extraResults?.status && /* @__PURE__ */ jsx10("p", { role: "status", "aria-live": "polite", className: "sr-only", children: extraResults.status }),
908
- 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: [
909
- extraResults && results.length > 0 && /* @__PURE__ */ jsx10("li", { className: heading, children: "Genres" }),
910
- results.map((genrePlaylist) => /* @__PURE__ */ jsx10("li", { children: /* @__PURE__ */ jsxs6(
916
+ extraResults?.status && /* @__PURE__ */ jsx9("p", { role: "status", "aria-live": "polite", className: "sr-only", children: extraResults.status }),
917
+ showList && /* @__PURE__ */ jsxs5("ul", { className: "genre-search-results absolute z-40 w-full max-h-[min(16rem,50dvh)] mt-1 overflow-y-auto bg-[var(--color-surface)] border border-[var(--color-border)] rounded-xl shadow-md", children: [
918
+ extraResults && results.length > 0 && /* @__PURE__ */ jsx9("li", { className: heading, children: "Genres" }),
919
+ results.map((genrePlaylist) => /* @__PURE__ */ jsx9("li", { children: /* @__PURE__ */ jsxs5(
911
920
  "button",
912
921
  {
913
922
  type: "button",
914
- className: "w-full px-3 py-1.5 text-left hover:bg-[var(--color-surface-muted)]",
923
+ className: "w-full px-3 py-1.5 [@media(pointer:coarse)]:min-h-[var(--layout-touch-target)] text-left hover:bg-[var(--color-surface-muted)]",
915
924
  onClick: () => {
916
925
  select(genrePlaylist);
917
926
  onQueryChange?.("");
918
927
  },
919
928
  children: [
920
929
  genrePlaylist.name,
921
- genrePlaylist.isUmbrella && /* @__PURE__ */ jsx10("span", { className: "ml-2 text-xs italic text-[var(--color-text-muted)]", children: "umbrella" })
930
+ genrePlaylist.isUmbrella && /* @__PURE__ */ jsx9("span", { className: "ml-2 text-xs italic text-[var(--color-text-muted)]", children: "umbrella" })
922
931
  ]
923
932
  }
924
933
  ) }, genrePlaylist.uuid)),
925
- extraResults && extraItems.length > 0 && /* @__PURE__ */ jsx10("li", { className: heading, children: extraResults.label }),
926
- extraItems.map((item) => /* @__PURE__ */ jsx10("li", { children: /* @__PURE__ */ jsx10(
934
+ extraResults && extraItems.length > 0 && /* @__PURE__ */ jsx9("li", { className: heading, children: extraResults.label }),
935
+ extraItems.map((item) => /* @__PURE__ */ jsx9("li", { children: /* @__PURE__ */ jsx9(
927
936
  "button",
928
937
  {
929
938
  type: "button",
930
- className: "w-full px-3 py-1.5 text-left hover:bg-[var(--color-surface-muted)]",
939
+ className: "w-full px-3 py-1.5 [@media(pointer:coarse)]:min-h-[var(--layout-touch-target)] text-left hover:bg-[var(--color-surface-muted)]",
931
940
  onClick: () => {
932
941
  setQuery("");
933
942
  onQueryChange?.("");
@@ -941,8 +950,227 @@ function GenreSearch({
941
950
  }
942
951
 
943
952
  // src/genre-tree/GenreActionBand.tsx
944
- import { MoveUpRight, Pause, Pencil, Play, Plus, ZoomIn } from "lucide-react";
953
+ import { ArrowLeftRight, MoveUpRight, Pause, Pencil, Play, Plus, ZoomIn } from "lucide-react";
954
+
955
+ // src/genre-tree/ShareActionButton.tsx
956
+ import { Share2 as Share22 } from "lucide-react";
957
+ import { usePopup as usePopup4 } from "@behindthemusictree/app-kit/popup";
958
+
959
+ // src/genre-tree/SharePopup.tsx
960
+ import { useCallback as useCallback5, useEffect as useEffect3, useMemo as useMemo5, useState as useState3 } from "react";
961
+ import { Link2, Share2 } from "lucide-react";
962
+ import { FaWhatsapp } from "react-icons/fa";
963
+ import { SiGmail } from "react-icons/si";
964
+
965
+ // src/genre-tree/ShareDestinationButton.tsx
966
+ import { Fragment as Fragment4, jsx as jsx10, jsxs as jsxs6 } from "react/jsx-runtime";
967
+ var className = "group flex min-w-0 flex-1 flex-col items-center justify-center gap-1.5 rounded-xl px-2 py-2 text-xs text-[var(--color-text)] transition-colors hover:bg-[var(--color-surface-muted)] focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[var(--color-focus-ring)] disabled:cursor-not-allowed disabled:opacity-50";
968
+ function Contents({ icon, label }) {
969
+ return /* @__PURE__ */ jsxs6(Fragment4, { children: [
970
+ /* @__PURE__ */ jsx10("span", { className: "flex h-12 w-12 items-center justify-center rounded-full bg-[var(--color-surface-muted)] transition-colors group-hover:bg-[var(--color-border)]", children: icon }),
971
+ /* @__PURE__ */ jsx10("span", { className: "max-w-full truncate", children: label })
972
+ ] });
973
+ }
974
+ function ShareDestinationButton({
975
+ label,
976
+ icon,
977
+ href,
978
+ onClick,
979
+ disabled = false,
980
+ ariaLabel
981
+ }) {
982
+ if (href) {
983
+ return /* @__PURE__ */ jsx10("a", { href, target: "_blank", rel: "noopener noreferrer", "aria-label": ariaLabel, className, children: /* @__PURE__ */ jsx10(Contents, { icon, label }) });
984
+ }
985
+ return /* @__PURE__ */ jsx10("button", { type: "button", onClick, disabled, "aria-label": ariaLabel, className, children: /* @__PURE__ */ jsx10(Contents, { icon, label }) });
986
+ }
987
+
988
+ // src/genre-tree/SharePreviewCard.tsx
989
+ import { Music2, UserRound } from "lucide-react";
945
990
  import { jsx as jsx11, jsxs as jsxs7 } from "react/jsx-runtime";
991
+ function SharePreviewCard({ target, url }) {
992
+ const Icon = target.kind === "genre" ? Music2 : UserRound;
993
+ const typeLabel = target.kind === "genre" ? "Genre" : "Artist";
994
+ return /* @__PURE__ */ jsx11("div", { className: "rounded-3xl bg-[var(--color-surface-muted)] p-4", children: /* @__PURE__ */ jsxs7("div", { className: "flex min-h-32 overflow-hidden rounded-2xl bg-[var(--color-surface)] shadow-lg", children: [
995
+ /* @__PURE__ */ jsx11("div", { className: "flex w-28 shrink-0 items-center justify-center bg-[var(--color-primary)]/15 text-[var(--color-primary)]", children: /* @__PURE__ */ jsx11(Icon, { "aria-hidden": "true", className: "h-10 w-10" }) }),
996
+ /* @__PURE__ */ jsxs7("div", { className: "flex min-w-0 flex-1 flex-col justify-center gap-1 p-4", children: [
997
+ /* @__PURE__ */ jsx11("span", { className: "text-xs font-medium uppercase tracking-wide text-[var(--color-text-muted)]", children: typeLabel }),
998
+ /* @__PURE__ */ jsx11("span", { className: "truncate text-base font-semibold text-[var(--color-text)]", title: target.name, children: target.name }),
999
+ /* @__PURE__ */ jsx11("span", { className: "truncate text-sm text-[var(--color-primary)]", title: url, children: url })
1000
+ ] })
1001
+ ] }) });
1002
+ }
1003
+
1004
+ // src/genre-tree/SharePopup.tsx
1005
+ import { Fragment as Fragment5, jsx as jsx12, jsxs as jsxs8 } from "react/jsx-runtime";
1006
+ function resolveAbsoluteUrl(url) {
1007
+ try {
1008
+ const parsed = new URL(url, window.location.href);
1009
+ return parsed.protocol === "http:" || parsed.protocol === "https:" ? parsed.href : void 0;
1010
+ } catch {
1011
+ return void 0;
1012
+ }
1013
+ }
1014
+ async function copyText(text) {
1015
+ try {
1016
+ if (navigator.clipboard?.writeText) {
1017
+ await navigator.clipboard.writeText(text);
1018
+ return true;
1019
+ }
1020
+ } catch {
1021
+ }
1022
+ const input = document.createElement("textarea");
1023
+ input.value = text;
1024
+ input.setAttribute("readonly", "");
1025
+ input.style.position = "fixed";
1026
+ input.style.opacity = "0";
1027
+ document.body.append(input);
1028
+ input.select();
1029
+ try {
1030
+ return document.execCommand("copy");
1031
+ } catch {
1032
+ return false;
1033
+ } finally {
1034
+ input.remove();
1035
+ }
1036
+ }
1037
+ function SharePopup({ target, url, onClose }) {
1038
+ const [absoluteUrl, setAbsoluteUrl] = useState3(url);
1039
+ const [copyStatus, setCopyStatus] = useState3("idle");
1040
+ const [canUseNativeShare, setCanUseNativeShare] = useState3(false);
1041
+ const [shareError, setShareError] = useState3(null);
1042
+ const [toastId, setToastId] = useState3(0);
1043
+ useEffect3(() => {
1044
+ setAbsoluteUrl(resolveAbsoluteUrl(url) ?? "");
1045
+ setCopyStatus("idle");
1046
+ setShareError(null);
1047
+ }, [url]);
1048
+ useEffect3(() => {
1049
+ setCanUseNativeShare(typeof navigator !== "undefined" && typeof navigator.share === "function");
1050
+ }, []);
1051
+ const destinationUrls = useMemo5(() => {
1052
+ if (!absoluteUrl) return null;
1053
+ const message = `${target.name} \u2014 ${absoluteUrl}`;
1054
+ return {
1055
+ whatsapp: `https://wa.me/?text=${encodeURIComponent(message)}`,
1056
+ gmail: `https://mail.google.com/mail/?view=cm&fs=1&su=${encodeURIComponent(`Share: ${target.name}`)}&body=${encodeURIComponent(message)}`
1057
+ };
1058
+ }, [absoluteUrl, target.name]);
1059
+ const handleCopy = async () => {
1060
+ if (!absoluteUrl) {
1061
+ setCopyStatus("failed");
1062
+ return;
1063
+ }
1064
+ setShareError(null);
1065
+ const copied = await copyText(absoluteUrl);
1066
+ setCopyStatus(copied ? "idle" : "failed");
1067
+ if (copied) setToastId((id) => id + 1);
1068
+ };
1069
+ const dismissToast = useCallback5(() => setToastId(0), []);
1070
+ const handleNativeShare = async () => {
1071
+ if (!absoluteUrl || !navigator.share) return;
1072
+ setShareError(null);
1073
+ try {
1074
+ await navigator.share({ title: target.name, url: absoluteUrl });
1075
+ } catch (error) {
1076
+ if (error instanceof Error && error.name === "AbortError") return;
1077
+ setShareError("Could not open the share options.");
1078
+ }
1079
+ };
1080
+ return /* @__PURE__ */ jsxs8(Fragment5, { children: [
1081
+ /* @__PURE__ */ jsx12(
1082
+ BasePopup_default,
1083
+ {
1084
+ title: "Share",
1085
+ onClose,
1086
+ isDismissable: true,
1087
+ trapFocus: true,
1088
+ closeOnEscape: true,
1089
+ closeOnBackdrop: true,
1090
+ className: "!w-full !max-w-[28rem] overflow-hidden !rounded-[2rem] !bg-[var(--color-surface)] shadow-2xl",
1091
+ overlayClassName: "!bg-black/50 p-4 backdrop-blur-[2px]",
1092
+ titleClassName: "!rounded-none !bg-transparent !px-6 !pb-2 !pt-6",
1093
+ titleTextClassName: "!text-xl !font-semibold !text-[var(--color-text)]",
1094
+ closeButtonClassName: "!h-10 !w-10 !rounded-none !border-0 !bg-transparent !p-0 !text-[var(--color-text)] hover:!bg-transparent hover:!text-[var(--color-text)]",
1095
+ contentClassName: "!rounded-none !bg-transparent !px-6 !pb-6 !pt-2",
1096
+ children: /* @__PURE__ */ jsxs8("div", { className: "flex flex-col gap-4", children: [
1097
+ /* @__PURE__ */ jsx12(SharePreviewCard, { target, url: absoluteUrl || url }),
1098
+ /* @__PURE__ */ jsxs8("div", { className: "grid grid-cols-3 gap-1 rounded-3xl bg-[var(--color-surface)] p-3 sm:grid-cols-4", children: [
1099
+ /* @__PURE__ */ jsx12(
1100
+ ShareDestinationButton,
1101
+ {
1102
+ label: "Copy Link",
1103
+ icon: /* @__PURE__ */ jsx12(Link2, { "aria-hidden": "true", className: "h-6 w-6" }),
1104
+ onClick: () => void handleCopy(),
1105
+ ariaLabel: "Copy link",
1106
+ disabled: !absoluteUrl
1107
+ }
1108
+ ),
1109
+ /* @__PURE__ */ jsx12(
1110
+ ShareDestinationButton,
1111
+ {
1112
+ label: "WhatsApp",
1113
+ icon: /* @__PURE__ */ jsx12(FaWhatsapp, { "aria-hidden": "true", className: "h-9 w-9 text-[#25D366]" }),
1114
+ href: destinationUrls?.whatsapp,
1115
+ ariaLabel: `Share ${target.name} via WhatsApp`,
1116
+ disabled: !absoluteUrl
1117
+ }
1118
+ ),
1119
+ /* @__PURE__ */ jsx12(
1120
+ ShareDestinationButton,
1121
+ {
1122
+ label: "Gmail",
1123
+ icon: /* @__PURE__ */ jsx12(SiGmail, { "aria-hidden": "true", className: "h-8 w-8 text-[#EA4335]" }),
1124
+ href: destinationUrls?.gmail,
1125
+ ariaLabel: `Email ${target.name} with Gmail`,
1126
+ disabled: !absoluteUrl
1127
+ }
1128
+ ),
1129
+ canUseNativeShare && /* @__PURE__ */ jsx12(
1130
+ ShareDestinationButton,
1131
+ {
1132
+ label: "More",
1133
+ icon: /* @__PURE__ */ jsx12(Share2, { "aria-hidden": "true", className: "h-6 w-6" }),
1134
+ onClick: () => void handleNativeShare(),
1135
+ ariaLabel: "More sharing options",
1136
+ disabled: !absoluteUrl
1137
+ }
1138
+ )
1139
+ ] }),
1140
+ copyStatus === "failed" && /* @__PURE__ */ jsx12("p", { role: "alert", className: "text-center text-sm text-[var(--color-red-500)]", children: "Could not copy the link." }),
1141
+ shareError && /* @__PURE__ */ jsx12("p", { role: "alert", className: "text-center text-sm text-[var(--color-red-500)]", children: shareError })
1142
+ ] })
1143
+ }
1144
+ ),
1145
+ toastId > 0 && /* @__PURE__ */ jsx12(Toast, { message: "Link copied", onClose: dismissToast }, toastId)
1146
+ ] });
1147
+ }
1148
+
1149
+ // src/genre-tree/ShareActionButton.tsx
1150
+ import { jsx as jsx13, jsxs as jsxs9 } from "react/jsx-runtime";
1151
+ function ShareActionButton({ target, url }) {
1152
+ const { showPopup, hidePopup } = usePopup4();
1153
+ const openSharePopup = () => {
1154
+ showPopup(/* @__PURE__ */ jsx13(SharePopup, { target, url, onClose: hidePopup }));
1155
+ };
1156
+ return /* @__PURE__ */ jsxs9(
1157
+ "button",
1158
+ {
1159
+ type: "button",
1160
+ onClick: openSharePopup,
1161
+ className: "group flex flex-col items-center gap-1 w-16 text-xs text-center text-[var(--color-text)] focus-visible:outline-none",
1162
+ "aria-label": `Share ${target.name}`,
1163
+ title: `Share ${target.name}`,
1164
+ children: [
1165
+ /* @__PURE__ */ jsx13("span", { className: "flex items-center justify-center w-10 h-10 rounded-full group-focus-visible:ring-2 group-focus-visible:ring-[var(--color-focus-ring)] bg-[var(--color-surface-muted)] transition-colors group-hover:bg-[var(--color-border)]", children: /* @__PURE__ */ jsx13(Share22, { "aria-hidden": "true", className: "w-5 h-5" }) }),
1166
+ "Share"
1167
+ ]
1168
+ }
1169
+ );
1170
+ }
1171
+
1172
+ // src/genre-tree/GenreActionBand.tsx
1173
+ import { jsx as jsx14, jsxs as jsxs10 } from "react/jsx-runtime";
946
1174
  function GenreActionBand({
947
1175
  node,
948
1176
  onPlayPause,
@@ -951,7 +1179,12 @@ function GenreActionBand({
951
1179
  onAddChild,
952
1180
  onRename,
953
1181
  onChangeParent,
954
- additionalActions
1182
+ side,
1183
+ onToggleSide,
1184
+ toggleSideDisabledReason,
1185
+ additionalActions,
1186
+ shareTarget,
1187
+ shareUrl
955
1188
  }) {
956
1189
  const items = [];
957
1190
  const adminItems = [];
@@ -959,7 +1192,7 @@ function GenreActionBand({
959
1192
  items.push({
960
1193
  key: "play-pause",
961
1194
  label: isPlaying ? "Pause" : "Play",
962
- icon: isPlaying ? /* @__PURE__ */ jsx11(Pause, { "aria-hidden": "true", className: "w-5 h-5" }) : /* @__PURE__ */ jsx11(Play, { "aria-hidden": "true", className: "w-5 h-5" }),
1195
+ icon: isPlaying ? /* @__PURE__ */ jsx14(Pause, { "aria-hidden": "true", className: "w-5 h-5" }) : /* @__PURE__ */ jsx14(Play, { "aria-hidden": "true", className: "w-5 h-5" }),
963
1196
  onClick: onPlayPause
964
1197
  });
965
1198
  }
@@ -967,18 +1200,28 @@ function GenreActionBand({
967
1200
  items.push({
968
1201
  key: "zoom-in",
969
1202
  label: "Focus",
970
- icon: /* @__PURE__ */ jsx11(ZoomIn, { "aria-hidden": "true", className: "w-5 h-5" }),
1203
+ icon: /* @__PURE__ */ jsx14(ZoomIn, { "aria-hidden": "true", className: "w-5 h-5" }),
971
1204
  onClick: onZoomIn
972
1205
  });
973
1206
  }
974
1207
  if (onAddChild) {
975
- adminItems.push({ key: "add-child", label: "Add", icon: /* @__PURE__ */ jsx11(Plus, { "aria-hidden": "true", className: "w-5 h-5" }), onClick: onAddChild });
1208
+ adminItems.push({ key: "add-child", label: "Add", icon: /* @__PURE__ */ jsx14(Plus, { "aria-hidden": "true", className: "w-5 h-5" }), onClick: onAddChild });
976
1209
  }
977
1210
  if (onRename) {
978
- adminItems.push({ key: "rename", label: "Rename", icon: /* @__PURE__ */ jsx11(Pencil, { "aria-hidden": "true", className: "w-5 h-5" }), onClick: onRename });
1211
+ adminItems.push({ key: "rename", label: "Rename", icon: /* @__PURE__ */ jsx14(Pencil, { "aria-hidden": "true", className: "w-5 h-5" }), onClick: onRename });
979
1212
  }
980
1213
  if (onChangeParent) {
981
- adminItems.push({ key: "change-parent", label: "Parent", icon: /* @__PURE__ */ jsx11(MoveUpRight, { "aria-hidden": "true", className: "w-5 h-5" }), onClick: onChangeParent });
1214
+ adminItems.push({ key: "change-parent", label: "Parent", icon: /* @__PURE__ */ jsx14(MoveUpRight, { "aria-hidden": "true", className: "w-5 h-5" }), onClick: onChangeParent });
1215
+ }
1216
+ if (onToggleSide) {
1217
+ adminItems.push({
1218
+ key: "toggle-side",
1219
+ label: side === "pop" ? "Make core" : "Make pop",
1220
+ icon: /* @__PURE__ */ jsx14(ArrowLeftRight, { "aria-hidden": "true", className: "w-5 h-5" }),
1221
+ onClick: onToggleSide,
1222
+ disabled: !!toggleSideDisabledReason,
1223
+ title: toggleSideDisabledReason
1224
+ });
982
1225
  }
983
1226
  for (const action of additionalActions?.(node) ?? []) {
984
1227
  items.push({
@@ -989,31 +1232,35 @@ function GenreActionBand({
989
1232
  disabled: action.enabled ? !action.enabled(node) : false
990
1233
  });
991
1234
  }
992
- if (items.length === 0 && adminItems.length === 0) return null;
993
- const renderItems = (list) => list.map((item) => /* @__PURE__ */ jsxs7(
1235
+ if (items.length === 0 && (!shareTarget || !shareUrl) && adminItems.length === 0) return null;
1236
+ const renderItems = (list) => list.map((item) => /* @__PURE__ */ jsxs10(
994
1237
  "button",
995
1238
  {
996
1239
  type: "button",
997
1240
  disabled: item.disabled,
1241
+ title: item.title,
998
1242
  onClick: item.onClick,
999
1243
  className: "group flex flex-col items-center gap-1 w-16 text-xs text-center text-[var(--color-text)] focus-visible:outline-none disabled:opacity-50",
1000
1244
  children: [
1001
- /* @__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 }),
1245
+ /* @__PURE__ */ jsx14("span", { className: "flex items-center justify-center w-10 h-10 rounded-full group-focus-visible:ring-2 group-focus-visible:ring-[var(--color-focus-ring)] bg-[var(--color-surface-muted)] transition-colors group-hover:bg-[var(--color-border)] group-disabled:group-hover:bg-[var(--color-surface-muted)]", children: item.icon }),
1002
1246
  item.label
1003
1247
  ]
1004
1248
  },
1005
1249
  item.key
1006
1250
  ));
1007
- return /* @__PURE__ */ jsxs7("div", { className: "genre-action-band border-b border-[var(--color-border)]", children: [
1008
- items.length > 0 && /* @__PURE__ */ jsx11("div", { className: "flex flex-wrap gap-1 px-3.5 py-3", children: renderItems(items) }),
1009
- adminItems.length > 0 && /* @__PURE__ */ jsxs7(
1251
+ return /* @__PURE__ */ jsxs10("div", { className: "genre-action-band border-b border-[var(--color-border)]", children: [
1252
+ (items.length > 0 || shareTarget && shareUrl) && /* @__PURE__ */ jsxs10("div", { className: "flex flex-wrap gap-1 px-3.5 py-3", children: [
1253
+ renderItems(items),
1254
+ shareTarget && shareUrl && /* @__PURE__ */ jsx14(ShareActionButton, { target: shareTarget, url: shareUrl })
1255
+ ] }),
1256
+ adminItems.length > 0 && /* @__PURE__ */ jsxs10(
1010
1257
  "section",
1011
1258
  {
1012
1259
  "aria-label": "Admin actions",
1013
1260
  className: `px-3.5 py-3 ${items.length > 0 ? "border-t border-[var(--color-border)]" : ""}`,
1014
1261
  children: [
1015
- /* @__PURE__ */ jsx11("h3", { className: "mb-2 text-xs font-semibold uppercase tracking-wide text-[var(--color-text-muted)]", children: "Admin" }),
1016
- /* @__PURE__ */ jsx11("div", { className: "flex flex-wrap gap-1", children: renderItems(adminItems) })
1262
+ /* @__PURE__ */ jsx14("h3", { className: "mb-2 text-xs font-semibold uppercase tracking-wide text-[var(--color-text-muted)]", children: "Admin" }),
1263
+ /* @__PURE__ */ jsx14("div", { className: "flex flex-wrap gap-1", children: renderItems(adminItems) })
1017
1264
  ]
1018
1265
  }
1019
1266
  )
@@ -1023,14 +1270,14 @@ function GenreActionBand({
1023
1270
  // src/genre-tree/PanelEdgeTab.tsx
1024
1271
  import { ChevronLeft, ChevronRight } from "lucide-react";
1025
1272
  import { twMerge } from "tailwind-merge";
1026
- import { jsx as jsx12, jsxs as jsxs8 } from "react/jsx-runtime";
1273
+ import { jsx as jsx15, jsxs as jsxs11 } from "react/jsx-runtime";
1027
1274
  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)]";
1028
- function PanelEdgeTab({ side, edge, center, controls, className }) {
1275
+ function PanelEdgeTab({ side, edge, center, controls, className: className2 }) {
1029
1276
  if (!edge && !center) return null;
1030
1277
  const isLeft = side === "left";
1031
1278
  const EdgeIcon = isLeft ? ChevronLeft : ChevronRight;
1032
1279
  const CenterIcon = isLeft ? ChevronRight : ChevronLeft;
1033
- const renderButton = (action, icon) => /* @__PURE__ */ jsx12(
1280
+ const renderButton = (action, icon) => /* @__PURE__ */ jsx15(
1034
1281
  "button",
1035
1282
  {
1036
1283
  type: "button",
@@ -1043,20 +1290,20 @@ function PanelEdgeTab({ side, edge, center, controls, className }) {
1043
1290
  children: icon
1044
1291
  }
1045
1292
  );
1046
- const edgeButton = edge && renderButton(edge, /* @__PURE__ */ jsx12(EdgeIcon, { "aria-hidden": "true", className: "h-5 w-5" }));
1047
- const centerButton = center && renderButton(center, /* @__PURE__ */ jsx12(CenterIcon, { "aria-hidden": "true", className: "h-5 w-5" }));
1293
+ const edgeButton = edge && renderButton(edge, /* @__PURE__ */ jsx15(EdgeIcon, { "aria-hidden": "true", className: "h-5 w-5" }));
1294
+ const centerButton = center && renderButton(center, /* @__PURE__ */ jsx15(CenterIcon, { "aria-hidden": "true", className: "h-5 w-5" }));
1048
1295
  const buttons = isLeft ? [edgeButton, centerButton] : [centerButton, edgeButton];
1049
- return /* @__PURE__ */ jsxs8(
1296
+ return /* @__PURE__ */ jsxs11(
1050
1297
  "div",
1051
1298
  {
1052
1299
  className: twMerge(
1053
1300
  "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)]",
1054
1301
  isLeft ? "rounded-r-lg" : "rounded-l-lg [clip-path:inset(-0.5rem_0_-0.5rem_-0.5rem)]",
1055
- className
1302
+ className2
1056
1303
  ),
1057
1304
  children: [
1058
1305
  buttons[0],
1059
- buttons[0] && buttons[1] && /* @__PURE__ */ jsx12("div", { role: "presentation", className: "h-px shrink-0 bg-[var(--color-border)]" }),
1306
+ buttons[0] && buttons[1] && /* @__PURE__ */ jsx15("div", { role: "presentation", className: "h-px shrink-0 bg-[var(--color-border)]" }),
1060
1307
  buttons[1]
1061
1308
  ]
1062
1309
  }
@@ -1064,12 +1311,12 @@ function PanelEdgeTab({ side, edge, center, controls, className }) {
1064
1311
  }
1065
1312
 
1066
1313
  // src/genre-tree/ChangeParentPopup.tsx
1067
- import { useEffect as useEffect3, useMemo as useMemo5 } from "react";
1314
+ import { useEffect as useEffect4, useMemo as useMemo6 } from "react";
1068
1315
  import {
1069
1316
  getReparentCandidates,
1070
1317
  useUpdateGenre
1071
1318
  } from "@behindthemusictree/app-kit/genre-tree";
1072
- import { jsx as jsx13, jsxs as jsxs9 } from "react/jsx-runtime";
1319
+ import { jsx as jsx16, jsxs as jsxs12 } from "react/jsx-runtime";
1073
1320
  function ChangeParentPopup({
1074
1321
  genrePlaylist,
1075
1322
  genrePlaylists,
@@ -1079,7 +1326,7 @@ function ChangeParentPopup({
1079
1326
  onParentChanged
1080
1327
  }) {
1081
1328
  const { mutate, isPending, formErrors } = useUpdateGenre(scope, getBackendBaseUrl);
1082
- const candidates = useMemo5(
1329
+ const candidates = useMemo6(
1083
1330
  () => getReparentCandidates(genrePlaylists, genrePlaylist.uuid),
1084
1331
  [genrePlaylists, genrePlaylist.uuid]
1085
1332
  );
@@ -1097,19 +1344,19 @@ function ChangeParentPopup({
1097
1344
  }
1098
1345
  );
1099
1346
  };
1100
- useEffect3(() => {
1347
+ useEffect4(() => {
1101
1348
  const handleKeyDown = (event) => {
1102
1349
  if (event.key === "Escape") onClose();
1103
1350
  };
1104
1351
  window.addEventListener("keydown", handleKeyDown);
1105
1352
  return () => window.removeEventListener("keydown", handleKeyDown);
1106
1353
  }, [onClose]);
1107
- return /* @__PURE__ */ jsxs9(BasePopup_default, { title: `Change parent of ${genrePlaylist.name}`, onClose, showCancelButton: true, fitContent: true, children: [
1108
- /* @__PURE__ */ jsxs9("p", { className: "mb-3 text-sm text-[var(--color-text-muted)]", children: [
1354
+ return /* @__PURE__ */ jsxs12(BasePopup_default, { title: `Change parent of ${genrePlaylist.name}`, onClose, showCancelButton: true, fitContent: true, children: [
1355
+ /* @__PURE__ */ jsxs12("p", { className: "mb-3 text-sm text-[var(--color-text-muted)]", children: [
1109
1356
  "Current parent: ",
1110
1357
  genrePlaylist.parent?.name ?? "none"
1111
1358
  ] }),
1112
- /* @__PURE__ */ jsx13("div", { "aria-busy": isPending, className: isPending ? "pointer-events-none opacity-60" : void 0, children: /* @__PURE__ */ jsx13(
1359
+ /* @__PURE__ */ jsx16("div", { "aria-busy": isPending, className: isPending ? "pointer-events-none opacity-60" : void 0, children: /* @__PURE__ */ jsx16(
1113
1360
  GenreSearch,
1114
1361
  {
1115
1362
  genrePlaylists: candidates,
@@ -1117,7 +1364,7 @@ function ChangeParentPopup({
1117
1364
  placeholder: "Search a new parent\u2026"
1118
1365
  }
1119
1366
  ) }),
1120
- formErrors.length > 0 && /* @__PURE__ */ jsx13("p", { role: "alert", className: "mt-3 text-sm text-[var(--color-red-500)]", children: formErrors.map(({ message }) => message).join(" ") })
1367
+ formErrors.length > 0 && /* @__PURE__ */ jsx16("p", { role: "alert", className: "mt-3 text-sm text-[var(--color-red-500)]", children: formErrors.map(({ message }) => message).join(" ") })
1121
1368
  ] });
1122
1369
  }
1123
1370
 
@@ -1129,29 +1376,29 @@ import { useArtist } from "@behindthemusictree/app-kit/genre-tree";
1129
1376
  import { FieldList } from "@behindthemusictree/ui";
1130
1377
 
1131
1378
  // src/genre-tree/artist/ArtistLinks.tsx
1132
- import { Globe, Share2 } from "lucide-react";
1379
+ import { Globe, Share2 as Share23 } from "lucide-react";
1133
1380
  import {
1134
1381
  SiBandcamp,
1135
1382
  SiDiscogs,
1136
1383
  SiMusicbrainz,
1137
1384
  SiSoundcloud,
1138
1385
  SiSpotify,
1139
- SiWikidata,
1140
- SiWikipedia,
1386
+ SiWikidata as SiWikidata2,
1387
+ SiWikipedia as SiWikipedia2,
1141
1388
  SiYoutube
1142
1389
  } from "react-icons/si";
1143
1390
  import { Chip } from "@behindthemusictree/ui";
1144
- import { jsx as jsx14 } from "react/jsx-runtime";
1391
+ import { jsx as jsx17 } from "react/jsx-runtime";
1145
1392
  var LINK_KINDS = {
1146
1393
  official: { label: "Official website", Icon: Globe },
1147
- wikipedia: { label: "Wikipedia", Icon: SiWikipedia },
1148
- wikidata: { label: "Wikidata", Icon: SiWikidata },
1394
+ wikipedia: { label: "Wikipedia", Icon: SiWikipedia2 },
1395
+ wikidata: { label: "Wikidata", Icon: SiWikidata2 },
1149
1396
  spotify: { label: "Spotify", Icon: SiSpotify },
1150
1397
  youtube: { label: "YouTube", Icon: SiYoutube },
1151
1398
  bandcamp: { label: "Bandcamp", Icon: SiBandcamp },
1152
1399
  soundcloud: { label: "SoundCloud", Icon: SiSoundcloud },
1153
1400
  discogs: { label: "Discogs", Icon: SiDiscogs },
1154
- social: { label: "Social", Icon: Share2 }
1401
+ social: { label: "Social", Icon: Share23 }
1155
1402
  };
1156
1403
  function socialLabel(url) {
1157
1404
  try {
@@ -1178,7 +1425,7 @@ function artistLinks(artist) {
1178
1425
  return links;
1179
1426
  }
1180
1427
  function ArtistLinks({ links }) {
1181
- 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)) });
1428
+ return /* @__PURE__ */ jsx17("ul", { className: "flex flex-wrap gap-1.5", children: links.map(({ key, url, Icon, label }) => /* @__PURE__ */ jsx17("li", { children: /* @__PURE__ */ jsx17(Chip, { href: url, icon: /* @__PURE__ */ jsx17(Icon, { className: "h-3.5 w-3.5 shrink-0", "aria-hidden": "true" }), children: label }) }, key)) });
1182
1429
  }
1183
1430
 
1184
1431
  // src/genre-tree/artist/formatYears.ts
@@ -1189,7 +1436,7 @@ function formatYears(beginYear, endYear, ended) {
1189
1436
  }
1190
1437
 
1191
1438
  // src/genre-tree/artist/ArtistHeader.tsx
1192
- import { jsx as jsx15 } from "react/jsx-runtime";
1439
+ import { jsx as jsx18 } from "react/jsx-runtime";
1193
1440
  var countryNames = new Intl.DisplayNames(["en"], { type: "region" });
1194
1441
  function ArtistHeader({ artist }) {
1195
1442
  const fields = [];
@@ -1199,35 +1446,35 @@ function ArtistHeader({ artist }) {
1199
1446
  const years = formatYears(artist.beginYear, artist.endYear, artist.ended);
1200
1447
  if (years) fields.push({ label: "Active", value: years });
1201
1448
  const links = artistLinks(artist);
1202
- if (links.length > 0) fields.push({ label: "Links", value: /* @__PURE__ */ jsx15(ArtistLinks, { links }) });
1449
+ if (links.length > 0) fields.push({ label: "Links", value: /* @__PURE__ */ jsx18(ArtistLinks, { links }) });
1203
1450
  if (fields.length === 0) return null;
1204
- return /* @__PURE__ */ jsx15(FieldList, { items: fields });
1451
+ return /* @__PURE__ */ jsx18(FieldList, { items: fields });
1205
1452
  }
1206
1453
 
1207
1454
  // src/genre-tree/artist/ArtistPhoto.tsx
1208
- import { jsx as jsx16, jsxs as jsxs10 } from "react/jsx-runtime";
1455
+ import { jsx as jsx19, jsxs as jsxs13 } from "react/jsx-runtime";
1209
1456
  function ExternalLink({ href, children }) {
1210
- return /* @__PURE__ */ jsx16("a", { href, target: "_blank", rel: "noopener noreferrer", className: "underline-offset-2 hover:underline", children });
1457
+ return /* @__PURE__ */ jsx19("a", { href, target: "_blank", rel: "noopener noreferrer", className: "underline-offset-2 hover:underline", children });
1211
1458
  }
1212
1459
  function ArtistPhoto({ image, name }) {
1213
- return /* @__PURE__ */ jsxs10("figure", { className: "flex flex-col gap-1.5 px-6 py-2.5", children: [
1214
- /* @__PURE__ */ jsx16("img", { src: image.url, alt: name, className: "h-64 w-full object-contain bg-[var(--color-surface-muted)] rounded" }),
1215
- /* @__PURE__ */ jsxs10("figcaption", { className: "text-xs text-[var(--color-text-muted)]", children: [
1460
+ return /* @__PURE__ */ jsxs13("figure", { className: "flex flex-col gap-1.5 px-6 py-2.5", children: [
1461
+ /* @__PURE__ */ jsx19("img", { src: image.url, alt: name, className: "h-64 w-full object-contain bg-[var(--color-surface-muted)] rounded" }),
1462
+ /* @__PURE__ */ jsxs13("figcaption", { className: "text-xs text-[var(--color-text-muted)]", children: [
1216
1463
  "Photo: ",
1217
- /* @__PURE__ */ jsx16(ExternalLink, { href: image.pageUrl, children: image.author ?? "Wikimedia Commons" }),
1464
+ /* @__PURE__ */ jsx19(ExternalLink, { href: image.pageUrl, children: image.author ?? "Wikimedia Commons" }),
1218
1465
  ",",
1219
1466
  " ",
1220
- image.licenseUrl ? /* @__PURE__ */ jsx16(ExternalLink, { href: image.licenseUrl, children: image.license }) : image.license
1467
+ image.licenseUrl ? /* @__PURE__ */ jsx19(ExternalLink, { href: image.licenseUrl, children: image.license }) : image.license
1221
1468
  ] })
1222
1469
  ] });
1223
1470
  }
1224
1471
 
1225
1472
  // src/genre-tree/artist/ArtistMembers.tsx
1226
1473
  import { PanelGroup, PanelSection } from "@behindthemusictree/ui";
1227
- import { jsx as jsx17, jsxs as jsxs11 } from "react/jsx-runtime";
1474
+ import { jsx as jsx20, jsxs as jsxs14 } from "react/jsx-runtime";
1228
1475
  function MembershipList({ title, memberships, renderArtistLink }) {
1229
1476
  if (memberships.length === 0) return null;
1230
- return /* @__PURE__ */ jsx17(PanelGroup, { title, children: /* @__PURE__ */ jsx17("ul", { children: memberships.map((membership, index) => {
1477
+ return /* @__PURE__ */ jsx20(PanelGroup, { title, children: /* @__PURE__ */ jsx20("ul", { children: memberships.map((membership, index) => {
1231
1478
  const details = [
1232
1479
  formatYears(membership.beginYear, membership.endYear, membership.ended),
1233
1480
  ...membership.roles
@@ -1235,9 +1482,9 @@ function MembershipList({ title, memberships, renderArtistLink }) {
1235
1482
  const detailsText = details.filter(Boolean).join(" \xB7 ");
1236
1483
  return (
1237
1484
  // A member can have several periods in the same group.
1238
- /* @__PURE__ */ jsxs11("li", { className: "truncate py-0.5", title: detailsText || void 0, children: [
1239
- /* @__PURE__ */ jsx17("span", { className: "font-medium text-[var(--color-text)]", children: renderArtistLink(membership.artist) }),
1240
- detailsText && /* @__PURE__ */ jsxs11("span", { className: "text-[var(--color-text-muted)]", children: [
1485
+ /* @__PURE__ */ jsxs14("li", { className: "truncate py-0.5", title: detailsText || void 0, children: [
1486
+ /* @__PURE__ */ jsx20("span", { className: "font-medium text-[var(--color-text)]", children: renderArtistLink(membership.artist) }),
1487
+ detailsText && /* @__PURE__ */ jsxs14("span", { className: "text-[var(--color-text-muted)]", children: [
1241
1488
  " \u2014 ",
1242
1489
  detailsText
1243
1490
  ] })
@@ -1248,9 +1495,9 @@ function MembershipList({ title, memberships, renderArtistLink }) {
1248
1495
  function ArtistMembers({ title, memberships, renderArtistLink }) {
1249
1496
  if (memberships.length === 0) return null;
1250
1497
  const isCurrent = (membership) => !membership.ended && membership.endYear === null;
1251
- return /* @__PURE__ */ jsxs11(PanelSection, { title, children: [
1252
- /* @__PURE__ */ jsx17(MembershipList, { title: "Current", memberships: memberships.filter(isCurrent), renderArtistLink }),
1253
- /* @__PURE__ */ jsx17(
1498
+ return /* @__PURE__ */ jsxs14(PanelSection, { title, children: [
1499
+ /* @__PURE__ */ jsx20(MembershipList, { title: "Current", memberships: memberships.filter(isCurrent), renderArtistLink }),
1500
+ /* @__PURE__ */ jsx20(
1254
1501
  MembershipList,
1255
1502
  {
1256
1503
  title: "Past",
@@ -1263,7 +1510,7 @@ function ArtistMembers({ title, memberships, renderArtistLink }) {
1263
1510
 
1264
1511
  // src/genre-tree/artist/ArtistDiscography.tsx
1265
1512
  import { PanelGroup as PanelGroup2, PanelSection as PanelSection2 } from "@behindthemusictree/ui";
1266
- import { jsx as jsx18, jsxs as jsxs12 } from "react/jsx-runtime";
1513
+ import { jsx as jsx21, jsxs as jsxs15 } from "react/jsx-runtime";
1267
1514
  var GROUPS = ["Albums", "EPs", "Singles", "Live", "Compilations", "Other"];
1268
1515
  var PRIMARY_GROUPS = { album: "Albums", ep: "EPs", single: "Singles" };
1269
1516
  function discographyGroup({ primaryType, secondaryTypes }) {
@@ -1283,18 +1530,18 @@ function byYear(a, b) {
1283
1530
  function ArtistDiscography({ albums }) {
1284
1531
  if (albums.length === 0) return null;
1285
1532
  const sorted = [...albums].sort(byYear);
1286
- return /* @__PURE__ */ jsx18(PanelSection2, { title: "Discography", children: GROUPS.map((group) => {
1533
+ return /* @__PURE__ */ jsx21(PanelSection2, { title: "Discography", children: GROUPS.map((group) => {
1287
1534
  const groupAlbums = sorted.filter((album) => discographyGroup(album) === group);
1288
1535
  if (groupAlbums.length === 0) return null;
1289
- 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: [
1290
- /* @__PURE__ */ jsx18("span", { className: "tabular-nums text-[var(--color-text-muted)]", children: album.year ?? "\u2014" }),
1291
- /* @__PURE__ */ jsx18("span", { className: "truncate text-[var(--color-text)]", title: album.name, children: album.name })
1536
+ return /* @__PURE__ */ jsx21(PanelGroup2, { title: group, children: /* @__PURE__ */ jsx21("ul", { children: groupAlbums.map((album) => /* @__PURE__ */ jsxs15("li", { className: "grid grid-cols-[2.5rem_minmax(0,1fr)] gap-2 py-0.5", children: [
1537
+ /* @__PURE__ */ jsx21("span", { className: "tabular-nums text-[var(--color-text-muted)]", children: album.year ?? "\u2014" }),
1538
+ /* @__PURE__ */ jsx21("span", { className: "truncate text-[var(--color-text)]", title: album.name, children: album.name })
1292
1539
  ] }, album.uuid)) }) }, group);
1293
1540
  }) });
1294
1541
  }
1295
1542
 
1296
1543
  // src/genre-tree/artist/ArtistSongs.tsx
1297
- import { useState as useState3 } from "react";
1544
+ import { useState as useState4 } from "react";
1298
1545
  import { keepPreviousData } from "@tanstack/react-query";
1299
1546
  import { Play as Play2 } from "lucide-react";
1300
1547
  import { Pagination, PanelSection as PanelSection3, Skeleton as Skeleton3 } from "@behindthemusictree/ui";
@@ -1315,41 +1562,49 @@ import { useTrackList as useTrackList3 } from "@behindthemusictree/app-kit/genre
1315
1562
  import { FaPlay as FaPlay2, FaPause as FaPause2 } from "react-icons/fa";
1316
1563
  import { usePlayer as usePlayer5 } from "@behindthemusictree/app-kit/player";
1317
1564
  import { PlayStates as PlayStates2 } from "@behindthemusictree/app-kit/player";
1318
- import { RingLoader as RingLoader2 } from "@behindthemusictree/ui";
1319
- import { jsx as jsx19, jsxs as jsxs13 } from "react/jsx-runtime";
1320
- function TrackPositionPlayPause({ position, uuid, handlePlayPauseClick }) {
1321
- const { playerTrackObject, playState } = usePlayer5();
1565
+ import { jsx as jsx22, jsxs as jsxs16 } from "react/jsx-runtime";
1566
+ function TrackPositionPlayPause({
1567
+ position,
1568
+ uuid,
1569
+ handlePlayPauseClick,
1570
+ isSelected = false
1571
+ }) {
1572
+ const { playerTrackObject, playbackConfirmedTrackId, playState, isLoading: isPlayerLoading } = usePlayer5();
1322
1573
  const isThisTrack = playerTrackObject?.track.id === uuid;
1323
- return /* @__PURE__ */ jsxs13(
1574
+ const isConfirmedPlayable = playbackConfirmedTrackId === uuid;
1575
+ const isAwaitingPlayback = (isSelected || isThisTrack) && !isConfirmedPlayable && !(isThisTrack && playerTrackObject?.loadError) && (!isThisTrack || isPlayerLoading || playState === PlayStates2.LOADING || playState === PlayStates2.PLAYING);
1576
+ const isPlaying = isThisTrack && isConfirmedPlayable && playState === PlayStates2.PLAYING;
1577
+ const isPaused = isThisTrack && isConfirmedPlayable && playState === PlayStates2.PAUSED;
1578
+ const isCurrentTrackActive = isPlaying || isPaused;
1579
+ return /* @__PURE__ */ jsxs16(
1324
1580
  "div",
1325
1581
  {
1326
- className: "track-position-play-pause flex items-center justify-center text-lg w-16 cursor-pointer",
1582
+ className: "track-position-play-pause relative flex items-center justify-center gap-2 text-lg w-16 cursor-pointer",
1327
1583
  style: { minWidth: "4rem", maxWidth: "4rem" },
1328
1584
  onClick: handlePlayPauseClick,
1329
1585
  children: [
1330
- /* @__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: [
1331
- /* @__PURE__ */ jsx19(
1332
- "div",
1333
- {
1334
- className: `w-playingbar bg-[var(--color-green-500)] h-3 origin-bottom ${playState === PlayStates2.PLAYING ? "animate-scale-pulse" : ""}`
1335
- }
1336
- ),
1337
- /* @__PURE__ */ jsx19(
1338
- "div",
1339
- {
1340
- className: `w-playingbar bg-[var(--color-green-500)] h-4 origin-bottom ${playState === PlayStates2.PLAYING ? "animate-scale-pulse delay-200" : ""}`,
1341
- style: { animationDelay: "0.2s" }
1342
- }
1343
- ),
1344
- /* @__PURE__ */ jsx19(
1345
- "div",
1346
- {
1347
- className: `w-playingbar bg-[var(--color-green-500)] h-3 origin-bottom ${playState === PlayStates2.PLAYING ? "animate-scale-pulse delay-400" : ""}`,
1348
- style: { animationDelay: "0.3s" }
1349
- }
1350
- )
1351
- ] }) : position }) }),
1352
- /* @__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, {}) })
1586
+ /* @__PURE__ */ jsx22("div", { className: "track-position-play-pause__status group-hover:hidden flex items-center justify-center", children: isAwaitingPlayback ? /* @__PURE__ */ jsx22("span", { className: "track-position-play-pause__spinner", role: "status", "aria-label": "Loading track" }) : isCurrentTrackActive ? /* @__PURE__ */ jsxs16(
1587
+ "span",
1588
+ {
1589
+ className: "track-position-play-pause__bars",
1590
+ "data-playing": isPlaying,
1591
+ role: "img",
1592
+ "aria-label": isPlaying ? "Playing" : "Paused",
1593
+ children: [
1594
+ /* @__PURE__ */ jsx22("span", { className: "track-position-play-pause__bar" }),
1595
+ /* @__PURE__ */ jsx22("span", { className: "track-position-play-pause__bar" }),
1596
+ /* @__PURE__ */ jsx22("span", { className: "track-position-play-pause__bar" })
1597
+ ]
1598
+ }
1599
+ ) : position }),
1600
+ /* @__PURE__ */ jsx22(
1601
+ "div",
1602
+ {
1603
+ className: `hidden group-hover:flex items-center justify-center ${isCurrentTrackActive ? "" : "absolute inset-0"}`,
1604
+ "aria-hidden": "true",
1605
+ children: isAwaitingPlayback ? /* @__PURE__ */ jsx22("span", { className: "track-position-play-pause__spinner" }) : isPlaying ? /* @__PURE__ */ jsx22(FaPause2, {}) : /* @__PURE__ */ jsx22(FaPlay2, {})
1606
+ }
1607
+ )
1353
1608
  ]
1354
1609
  }
1355
1610
  );
@@ -1358,7 +1613,7 @@ function TrackPositionPlayPause({ position, uuid, handlePlayPauseClick }) {
1358
1613
  // src/genre-tree/track-list-sidebar/TrackItem.tsx
1359
1614
  import { isSong as isSong2, songUnplayableReason as songUnplayableReason2 } from "@behindthemusictree/app-kit/genre-tree";
1360
1615
  import { ArtistNames as ArtistNames2 } from "@behindthemusictree/app-kit/genre-tree";
1361
- import { jsx as jsx20, jsxs as jsxs14 } from "react/jsx-runtime";
1616
+ import { jsx as jsx23, jsxs as jsxs17 } from "react/jsx-runtime";
1362
1617
  function TrackItem({
1363
1618
  track,
1364
1619
  position,
@@ -1367,25 +1622,26 @@ function TrackItem({
1367
1622
  renderArtistLink
1368
1623
  }) {
1369
1624
  const { handlePlayPauseAction, playerTrackObject, loadTrackForPlayer } = usePlayer6();
1370
- const { trackList, toTrackAtPosition } = useTrackList3();
1625
+ const { trackList, selectedTrack, toTrackAtPosition, runtimeUnplayableTrackIds } = useTrackList3();
1371
1626
  const scope = trackList?.origin?.scope ?? null;
1372
- const unplayableReason = isSong2(track) ? songUnplayableReason2(track) : null;
1627
+ const isCurrentTrack = playerTrackObject?.track.id === track.uuid;
1628
+ const hasPlaybackError = isCurrentTrack && !!playerTrackObject?.loadError;
1629
+ const isUnplayable = hasPlaybackError || runtimeUnplayableTrackIds?.has(track.uuid) === true || isSong2(track) && songUnplayableReason2(track) !== null;
1373
1630
  const handlePlayPauseClick = (event) => {
1374
1631
  event.stopPropagation();
1375
- if (playerTrackObject && playerTrackObject.track.id === track.uuid) {
1632
+ if (isCurrentTrack) {
1376
1633
  handlePlayPauseAction();
1377
1634
  } else if (scope != null) {
1378
1635
  toTrackAtPosition(position);
1379
1636
  loadTrackForPlayer(track.uuid);
1380
1637
  }
1381
1638
  };
1382
- return /* @__PURE__ */ jsxs14(
1639
+ return /* @__PURE__ */ jsxs17(
1383
1640
  "div",
1384
1641
  {
1385
- className: `track-item flex h-14 text-[var(--color-text)] ${unplayableReason ? "opacity-50" : "hover:bg-[var(--color-surface-muted)] group"}`,
1386
- title: unplayableReason ?? void 0,
1642
+ className: `track-item flex h-14 text-[var(--color-text)] ${isUnplayable ? "opacity-50" : "hover:bg-[var(--color-surface-muted)] group"}`,
1387
1643
  children: [
1388
- unplayableReason ? /* @__PURE__ */ jsx20(
1644
+ isUnplayable ? /* @__PURE__ */ jsx23(
1389
1645
  "div",
1390
1646
  {
1391
1647
  className: "flex items-center justify-center text-lg w-16",
@@ -1393,18 +1649,21 @@ function TrackItem({
1393
1649
  "aria-disabled": "true",
1394
1650
  children: position
1395
1651
  }
1396
- ) : /* @__PURE__ */ jsx20(TrackPositionPlayPause, { position, uuid: track.uuid, handlePlayPauseClick }),
1397
- /* @__PURE__ */ jsxs14("div", { className: "title-artist-container flex flex-col items-start justify-center min-w-0 flex-1", children: [
1398
- /* @__PURE__ */ jsxs14("div", { className: "flex max-w-full items-center gap-2", children: [
1399
- /* @__PURE__ */ jsx20("div", { className: "title text-lg font-bold text-[var(--color-text)] max-w-full truncate", children: track.title }),
1400
- isSong2(track) && /* @__PURE__ */ jsx20(MusicBrainzRecordingLink, { song: track })
1401
- ] }),
1402
- 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 }) }) : "",
1403
- unplayableReason && /* @__PURE__ */ jsx20("div", { className: "text-xs", children: /* @__PURE__ */ jsx20(UnplayableReason, { reason: unplayableReason }) })
1652
+ ) : /* @__PURE__ */ jsx23(
1653
+ TrackPositionPlayPause,
1654
+ {
1655
+ position,
1656
+ uuid: track.uuid,
1657
+ handlePlayPauseClick,
1658
+ isSelected: selectedTrack?.uuid === track.uuid
1659
+ }
1660
+ ),
1661
+ /* @__PURE__ */ jsxs17("div", { className: "title-artist-container flex flex-col items-start justify-center min-w-0 flex-1", children: [
1662
+ /* @__PURE__ */ jsx23("div", { className: "flex max-w-full items-center gap-2", children: /* @__PURE__ */ jsx23("div", { className: "title text-lg font-bold text-[var(--color-text)] max-w-full truncate", children: isSong2(track) ? /* @__PURE__ */ jsx23(MusicBrainzRecordingLink, { song: track, className: "hover:underline focus-visible:underline", children: track.title }) : track.title }) }),
1663
+ track.artists && track.artists.length > 0 ? /* @__PURE__ */ jsx23("div", { className: "artist text-base max-w-full truncate text-[var(--color-text-muted)]", children: /* @__PURE__ */ jsx23(ArtistNames2, { artists: track.artists, renderArtistLink }) }) : ""
1404
1664
  ] }),
1405
- /* @__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 : "" }),
1406
- /* @__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 }) : "" }),
1407
- renderDuration && /* @__PURE__ */ jsx20(
1665
+ /* @__PURE__ */ jsx23("div", { className: "genre-name-container flex items-center justify-start w-[21.6%] min-w-0 shrink-0", children: track.genre ? /* @__PURE__ */ jsx23("div", { className: "genre-name max-w-full whitespace-normal break-words p-1 text-xs font-bold", children: track.genre.name }) : "" }),
1666
+ renderDuration && /* @__PURE__ */ jsx23(
1408
1667
  "div",
1409
1668
  {
1410
1669
  className: "duration flex text-base w-16 items-center justify-center",
@@ -1412,22 +1671,22 @@ function TrackItem({
1412
1671
  children: renderDuration(track)
1413
1672
  }
1414
1673
  ),
1415
- renderActions && /* @__PURE__ */ jsx20("div", { className: "edit flex text-base w-6 items-center justify-center mr-2 cursor-pointer", children: renderActions(track) })
1674
+ renderActions && /* @__PURE__ */ jsx23("div", { className: "edit flex text-base w-6 items-center justify-center mr-2 cursor-pointer", children: renderActions(track) })
1416
1675
  ]
1417
1676
  }
1418
1677
  );
1419
1678
  }
1420
1679
 
1421
1680
  // src/genre-tree/artist/ArtistSongs.tsx
1422
- import { Fragment as Fragment4, jsx as jsx21, jsxs as jsxs15 } from "react/jsx-runtime";
1681
+ import { Fragment as Fragment6, jsx as jsx24, jsxs as jsxs18 } from "react/jsx-runtime";
1423
1682
  var SongPageSchema2 = PaginatedResponseSchema2(SongDetailedSchema2);
1424
1683
  var SORTS = [
1425
1684
  { ordering: "title", label: "Title" },
1426
1685
  { ordering: "-albumYear", label: "Year" }
1427
1686
  ];
1428
1687
  function ArtistSongs({ artist, getBackendBaseUrl, renderArtistLink }) {
1429
- const [page, setPage] = useState3(1);
1430
- const [ordering, setOrdering] = useState3("title");
1688
+ const [page, setPage] = useState4(1);
1689
+ const [ordering, setOrdering] = useState4("title");
1431
1690
  const { fetch } = useFetchWrapper2(getBackendBaseUrl);
1432
1691
  const { playNewTrackListFromArtist } = useTrackList4();
1433
1692
  const { playerTrackObject } = usePlayer7();
@@ -1452,21 +1711,21 @@ function ArtistSongs({ artist, getBackendBaseUrl, renderArtistLink }) {
1452
1711
  event.stopPropagation();
1453
1712
  void play(song.uuid);
1454
1713
  };
1455
- 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: [
1456
- /* @__PURE__ */ jsxs15("div", { className: "flex flex-wrap items-center justify-between gap-2 mb-2", children: [
1457
- /* @__PURE__ */ jsxs15(
1714
+ return /* @__PURE__ */ jsx24(PanelSection3, { title: "Songs", children: isPending ? /* @__PURE__ */ jsx24(Skeleton3, { className: "h-14", "aria-busy": "true" }) : isError ? /* @__PURE__ */ jsx24("p", { className: "text-red-600", children: "Could not load songs." }) : data.results.length === 0 ? /* @__PURE__ */ jsx24("p", { className: "text-[var(--color-text-muted)]", children: "No songs yet." }) : /* @__PURE__ */ jsxs18(Fragment6, { children: [
1715
+ /* @__PURE__ */ jsxs18("div", { className: "flex flex-wrap items-center justify-between gap-2 mb-2", children: [
1716
+ /* @__PURE__ */ jsxs18(
1458
1717
  "button",
1459
1718
  {
1460
1719
  type: "button",
1461
1720
  onClick: () => void play(),
1462
1721
  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",
1463
1722
  children: [
1464
- /* @__PURE__ */ jsx21(Play2, { className: "h-3.5 w-3.5", "aria-hidden": "true" }),
1723
+ /* @__PURE__ */ jsx24(Play2, { className: "h-3.5 w-3.5", "aria-hidden": "true" }),
1465
1724
  "Play all"
1466
1725
  ]
1467
1726
  }
1468
1727
  ),
1469
- /* @__PURE__ */ jsx21("div", { role: "group", "aria-label": "Sort songs by", className: "flex gap-1 text-xs", children: SORTS.map((sort) => /* @__PURE__ */ jsx21(
1728
+ /* @__PURE__ */ jsx24("div", { role: "group", "aria-label": "Sort songs by", className: "flex gap-1 text-xs", children: SORTS.map((sort) => /* @__PURE__ */ jsx24(
1470
1729
  "button",
1471
1730
  {
1472
1731
  type: "button",
@@ -1481,7 +1740,7 @@ function ArtistSongs({ artist, getBackendBaseUrl, renderArtistLink }) {
1481
1740
  sort.ordering
1482
1741
  )) })
1483
1742
  ] }),
1484
- /* @__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(
1743
+ /* @__PURE__ */ jsx24("ul", { className: "overflow-hidden mb-2 bg-zinc-950 rounded-lg", children: data.results.map((song, index) => /* @__PURE__ */ jsx24("li", { onClickCapture: (event) => handleRowClickCapture(event, song), children: /* @__PURE__ */ jsx24(
1485
1744
  TrackItem,
1486
1745
  {
1487
1746
  track: song,
@@ -1489,15 +1748,17 @@ function ArtistSongs({ artist, getBackendBaseUrl, renderArtistLink }) {
1489
1748
  renderArtistLink
1490
1749
  }
1491
1750
  ) }, song.uuid)) }),
1492
- /* @__PURE__ */ jsx21(Pagination, { currentPage: page, totalPages: data.totalPages, onPageChange: setPage })
1751
+ /* @__PURE__ */ jsx24(Pagination, { currentPage: page, totalPages: data.totalPages, onPageChange: setPage })
1493
1752
  ] }) });
1494
1753
  }
1495
1754
 
1496
1755
  // src/genre-tree/artist/useArtistPanel.tsx
1497
- import { Fragment as Fragment5, jsx as jsx22, jsxs as jsxs16 } from "react/jsx-runtime";
1756
+ import { Fragment as Fragment7, jsx as jsx25, jsxs as jsxs19 } from "react/jsx-runtime";
1498
1757
  function useArtistPanel({
1499
1758
  uuid,
1759
+ scope,
1500
1760
  getBackendBaseUrl,
1761
+ getShareUrl,
1501
1762
  renderArtistLink,
1502
1763
  expanded,
1503
1764
  onExpandedChange,
@@ -1512,38 +1773,41 @@ function useArtistPanel({
1512
1773
  return {
1513
1774
  ...panel,
1514
1775
  title: "Artist",
1515
- content: /* @__PURE__ */ jsxs16("div", { "aria-busy": "true", className: "flex flex-col gap-2.5 px-6 py-2.5", children: [
1516
- /* @__PURE__ */ jsx22(Skeleton4, { className: "h-3 w-16" }),
1517
- /* @__PURE__ */ jsx22(Skeleton4, { className: "h-4 w-32" }),
1518
- /* @__PURE__ */ jsx22(Skeleton4, { className: "h-3 w-16" }),
1519
- /* @__PURE__ */ jsx22(Skeleton4, { className: "h-4 w-24" }),
1520
- /* @__PURE__ */ jsx22(Skeleton4, { className: "h-16 mt-2.5" }),
1521
- /* @__PURE__ */ jsx22(Skeleton4, { className: "h-16" })
1776
+ content: /* @__PURE__ */ jsxs19("div", { "aria-busy": "true", className: "flex flex-col gap-2.5 px-6 py-2.5", children: [
1777
+ /* @__PURE__ */ jsx25(Skeleton4, { className: "h-3 w-16" }),
1778
+ /* @__PURE__ */ jsx25(Skeleton4, { className: "h-4 w-32" }),
1779
+ /* @__PURE__ */ jsx25(Skeleton4, { className: "h-3 w-16" }),
1780
+ /* @__PURE__ */ jsx25(Skeleton4, { className: "h-4 w-24" }),
1781
+ /* @__PURE__ */ jsx25(Skeleton4, { className: "h-16 mt-2.5" }),
1782
+ /* @__PURE__ */ jsx25(Skeleton4, { className: "h-16" })
1522
1783
  ] })
1523
1784
  };
1524
1785
  case "not-found":
1525
1786
  return {
1526
1787
  ...panel,
1527
1788
  title: "Artist not found",
1528
- content: /* @__PURE__ */ jsx22("p", { className: "px-6 py-2.5 text-[var(--color-text-muted)]", children: "This artist does not exist." })
1789
+ content: /* @__PURE__ */ jsx25("p", { className: "px-6 py-2.5 text-[var(--color-text-muted)]", children: "This artist does not exist." })
1529
1790
  };
1530
1791
  case "error":
1531
1792
  return {
1532
1793
  ...panel,
1533
1794
  title: "Artist",
1534
- content: /* @__PURE__ */ jsx22("p", { className: "px-6 py-2.5 text-red-600", children: "Could not load this artist." })
1795
+ content: /* @__PURE__ */ jsx25("p", { className: "px-6 py-2.5 text-red-600", children: "Could not load this artist." })
1535
1796
  };
1536
1797
  case "success": {
1537
1798
  const { artist } = result;
1799
+ const shareTarget = scope ? { kind: "artist", uuid: artist.uuid, name: artist.name, scope } : void 0;
1800
+ const shareUrl = shareTarget ? getShareUrl?.(shareTarget) : void 0;
1538
1801
  return {
1539
1802
  ...panel,
1540
1803
  title: artist.name,
1541
- content: /* @__PURE__ */ jsxs16(Fragment5, { children: [
1542
- artist.image && /* @__PURE__ */ jsx22(ArtistPhoto, { image: artist.image, name: artist.name }),
1543
- /* @__PURE__ */ jsx22(ArtistHeader, { artist }),
1544
- /* @__PURE__ */ jsx22(ArtistMembers, { title: "Members", memberships: artist.members, renderArtistLink }),
1545
- /* @__PURE__ */ jsx22(ArtistMembers, { title: "Member of", memberships: artist.memberOf, renderArtistLink }),
1546
- artist.tracksCount > 0 && /* @__PURE__ */ jsx22(
1804
+ content: /* @__PURE__ */ jsxs19(Fragment7, { children: [
1805
+ shareUrl && /* @__PURE__ */ jsx25("div", { className: "genre-action-band border-b border-[var(--color-border)]", children: /* @__PURE__ */ jsx25("div", { className: "flex flex-wrap gap-1 px-3.5 py-3", children: shareTarget && /* @__PURE__ */ jsx25(ShareActionButton, { target: shareTarget, url: shareUrl }) }) }),
1806
+ artist.image && /* @__PURE__ */ jsx25(ArtistPhoto, { image: artist.image, name: artist.name }),
1807
+ /* @__PURE__ */ jsx25(ArtistHeader, { artist }),
1808
+ /* @__PURE__ */ jsx25(ArtistMembers, { title: "Members", memberships: artist.members, renderArtistLink }),
1809
+ /* @__PURE__ */ jsx25(ArtistMembers, { title: "Member of", memberships: artist.memberOf, renderArtistLink }),
1810
+ artist.tracksCount > 0 && /* @__PURE__ */ jsx25(
1547
1811
  ArtistSongs,
1548
1812
  {
1549
1813
  artist: { uuid: artist.uuid, name: artist.name },
@@ -1552,7 +1816,7 @@ function useArtistPanel({
1552
1816
  }
1553
1817
  )
1554
1818
  ] }),
1555
- extraContent: artist.albums.length > 0 ? /* @__PURE__ */ jsx22(ArtistDiscography, { albums: artist.albums }) : void 0
1819
+ extraContent: artist.albums.length > 0 ? /* @__PURE__ */ jsx25(ArtistDiscography, { albums: artist.albums }) : void 0
1556
1820
  };
1557
1821
  }
1558
1822
  }
@@ -1561,27 +1825,27 @@ function useArtistPanel({
1561
1825
  // src/genre-tree/SizeMetricToggle.tsx
1562
1826
  import { MdMusicNote, MdVisibility } from "react-icons/md";
1563
1827
  import { SegmentedControl } from "@behindthemusictree/ui";
1564
- import { jsx as jsx23 } from "react/jsx-runtime";
1828
+ import { jsx as jsx26 } from "react/jsx-runtime";
1565
1829
  function SizeMetricToggle({
1566
1830
  value,
1567
1831
  onChange
1568
1832
  }) {
1569
- return /* @__PURE__ */ jsx23(
1833
+ return /* @__PURE__ */ jsx26(
1570
1834
  SegmentedControl,
1571
1835
  {
1572
1836
  "aria-label": "Size nodes by",
1573
1837
  value,
1574
1838
  onChange,
1575
1839
  options: [
1576
- { value: "songs", label: /* @__PURE__ */ jsx23(MdMusicNote, { size: 18, "aria-hidden": true }), "aria-label": "Size by songs", title: "Node size by song count" },
1577
- { value: "views", label: /* @__PURE__ */ jsx23(MdVisibility, { size: 18, "aria-hidden": true }), "aria-label": "Size by views", title: "Node size by song views" }
1840
+ { value: "songs", label: /* @__PURE__ */ jsx26(MdMusicNote, { size: 18, "aria-hidden": true }), "aria-label": "Size by songs", title: "Node size by song count" },
1841
+ { value: "views", label: /* @__PURE__ */ jsx26(MdVisibility, { size: 18, "aria-hidden": true }), "aria-label": "Size by views", title: "Node size by song views" }
1578
1842
  ]
1579
1843
  }
1580
1844
  );
1581
1845
  }
1582
1846
 
1583
1847
  // src/genre-tree/GenreTreeView.tsx
1584
- import { Fragment as Fragment6, jsx as jsx24, jsxs as jsxs17 } from "react/jsx-runtime";
1848
+ import { Fragment as Fragment8, jsx as jsx27, jsxs as jsxs20 } from "react/jsx-runtime";
1585
1849
  var PREVIOUS_BUTTON_CSS = `
1586
1850
  .genre-tree-view .search-overlay .gtv-info-panel-previous { display: none; }
1587
1851
  .genre-tree-view:has(.search-overlay .gtv-info-panel-previous) .genre-search input { padding-left: 2.75rem; }
@@ -1603,7 +1867,7 @@ function GenreNodeActionBand({ genrePlaylist, scope, playback, ...bandProps }) {
1603
1867
  console.error("Failed to load genre playlist tracks:", error);
1604
1868
  });
1605
1869
  };
1606
- return /* @__PURE__ */ jsx24(
1870
+ return /* @__PURE__ */ jsx27(
1607
1871
  GenreActionBand,
1608
1872
  {
1609
1873
  ...bandProps,
@@ -1627,13 +1891,14 @@ function GenreTreeView({
1627
1891
  defaultViewport,
1628
1892
  onViewportChange,
1629
1893
  readOnly = false,
1630
- className,
1894
+ className: className2,
1631
1895
  showDisplayOptions = true,
1632
1896
  showAddRoot = true,
1633
1897
  allowWheelRotation: controlledAllowWheelRotation,
1634
1898
  criteriaOverviewSchema,
1635
1899
  renderGenreDetailExtras,
1636
1900
  controlsOrientation = "horizontal",
1901
+ floatingControlsLeading,
1637
1902
  sizeMetric: controlledSizeMetric,
1638
1903
  onSizeMetricChange,
1639
1904
  renderArtistLink,
@@ -1641,6 +1906,7 @@ function GenreTreeView({
1641
1906
  onArtistUuidChange,
1642
1907
  artistExpanded: controlledArtistExpanded,
1643
1908
  onArtistExpandedChange,
1909
+ getShareUrl,
1644
1910
  artistSearch = true,
1645
1911
  onArtistSelect,
1646
1912
  artistSearchBaseUrl,
@@ -1658,10 +1924,12 @@ function GenreTreeView({
1658
1924
  placement = "default",
1659
1925
  ...focus
1660
1926
  }) {
1661
- const [internalArtistUuid, setInternalArtistUuid] = useState4(null);
1927
+ const getShareUrlRef = useRef4(getShareUrl);
1928
+ getShareUrlRef.current = getShareUrl;
1929
+ const [internalArtistUuid, setInternalArtistUuid] = useState5(null);
1662
1930
  const isArtistControlled = controlledArtistUuid !== void 0;
1663
1931
  const artistUuid = isArtistControlled ? controlledArtistUuid : internalArtistUuid;
1664
- const [internalSizeMetric, setInternalSizeMetric] = useState4("songs");
1932
+ const [internalSizeMetric, setInternalSizeMetric] = useState5("songs");
1665
1933
  const sizeMetric = controlledSizeMetric ?? internalSizeMetric;
1666
1934
  const deferredSizeMetric = useDeferredValue(sizeMetric);
1667
1935
  const changeSizeMetric = (metric) => {
@@ -1700,24 +1968,30 @@ function GenreTreeView({
1700
1968
  artistUuid
1701
1969
  });
1702
1970
  const isGenreUpdating = useIsMutating({ mutationKey: genreUpdateMutationKey }) > 0;
1971
+ const { mutate: updateGenre } = useUpdateGenre2(scope, getBackendBaseUrl);
1703
1972
  const busy = deferredSizeMetric !== sizeMetric || isTreeFetching || isGenreUpdating;
1973
+ const isArtistFiltered = artistUuid !== null;
1974
+ const canSizeByViews = !isArtistFiltered && hasViewsCount(visibleGenrePlaylists);
1704
1975
  const artistResult = useArtist2(artistUuid, artistSearchBaseUrl ?? getBackendBaseUrl);
1705
1976
  const filterArtistName = artistResult.status === "success" ? artistResult.artist.name : null;
1706
- const filterArtist = useMemo6(
1977
+ const filterArtist = useMemo7(
1707
1978
  () => artistUuid !== null && filterArtistName !== null ? { uuid: artistUuid, name: filterArtistName } : null,
1708
1979
  [artistUuid, filterArtistName]
1709
1980
  );
1981
+ const hasSelectedGenreNode = selectedGenrePlaylist !== void 0 && visibleGenrePlaylists.some(({ uuid }) => uuid === selectedGenrePlaylist.uuid);
1710
1982
  const playback = useGenrePlaylistPlayback({ scope, genrePlaylists: visibleGenrePlaylists, artist: filterArtist });
1711
1983
  const openArtist = (uuid) => {
1712
1984
  if (selectedNodeId !== null) clearSelection();
1713
1985
  changeArtist(uuid);
1714
1986
  };
1715
- const [expandedArtistUuid, setExpandedArtistUuid] = useState4(null);
1987
+ const [expandedArtistUuid, setExpandedArtistUuid] = useState5(null);
1716
1988
  const artistBaseUrl = artistSearchBaseUrl ?? getBackendBaseUrl;
1717
1989
  const artistPanel = useArtistPanel({
1718
1990
  uuid: artistUuid,
1991
+ scope,
1719
1992
  getBackendBaseUrl: artistBaseUrl,
1720
- renderArtistLink: renderArtistLink ?? ((artist) => artist.isSpecialPurpose ? artist.name : /* @__PURE__ */ jsx24("button", { type: "button", onClick: () => openArtist(artist.uuid), className: "hover:underline", children: artist.name })),
1993
+ getShareUrl: getShareUrlRef.current,
1994
+ renderArtistLink: renderArtistLink ?? ((artist) => artist.isSpecialPurpose ? artist.name : /* @__PURE__ */ jsx27("button", { type: "button", onClick: () => openArtist(artist.uuid), className: "hover:underline", children: artist.name })),
1721
1995
  expanded: controlledArtistExpanded ?? (artistUuid !== null && expandedArtistUuid === artistUuid),
1722
1996
  onExpandedChange: (on) => {
1723
1997
  if (controlledArtistExpanded === void 0) setExpandedArtistUuid(on ? artistUuid : null);
@@ -1725,10 +1999,10 @@ function GenreTreeView({
1725
1999
  },
1726
2000
  onClose: () => changeArtist(null)
1727
2001
  });
1728
- const builtInArtistPanel = overlayPanel || selectedNodeId !== null ? void 0 : artistPanel;
2002
+ const builtInArtistPanel = overlayPanel || hasSelectedGenreNode ? void 0 : artistPanel;
1729
2003
  const activeOverlayPanel = overlayPanel ?? (builtInArtistPanel && { ...builtInArtistPanel, hideTitle: true, hideClose: true });
1730
- const [uncontrolledCollapsed, setUncontrolledCollapsed] = useState4(false);
1731
- const [uncontrolledExpanded, setUncontrolledExpanded] = useState4(false);
2004
+ const [uncontrolledCollapsed, setUncontrolledCollapsed] = useState5(false);
2005
+ const [uncontrolledExpanded, setUncontrolledExpanded] = useState5(false);
1732
2006
  const isCollapsed = collapsed ?? uncontrolledCollapsed;
1733
2007
  const isExpanded = expanded ?? uncontrolledExpanded;
1734
2008
  const changeCollapsed = (next) => {
@@ -1739,11 +2013,11 @@ function GenreTreeView({
1739
2013
  setUncontrolledExpanded(next);
1740
2014
  onExpandedChange?.(next);
1741
2015
  };
1742
- useEffect4(() => {
2016
+ useEffect5(() => {
1743
2017
  setUncontrolledCollapsed(false);
1744
2018
  }, [selectedNodeId, artistUuid]);
1745
- const isDetailHidden = isCollapsed || !selectedNodeId && !activeOverlayPanel;
1746
- useEffect4(() => {
2019
+ const isDetailHidden = isCollapsed || !hasSelectedGenreNode && !activeOverlayPanel;
2020
+ useEffect5(() => {
1747
2021
  if (placement !== "regions" || !isDetailHidden) return void 0;
1748
2022
  const root = document.documentElement;
1749
2023
  root.style.setProperty("--layout-left-w", "0px");
@@ -1762,29 +2036,38 @@ function GenreTreeView({
1762
2036
  hideExpandButton: placement === "regions" ? true : void 0,
1763
2037
  overlayPanel: detailOverlay ? { ...detailOverlay, collapsed: isCollapsed, hideExpandButton: true } : activeOverlayPanel
1764
2038
  };
1765
- const [isWheelRevealed, setIsWheelRevealed] = useState4(false);
2039
+ const [isWheelRevealed, setIsWheelRevealed] = useState5(false);
1766
2040
  const usesHandoff = viewMode === "wheel" || viewMode === "pop-core";
1767
2041
  const isTreeReady = !isLoading && (!usesHandoff || isWheelRevealed);
1768
2042
  const rootRef = useRef4(null);
1769
2043
  const handlePrevious = () => {
1770
2044
  rootRef.current?.querySelector(".search-overlay .gtv-info-panel-previous")?.click();
1771
2045
  };
1772
- const { showPopup, hidePopup } = usePopup4();
1773
- const [searchText, setSearchText] = useState4("");
2046
+ useEffect5(() => {
2047
+ if (!isTreeReady || viewMode !== "pop-core" || !hasSelectedGenreNode || !selectedGenrePlaylist) return;
2048
+ const root = rootRef.current;
2049
+ const centerChip = root?.querySelector(".gtv-wheel-center-node .gtv-wheel-chip");
2050
+ if (!centerChip || centerChip.getAttribute("aria-label") !== selectedGenrePlaylist.name) return;
2051
+ const panelTitle = root?.querySelector(".gtv-info-panel-title");
2052
+ if (panelTitle?.textContent?.trim().startsWith(selectedGenrePlaylist.name)) return;
2053
+ centerChip.click();
2054
+ }, [hasSelectedGenreNode, isTreeReady, selectedGenrePlaylist, viewMode]);
2055
+ const { showPopup, hidePopup } = usePopup5();
2056
+ const [searchText, setSearchText] = useState5("");
1774
2057
  const artistSearchResult = useArtistSearch(artistBaseUrl, artistSearch ? searchText : "");
1775
2058
  const artistExtraResults = artistSearch ? {
1776
2059
  label: "Artists",
1777
2060
  items: artistSearchResult.artists.map((artist) => ({
1778
2061
  key: artist.uuid,
1779
- content: /* @__PURE__ */ jsxs17(Fragment6, { children: [
1780
- /* @__PURE__ */ jsxs17("span", { className: "block truncate font-medium", children: [
2062
+ content: /* @__PURE__ */ jsxs20(Fragment8, { children: [
2063
+ /* @__PURE__ */ jsxs20("span", { className: "block truncate font-medium", children: [
1781
2064
  artist.name,
1782
- artist.disambiguation && /* @__PURE__ */ jsxs17("span", { className: "font-normal text-[var(--color-neutral-500)]", children: [
2065
+ artist.disambiguation && /* @__PURE__ */ jsxs20("span", { className: "font-normal text-[var(--color-neutral-500)]", children: [
1783
2066
  " \xB7 ",
1784
2067
  artist.disambiguation
1785
2068
  ] })
1786
2069
  ] }),
1787
- artist.matchedAlias && /* @__PURE__ */ jsxs17("span", { className: "block truncate text-xs text-[var(--color-neutral-500)]", children: [
2070
+ artist.matchedAlias && /* @__PURE__ */ jsxs20("span", { className: "block truncate text-xs text-[var(--color-neutral-500)]", children: [
1788
2071
  "also known as ",
1789
2072
  artist.matchedAlias
1790
2073
  ] })
@@ -1812,29 +2095,45 @@ function GenreTreeView({
1812
2095
  };
1813
2096
  const zoomInRef = useRef4(void 0);
1814
2097
  zoomInRef.current = canZoomIn ? handleGenreSearchZoomIn : void 0;
1815
- const renderInfoPanelBody = useCallback5(
2098
+ const renderInfoPanelBody = useCallback6(
1816
2099
  (node, { toolbar, ancestors, family }) => {
1817
2100
  const genrePlaylist = genrePlaylistByNodeId.get(node.id);
1818
2101
  const criteria = genrePlaylist?.criteria;
1819
- if (!genrePlaylist || !criteria) return { primary: /* @__PURE__ */ jsxs17(Fragment6, { children: [
2102
+ if (!genrePlaylist || !criteria) return { primary: /* @__PURE__ */ jsxs20(Fragment8, { children: [
1820
2103
  toolbar,
1821
2104
  family
1822
2105
  ] }), aside: ancestors };
1823
2106
  const genreUuid = criteria.uuid;
2107
+ const parentPlaylist = visibleGenrePlaylists.find(({ uuid }) => uuid === genrePlaylist.parent?.uuid);
2108
+ const canToggleSide = !readOnly && parentPlaylist !== void 0 && parentPlaylist.parent === null;
2109
+ const side = criteria.side ?? "core";
2110
+ const shareTarget = {
2111
+ kind: "genre",
2112
+ genrePlaylistUuid: genrePlaylist.uuid,
2113
+ name: criteria.name,
2114
+ scope,
2115
+ artistUuid
2116
+ };
2117
+ const shareUrl = getShareUrlRef.current?.(shareTarget) ?? void 0;
2118
+ const popSibling = canToggleSide && side === "core" ? visibleGenrePlaylists.find(
2119
+ (sibling) => sibling.parent?.uuid === parentPlaylist.uuid && sibling.uuid !== genrePlaylist.uuid && sibling.criteria?.side === "pop"
2120
+ ) : void 0;
1824
2121
  return {
1825
- primary: /* @__PURE__ */ jsxs17(Fragment6, { children: [
2122
+ primary: /* @__PURE__ */ jsxs20(Fragment8, { children: [
1826
2123
  toolbar,
1827
- /* @__PURE__ */ jsx24(
2124
+ /* @__PURE__ */ jsx27(
1828
2125
  GenreNodeActionBand,
1829
2126
  {
1830
2127
  node,
1831
2128
  genrePlaylist,
1832
2129
  scope,
2130
+ shareTarget: shareUrl ? shareTarget : void 0,
2131
+ shareUrl,
1833
2132
  onZoomIn: zoomInRef.current,
1834
2133
  onAddChild: readOnly ? void 0 : () => handleGenreCreationAction(criteria),
1835
2134
  onRename: readOnly ? void 0 : () => handleGenreRenameAction(criteria),
1836
2135
  onChangeParent: readOnly ? void 0 : () => showPopup(
1837
- /* @__PURE__ */ jsx24(
2136
+ /* @__PURE__ */ jsx27(
1838
2137
  ChangeParentPopup,
1839
2138
  {
1840
2139
  genrePlaylist: { ...genrePlaylist, criteria },
@@ -1846,16 +2145,20 @@ function GenreTreeView({
1846
2145
  }
1847
2146
  )
1848
2147
  ),
2148
+ side,
2149
+ onToggleSide: canToggleSide ? () => updateGenre({ uuid: genreUuid, data: { side: side === "pop" ? "core" : "pop" } }) : void 0,
2150
+ toggleSideDisabledReason: popSibling ? `${popSibling.name} is already the pop side of ${parentPlaylist?.name}` : void 0,
1849
2151
  additionalActions,
1850
2152
  playback
1851
2153
  }
1852
2154
  ),
1853
- /* @__PURE__ */ jsx24(
2155
+ /* @__PURE__ */ jsx27(
1854
2156
  GenreDetailExtras,
1855
2157
  {
1856
2158
  family,
1857
2159
  genreUuid,
1858
2160
  genrePlaylist,
2161
+ wikidataId: criteria.wikidataId,
1859
2162
  scope,
1860
2163
  getBackendBaseUrl,
1861
2164
  criteriaOverviewSchema,
@@ -1884,6 +2187,7 @@ function GenreTreeView({
1884
2187
  hidePopup,
1885
2188
  onParentChanged,
1886
2189
  visibleGenrePlaylists,
2190
+ updateGenre,
1887
2191
  playback,
1888
2192
  filterArtist,
1889
2193
  scope,
@@ -1900,7 +2204,7 @@ function GenreTreeView({
1900
2204
  );
1901
2205
  const isRotationControlled = controlledAllowWheelRotation !== void 0;
1902
2206
  const allowWheelRotation = controlledAllowWheelRotation ?? internalAllowWheelRotation;
1903
- const detailTab = placement === "regions" && (selectedNodeId || detailOverlay) ? /* @__PURE__ */ jsx24(
2207
+ const detailTab = placement === "regions" && (hasSelectedGenreNode || detailOverlay) ? /* @__PURE__ */ jsx27(
1904
2208
  PanelEdgeTab,
1905
2209
  {
1906
2210
  side: "left",
@@ -1908,25 +2212,14 @@ function GenreTreeView({
1908
2212
  center: detailExpanded ? null : isCollapsed ? { label: "Show panel", onClick: () => changeCollapsed(false) } : onDetailExpandedChange ? { label: "Extend panel", onClick: () => onDetailExpandedChange(true) } : null
1909
2213
  }
1910
2214
  ) : null;
1911
- const actions = /* @__PURE__ */ jsxs17(Fragment6, { children: [
1912
- !isLoading && /* @__PURE__ */ jsx24(
1913
- GenreSearch,
1914
- {
1915
- genrePlaylists: visibleGenrePlaylists,
1916
- onSelect: handleGenreSearchSelect,
1917
- selectedName: selectedGenrePlaylist?.name ?? builtInArtistPanel?.title ?? null,
1918
- onClear: builtInArtistPanel ? () => changeArtist(null) : handleGenreSearchClear,
1919
- onPrevious: handlePrevious,
1920
- onQueryChange: artistSearch ? setSearchText : void 0,
1921
- extraResults: artistExtraResults
1922
- }
1923
- ),
1924
- !isLoading && filterArtist && /* @__PURE__ */ jsxs17("span", { className: "inline-flex items-center overflow-hidden rounded-full bg-[var(--color-neutral-100)] text-sm", children: [
1925
- /* @__PURE__ */ jsx24(
2215
+ const renderArtistFilter = () => {
2216
+ if (isLoading || !filterArtist) return null;
2217
+ return /* @__PURE__ */ jsxs20("span", { className: "inline-flex items-center overflow-hidden rounded-full bg-[var(--color-neutral-100)] text-sm", children: [
2218
+ /* @__PURE__ */ jsx27(
1926
2219
  "button",
1927
2220
  {
1928
2221
  type: "button",
1929
- className: "py-1 pl-3 pr-2 hover:underline",
2222
+ className: "py-1 pl-3 pr-2 [@media(pointer:coarse)]:min-h-[var(--layout-touch-target)] hover:underline",
1930
2223
  "aria-label": `Show artist ${filterArtist.name}`,
1931
2224
  onClick: () => {
1932
2225
  if (selectedNodeId !== null) clearSelection();
@@ -1934,86 +2227,174 @@ function GenreTreeView({
1934
2227
  children: filterArtist.name
1935
2228
  }
1936
2229
  ),
1937
- /* @__PURE__ */ jsx24(
2230
+ /* @__PURE__ */ jsx27(
1938
2231
  "button",
1939
2232
  {
1940
2233
  type: "button",
1941
- className: "py-1 pl-1 pr-3 hover:text-[var(--color-text)]",
2234
+ className: "py-1 pl-1 pr-3 [@media(pointer:coarse)]:min-h-[var(--layout-touch-target)] hover:text-[var(--color-text)]",
1942
2235
  "aria-label": `Clear artist filter ${filterArtist.name}`,
1943
2236
  onClick: () => changeArtist(null),
1944
2237
  children: "\xD7"
1945
2238
  }
1946
2239
  )
1947
- ] }),
1948
- !isLoading && !isControlled && /* @__PURE__ */ jsx24(
1949
- SegmentedControl2,
1950
- {
1951
- "aria-label": "Tree view mode",
1952
- value: viewMode,
1953
- onChange: setViewMode,
1954
- options: [
1955
- { value: "pop-core", label: "Pop/Core", disabled: !canShowPopCore, title: canShowPopCore ? void 0 : NO_POP_ROOT_TITLE },
1956
- { value: "wheel", label: "Wheel" },
1957
- { value: "stacked", label: "Stacked" },
1958
- { value: "outline", label: "Outline", disabled: !canShowPopCore, title: canShowPopCore ? void 0 : NO_POP_ROOT_TITLE }
1959
- ]
1960
- }
1961
- ),
1962
- !isLoading && showDisplayOptions && /* @__PURE__ */ jsx24(
1963
- "div",
2240
+ ] });
2241
+ };
2242
+ const actions = /* @__PURE__ */ jsxs20(Fragment8, { children: [
2243
+ !isLoading && /* @__PURE__ */ jsx27(
2244
+ GenreSearch,
1964
2245
  {
1965
- className: "flex items-center gap-1",
1966
- role: "group",
1967
- "aria-label": "Tree display options",
1968
- children: !isRotationControlled && viewMode !== "stacked" && viewMode !== "outline" && /* @__PURE__ */ jsx24(
1969
- Button,
1970
- {
1971
- variant: allowWheelRotation ? "default" : "outline",
1972
- size: "sm",
1973
- onClick: toggleWheelRotation,
1974
- children: "Rotation"
1975
- }
1976
- )
2246
+ genrePlaylists: visibleGenrePlaylists,
2247
+ onSelect: handleGenreSearchSelect,
2248
+ selectedName: selectedGenrePlaylist?.name ?? builtInArtistPanel?.title ?? null,
2249
+ onClear: builtInArtistPanel ? () => changeArtist(null) : handleGenreSearchClear,
2250
+ onPrevious: handlePrevious,
2251
+ onQueryChange: artistSearch ? setSearchText : void 0,
2252
+ extraResults: artistExtraResults
1977
2253
  }
1978
2254
  ),
1979
- !isLoading && !readOnly && showAddRoot && /* @__PURE__ */ jsx24(
1980
- IconTextButton,
1981
- {
1982
- icon: Plus2,
1983
- text: "Add root",
1984
- onClick: () => handleGenreCreationAction(null)
1985
- }
1986
- )
2255
+ !isLoading && filterArtist && /* @__PURE__ */ jsx27("div", { className: "hidden w-full justify-center max-md:flex", children: renderArtistFilter() }),
2256
+ /* @__PURE__ */ jsxs20("div", { className: "contents max-md:flex max-md:w-full max-md:items-center max-md:gap-2 max-md:overflow-x-auto max-md:[scrollbar-width:none] max-md:[&>*]:shrink-0", children: [
2257
+ !isLoading && !isControlled && /* @__PURE__ */ jsx27(
2258
+ SegmentedControl2,
2259
+ {
2260
+ "aria-label": "Tree view mode",
2261
+ value: viewMode,
2262
+ onChange: setViewMode,
2263
+ options: [
2264
+ { value: "pop-core", label: "Pop/Core", disabled: !canShowPopCore, title: canShowPopCore ? void 0 : NO_POP_ROOT_TITLE },
2265
+ { value: "wheel", label: "Wheel" },
2266
+ { value: "stacked", label: "Stacked" },
2267
+ { value: "outline", label: "Outline", disabled: !canShowPopCore, title: canShowPopCore ? void 0 : NO_POP_ROOT_TITLE }
2268
+ ]
2269
+ }
2270
+ ),
2271
+ !isLoading && showDisplayOptions && /* @__PURE__ */ jsx27(
2272
+ "div",
2273
+ {
2274
+ className: "flex items-center gap-1",
2275
+ role: "group",
2276
+ "aria-label": "Tree display options",
2277
+ children: !isRotationControlled && viewMode !== "stacked" && viewMode !== "outline" && /* @__PURE__ */ jsx27(
2278
+ Button,
2279
+ {
2280
+ variant: allowWheelRotation ? "default" : "outline",
2281
+ size: "sm",
2282
+ onClick: toggleWheelRotation,
2283
+ children: "Rotation"
2284
+ }
2285
+ )
2286
+ }
2287
+ ),
2288
+ !isLoading && !readOnly && showAddRoot && /* @__PURE__ */ jsx27(
2289
+ IconTextButton,
2290
+ {
2291
+ icon: Plus2,
2292
+ text: "Add root",
2293
+ onClick: () => handleGenreCreationAction(null)
2294
+ }
2295
+ )
2296
+ ] })
1987
2297
  ] });
1988
- return /* @__PURE__ */ jsxs17(
2298
+ return /* @__PURE__ */ jsxs20(
1989
2299
  "div",
1990
2300
  {
1991
2301
  ref: rootRef,
1992
- className: ["genre-tree-view relative flex flex-col h-full", placement === "regions" && REGIONS_CLASS, className].filter(Boolean).join(" "),
2302
+ className: ["genre-tree-view relative flex flex-col h-full", placement === "regions" && REGIONS_CLASS, className2].filter(Boolean).join(" "),
1993
2303
  children: [
1994
- /* @__PURE__ */ jsx24("style", { children: PREVIOUS_BUTTON_CSS }),
1995
- /* @__PURE__ */ jsx24(
2304
+ /* @__PURE__ */ jsx27("style", { children: PREVIOUS_BUTTON_CSS }),
2305
+ /* @__PURE__ */ jsx27(
1996
2306
  "div",
1997
2307
  {
1998
- 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"}`,
2308
+ className: `actions-container absolute z-30 flex items-center gap-2 ${placement === "regions" ? "left-[calc(var(--layout-gutter)+var(--layout-panel-inset))] top-[calc(var(--layout-gutter)+var(--layout-panel-inset))] max-md:right-[calc(var(--layout-gutter)+var(--layout-panel-inset))] max-md:top-[var(--layout-band-top)] max-md:flex-col max-md:items-stretch" : "left-5 top-5"}`,
1999
2309
  children: actions
2000
2310
  }
2001
2311
  ),
2002
- detailTab && /* @__PURE__ */ jsx24(
2312
+ !isLoading && filterArtist && /* @__PURE__ */ jsx27(
2003
2313
  "div",
2004
2314
  {
2005
- 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))]"}`,
2315
+ className: `absolute left-1/2 z-30 hidden -translate-x-1/2 md:block ${placement === "regions" ? "top-[calc(var(--layout-gutter)+var(--layout-panel-inset))]" : "top-5"}`,
2316
+ children: renderArtistFilter()
2317
+ }
2318
+ ),
2319
+ detailTab && /* @__PURE__ */ jsx27(
2320
+ "div",
2321
+ {
2322
+ className: `absolute top-1/2 z-30 -translate-y-1/2 max-md:hidden transition-[opacity,visibility] duration-150 ease-out ${isTreeReady ? "opacity-100" : "pointer-events-none invisible opacity-0"} ${detailExpanded ? "right-[calc(var(--layout-center-right)-var(--layout-edge-tab-w))]" : isCollapsed ? "left-0" : "left-[calc(var(--layout-gutter)+var(--layout-left-w))]"}`,
2006
2323
  children: detailTab
2007
2324
  }
2008
2325
  ),
2009
- /* @__PURE__ */ jsx24("div", { className: "content-container flex-1 min-h-0 flex flex-row gap-4", children: /* @__PURE__ */ jsx24("div", { className: "tree-view-container flex-1 min-w-0 flex flex-col h-full", children: isLoading ? /* @__PURE__ */ jsx24(GenreTreeViewSkeleton, { viewMode }) : viewMode === "wheel" ? (
2010
- // The info panel's top edge aligns with the tree's; the search bar floats `--layout-panel-inset` below it,
2011
- // and the header inset (the bar's height plus that inset) extends the header's fill behind the bar,
2012
- // Google Maps style; the 8px bottom padding mirrors the inset above the bar, and the hidden title's height is zeroed.
2013
- // The panel's own Zoom in button is hidden: the search bar carries it.
2014
- /* @__PURE__ */ jsx24("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__ */ jsx24(GenreTreeWheelHandoff, { skeleton: /* @__PURE__ */ jsx24(GenreTreeWheelSkeleton, {}), onRevealedChange: setIsWheelRevealed, children: /* @__PURE__ */ jsx24(
2015
- GenrePlaylistTreeWheel,
2326
+ /* @__PURE__ */ jsx27("div", { className: "content-container flex-1 min-h-0 flex flex-row gap-4", children: /* @__PURE__ */ jsxs20("div", { className: "tree-view-container relative flex-1 min-w-0 flex flex-col h-full", children: [
2327
+ !isLoading && viewMode === "outline" && floatingControlsLeading && /* @__PURE__ */ jsx27("div", { className: "absolute bottom-3 right-3 z-10", children: floatingControlsLeading }),
2328
+ isLoading ? /* @__PURE__ */ jsx27(GenreTreeViewSkeleton, { viewMode }) : viewMode === "wheel" ? (
2329
+ // The info panel's top edge aligns with the tree's; the search bar floats `--layout-panel-inset` below it,
2330
+ // and the header inset (the bar's height plus that inset) extends the header's fill behind the bar,
2331
+ // Google Maps style; the 8px bottom padding mirrors the inset above the bar, and the hidden title's height is zeroed.
2332
+ // The panel's own Zoom in button is hidden: the search bar carries it.
2333
+ /* @__PURE__ */ jsx27("div", { className: "tree-container search-overlay relative flex-1 w-full min-h-0 [--gtv-info-panel-header-inset:calc(var(--layout-search-h,2.5rem)+var(--layout-panel-inset,0.5rem))] [&_.gtv-info-panel-header]:pb-2 [&_.gtv-info-panel-title--hidden]:h-0 [&_.gtv-toolbar]:hidden", children: /* @__PURE__ */ jsx27(GenreTreeWheelHandoff, { skeleton: /* @__PURE__ */ jsx27(GenreTreeWheelSkeleton, {}), onRevealedChange: setIsWheelRevealed, children: /* @__PURE__ */ jsx27(
2334
+ GenrePlaylistTreeWheel,
2335
+ {
2336
+ scope,
2337
+ genrePlaylists: visibleGenrePlaylists,
2338
+ handleGenreCreationAction,
2339
+ handleGenreRenameAction,
2340
+ additionalActions,
2341
+ onSelectedNodeChange: handleSelectedNodeChange,
2342
+ hideInfoPanelClose: true,
2343
+ hideInfoPanelTitle: true,
2344
+ onNodeHover: handleNodeHover,
2345
+ renderInfoPanelBody,
2346
+ playback,
2347
+ selectedNodeId,
2348
+ readOnly,
2349
+ allowWheelRotation,
2350
+ controlsOrientation,
2351
+ sizeMetric: isArtistFiltered || !canSizeByViews ? "songs" : deferredSizeMetric,
2352
+ busy,
2353
+ floatingControlsLeading: floatingControlsLeading || canSizeByViews ? /* @__PURE__ */ jsxs20("div", { className: "flex items-center gap-2", children: [
2354
+ floatingControlsLeading,
2355
+ canSizeByViews && /* @__PURE__ */ jsx27(SizeMetricToggle, { value: sizeMetric, onChange: changeSizeMetric })
2356
+ ] }) : void 0,
2357
+ defaultViewport,
2358
+ onViewportChange,
2359
+ ...infoPanelCuration
2360
+ }
2361
+ ) }) })
2362
+ ) : viewMode === "pop-core" ? (
2363
+ // Same as the wheel: the header inset extends its fill behind the floating search bar, Google Maps style.
2364
+ /* @__PURE__ */ jsx27("div", { className: "tree-container search-overlay relative flex-1 w-full min-h-0 [--gtv-info-panel-header-inset:calc(var(--layout-search-h,2.5rem)+var(--layout-panel-inset,0.5rem))] [&_.gtv-info-panel-header]:pb-2 [&_.gtv-info-panel-title--hidden]:h-0 [&_.gtv-toolbar]:hidden [&_.gtv-info-panel-zoom-in]:hidden", children: /* @__PURE__ */ jsx27(GenreTreeWheelHandoff, { skeleton: /* @__PURE__ */ jsx27(GenreTreeWheelSkeleton, {}), onRevealedChange: setIsWheelRevealed, children: /* @__PURE__ */ jsx27(
2365
+ GenrePlaylistTreeWheelRadialPopCore,
2366
+ {
2367
+ scope,
2368
+ genrePlaylists: visibleGenrePlaylists,
2369
+ handleGenreCreationAction,
2370
+ handleGenreRenameAction,
2371
+ additionalActions,
2372
+ onSelectedNodeChange: handleSelectedNodeChange,
2373
+ hideInfoPanelClose: true,
2374
+ hideInfoPanelTitle: true,
2375
+ onNodeHover: handleNodeHover,
2376
+ renderInfoPanelBody,
2377
+ playback,
2378
+ selectedNodeId,
2379
+ readOnly,
2380
+ allowWheelRotation,
2381
+ controlsOrientation,
2382
+ sizeMetric: isArtistFiltered || !canSizeByViews ? "songs" : deferredSizeMetric,
2383
+ busy,
2384
+ floatingControlsLeading: floatingControlsLeading || canSizeByViews ? /* @__PURE__ */ jsxs20("div", { className: "flex items-center gap-2", children: [
2385
+ floatingControlsLeading,
2386
+ canSizeByViews && /* @__PURE__ */ jsx27(SizeMetricToggle, { value: sizeMetric, onChange: changeSizeMetric })
2387
+ ] }) : void 0,
2388
+ defaultViewport,
2389
+ onViewportChange,
2390
+ ...infoPanelCuration,
2391
+ ...focus
2392
+ }
2393
+ ) }) })
2394
+ ) : viewMode === "outline" ? /* @__PURE__ */ jsx27("div", { className: "tree-container flex-1 min-h-0 w-full overflow-y-auto relative [--gtv-info-panel-header-inset:calc(var(--layout-search-h,2.5rem)+var(--layout-panel-inset,0.5rem)+0.75rem)] [&_.gtv-toolbar]:hidden", children: /* @__PURE__ */ jsx27(
2395
+ GenrePlaylistTreeWheelRadialPopCore,
2016
2396
  {
2397
+ outline: true,
2017
2398
  scope,
2018
2399
  genrePlaylists: visibleGenrePlaylists,
2019
2400
  handleGenreCreationAction,
@@ -2027,100 +2408,45 @@ function GenreTreeView({
2027
2408
  playback,
2028
2409
  selectedNodeId,
2029
2410
  readOnly,
2030
- allowWheelRotation,
2031
- controlsOrientation,
2032
- sizeMetric: filterArtist ? "songs" : deferredSizeMetric,
2033
- busy,
2034
- floatingControlsLeading: filterArtist ? void 0 : /* @__PURE__ */ jsx24(SizeMetricToggle, { value: sizeMetric, onChange: changeSizeMetric }),
2035
- defaultViewport,
2036
- onViewportChange,
2037
2411
  ...infoPanelCuration
2038
2412
  }
2039
- ) }) })
2040
- ) : viewMode === "pop-core" ? (
2041
- // Same as the wheel: the header inset extends its fill behind the floating search bar, Google Maps style.
2042
- /* @__PURE__ */ jsx24("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__ */ jsx24(GenreTreeWheelHandoff, { skeleton: /* @__PURE__ */ jsx24(GenreTreeWheelSkeleton, {}), onRevealedChange: setIsWheelRevealed, children: /* @__PURE__ */ jsx24(
2043
- GenrePlaylistTreeWheelRadialPopCore,
2044
- {
2045
- scope,
2046
- genrePlaylists: visibleGenrePlaylists,
2047
- handleGenreCreationAction,
2048
- handleGenreRenameAction,
2049
- additionalActions,
2050
- onSelectedNodeChange: handleSelectedNodeChange,
2051
- hideInfoPanelClose: true,
2052
- hideInfoPanelTitle: true,
2053
- onNodeHover: handleNodeHover,
2054
- renderInfoPanelBody,
2055
- playback,
2056
- selectedNodeId,
2057
- readOnly,
2058
- allowWheelRotation,
2059
- controlsOrientation,
2060
- sizeMetric: filterArtist ? "songs" : deferredSizeMetric,
2061
- busy,
2062
- floatingControlsLeading: filterArtist ? void 0 : /* @__PURE__ */ jsx24(SizeMetricToggle, { value: sizeMetric, onChange: changeSizeMetric }),
2063
- defaultViewport,
2064
- onViewportChange,
2065
- ...infoPanelCuration,
2066
- ...focus
2413
+ ) }) : /* @__PURE__ */ jsx27("div", { className: "tree-container relative flex flex-col gap-4 w-full pt-14 overflow-x-auto overflow-y-auto text-[var(--color-text)] [&_.gtv-toolbar]:hidden", children: Object.entries(groupedGenrePlaylistsByRoot).map(
2414
+ ([uuid, genrePlaylistTreePerRoot], rootIndex) => {
2415
+ return /* @__PURE__ */ jsx27(
2416
+ "div",
2417
+ {
2418
+ className: "tree-per-root-container relative shrink-0 h-[28rem] mt-2 mr-16 p-2 bg-[var(--color-neutral-100)] rounded-lg",
2419
+ children: /* @__PURE__ */ jsx27("div", { className: "graph-container relative z-10 h-full", children: /* @__PURE__ */ jsx27(
2420
+ GenrePlaylistTreePerRoot,
2421
+ {
2422
+ scope,
2423
+ rootUuid: uuid,
2424
+ genrePlaylistTreePerRoot,
2425
+ handleGenreCreationAction,
2426
+ handleGenreRenameAction,
2427
+ additionalActions,
2428
+ onSelectedNodeChange: handleSelectedNodeChange,
2429
+ hideInfoPanelClose: true,
2430
+ hideInfoPanelTitle: true,
2431
+ onNodeHover: handleNodeHover,
2432
+ renderInfoPanelBody,
2433
+ playback,
2434
+ selectedNodeId: uuid === selectedGenrePlaylist?.root.uuid ? selectedNodeId : null,
2435
+ readOnly,
2436
+ wheelZoom: "modifier",
2437
+ controlsOrientation,
2438
+ defaultViewport: uuid === selectedGenrePlaylist?.root.uuid ? defaultViewport : void 0,
2439
+ onViewportChange: uuid === selectedGenrePlaylist?.root.uuid ? onViewportChange : void 0,
2440
+ ...infoPanelCuration,
2441
+ overlayPanel: rootIndex === 0 ? activeOverlayPanel : void 0
2442
+ }
2443
+ ) })
2444
+ },
2445
+ uuid
2446
+ );
2067
2447
  }
2068
- ) }) })
2069
- ) : viewMode === "outline" ? /* @__PURE__ */ jsx24("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__ */ jsx24(
2070
- GenrePlaylistTreeWheelRadialPopCore,
2071
- {
2072
- outline: true,
2073
- scope,
2074
- genrePlaylists: visibleGenrePlaylists,
2075
- handleGenreCreationAction,
2076
- handleGenreRenameAction,
2077
- additionalActions,
2078
- onSelectedNodeChange: handleSelectedNodeChange,
2079
- hideInfoPanelClose: true,
2080
- hideInfoPanelTitle: true,
2081
- onNodeHover: handleNodeHover,
2082
- renderInfoPanelBody,
2083
- playback,
2084
- selectedNodeId,
2085
- readOnly,
2086
- ...infoPanelCuration
2087
- }
2088
- ) }) : /* @__PURE__ */ jsx24("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(
2089
- ([uuid, genrePlaylistTreePerRoot], rootIndex) => {
2090
- return /* @__PURE__ */ jsx24(
2091
- "div",
2092
- {
2093
- className: "tree-per-root-container relative shrink-0 h-[28rem] mt-2 mr-16 p-2 bg-[var(--color-neutral-100)] rounded-lg",
2094
- children: /* @__PURE__ */ jsx24("div", { className: "graph-container relative z-10 h-full", children: /* @__PURE__ */ jsx24(
2095
- GenrePlaylistTreePerRoot,
2096
- {
2097
- scope,
2098
- rootUuid: uuid,
2099
- genrePlaylistTreePerRoot,
2100
- handleGenreCreationAction,
2101
- handleGenreRenameAction,
2102
- additionalActions,
2103
- onSelectedNodeChange: handleSelectedNodeChange,
2104
- hideInfoPanelClose: true,
2105
- hideInfoPanelTitle: true,
2106
- onNodeHover: handleNodeHover,
2107
- renderInfoPanelBody,
2108
- playback,
2109
- selectedNodeId: uuid === selectedGenrePlaylist?.root.uuid ? selectedNodeId : null,
2110
- readOnly,
2111
- wheelZoom: "modifier",
2112
- controlsOrientation,
2113
- defaultViewport: uuid === selectedGenrePlaylist?.root.uuid ? defaultViewport : void 0,
2114
- onViewportChange: uuid === selectedGenrePlaylist?.root.uuid ? onViewportChange : void 0,
2115
- ...infoPanelCuration,
2116
- overlayPanel: rootIndex === 0 ? activeOverlayPanel : void 0
2117
- }
2118
- ) })
2119
- },
2120
- uuid
2121
- );
2122
- }
2123
- ) }) }) })
2448
+ ) })
2449
+ ] }) })
2124
2450
  ]
2125
2451
  }
2126
2452
  );
@@ -2135,24 +2461,35 @@ import {
2135
2461
  } from "@behindthemusictree/genre-tree-view";
2136
2462
 
2137
2463
  // src/genre-tree/track-list-sidebar/TrackListSidebar.tsx
2138
- import { useEffect as useEffect5, useRef as useRef5 } from "react";
2464
+ import { useEffect as useEffect6, useLayoutEffect as useLayoutEffect2, useRef as useRef5 } from "react";
2465
+ import { ListMusic } from "lucide-react";
2139
2466
  import { useTrackList as useTrackList5 } from "@behindthemusictree/app-kit/genre-tree";
2140
- import { useTrackListSidebarVisibility } from "@behindthemusictree/app-kit/genre-tree";
2141
2467
  import { TrackListOriginType as TrackListOriginType2 } from "@behindthemusictree/app-kit/genre-tree";
2142
2468
  import { useTrackNoun as useTrackNoun6 } from "@behindthemusictree/app-kit/track-noun";
2143
- import { jsx as jsx25, jsxs as jsxs18 } from "react/jsx-runtime";
2469
+ import { jsx as jsx28, jsxs as jsxs21 } from "react/jsx-runtime";
2470
+ var useBrowserLayoutEffect = typeof window === "undefined" ? useEffect6 : useLayoutEffect2;
2144
2471
  function TrackListSidebar({
2145
- className = "",
2472
+ className: className2 = "",
2146
2473
  renderDuration,
2147
2474
  renderActions,
2148
2475
  layout = "fixed",
2476
+ isCollapsed = false,
2477
+ onExpand,
2478
+ onMinimize,
2149
2479
  renderArtistLink
2150
2480
  }) {
2151
- const { trackList, loadMore } = useTrackList5();
2481
+ const { trackList, loadMore, autoAdvanceScrollRequest } = useTrackList5();
2152
2482
  const trackNoun = useTrackNoun6();
2483
+ const trackListRef = useRef5(null);
2484
+ const previousOriginRef = useRef5(trackList?.origin ?? null);
2485
+ const handledAutoAdvanceRequestIdRef = useRef5(null);
2486
+ const shouldFollowPlaybackRef = useRef5(true);
2487
+ const programmaticScrollTopRef = useRef5(null);
2488
+ const userScrollIntentRef = useRef5(false);
2489
+ const userScrollIntentTimerRef = useRef5(null);
2153
2490
  const loadMoreSentinelRef = useRef5(null);
2154
2491
  const hasMore = !!trackList && trackList.nextPage !== null;
2155
- useEffect5(() => {
2492
+ useEffect6(() => {
2156
2493
  const sentinel = loadMoreSentinelRef.current;
2157
2494
  if (!hasMore || !sentinel) return;
2158
2495
  const observer = new IntersectionObserver((entries) => {
@@ -2163,44 +2500,115 @@ function TrackListSidebar({
2163
2500
  observer.observe(sentinel);
2164
2501
  return () => observer.disconnect();
2165
2502
  }, [hasMore, loadMore]);
2166
- const { hideTrackListSidebar } = useTrackListSidebarVisibility();
2167
- const positionClasses = layout === "inline" ? "relative w-full flex h-full flex-col" : "fixed right-0 w-[50rem]";
2168
- return trackList ? /* @__PURE__ */ jsxs18(
2503
+ useBrowserLayoutEffect(() => {
2504
+ const origin = trackList?.origin ?? null;
2505
+ if (previousOriginRef.current === origin) return;
2506
+ previousOriginRef.current = origin;
2507
+ handledAutoAdvanceRequestIdRef.current = null;
2508
+ shouldFollowPlaybackRef.current = true;
2509
+ programmaticScrollTopRef.current = null;
2510
+ if (trackListRef.current) trackListRef.current.scrollTop = 0;
2511
+ }, [trackList?.origin]);
2512
+ useEffect6(() => {
2513
+ const list = trackListRef.current;
2514
+ if (!isCollapsed && list?.scrollTop === 0) shouldFollowPlaybackRef.current = true;
2515
+ }, [isCollapsed]);
2516
+ useEffect6(
2517
+ () => () => {
2518
+ if (userScrollIntentTimerRef.current !== null) clearTimeout(userScrollIntentTimerRef.current);
2519
+ },
2520
+ []
2521
+ );
2522
+ useBrowserLayoutEffect(() => {
2523
+ if (!autoAdvanceScrollRequest || handledAutoAdvanceRequestIdRef.current === autoAdvanceScrollRequest.id) {
2524
+ return;
2525
+ }
2526
+ if (isCollapsed) return;
2527
+ const list = trackListRef.current;
2528
+ if (!list) return;
2529
+ if (list.scrollTop <= 1) shouldFollowPlaybackRef.current = true;
2530
+ if (!shouldFollowPlaybackRef.current) {
2531
+ handledAutoAdvanceRequestIdRef.current = autoAdvanceScrollRequest.id;
2532
+ return;
2533
+ }
2534
+ const row = Array.from(list.querySelectorAll("[data-track-uuid]")).find(
2535
+ (element) => element.dataset.trackUuid === autoAdvanceScrollRequest.trackUuid
2536
+ );
2537
+ if (!row) return;
2538
+ handledAutoAdvanceRequestIdRef.current = autoAdvanceScrollRequest.id;
2539
+ const rowTop = row.getBoundingClientRect().top - list.getBoundingClientRect().top;
2540
+ const targetScrollTop = list.scrollTop + rowTop;
2541
+ programmaticScrollTopRef.current = targetScrollTop;
2542
+ list.scrollTop = targetScrollTop;
2543
+ programmaticScrollTopRef.current = list.scrollTop;
2544
+ }, [autoAdvanceScrollRequest, isCollapsed, trackList]);
2545
+ const markUserScrollIntent = () => {
2546
+ userScrollIntentRef.current = true;
2547
+ if (userScrollIntentTimerRef.current !== null) clearTimeout(userScrollIntentTimerRef.current);
2548
+ userScrollIntentTimerRef.current = setTimeout(() => {
2549
+ userScrollIntentRef.current = false;
2550
+ userScrollIntentTimerRef.current = null;
2551
+ }, 250);
2552
+ };
2553
+ const handleTrackListScroll = (event) => {
2554
+ const list = event.currentTarget;
2555
+ if (userScrollIntentRef.current) {
2556
+ userScrollIntentRef.current = false;
2557
+ if (userScrollIntentTimerRef.current !== null) clearTimeout(userScrollIntentTimerRef.current);
2558
+ userScrollIntentTimerRef.current = null;
2559
+ programmaticScrollTopRef.current = null;
2560
+ shouldFollowPlaybackRef.current = list.scrollTop <= 1;
2561
+ return;
2562
+ }
2563
+ const programmaticScrollTop = programmaticScrollTopRef.current;
2564
+ if (programmaticScrollTop !== null) {
2565
+ if (Math.abs(list.scrollTop - programmaticScrollTop) <= 1) programmaticScrollTopRef.current = null;
2566
+ return;
2567
+ }
2568
+ };
2569
+ const positionClasses = layout === "inline" ? "relative w-full flex h-full flex-col" : "fixed right-0 w-[50rem] flex flex-col";
2570
+ return trackList ? /* @__PURE__ */ jsxs21(
2169
2571
  "div",
2170
2572
  {
2171
2573
  "data-theme": "dark",
2172
- className: `track-list-sidebar ${positionClasses} rounded-2xl bg-[var(--color-surface)] pb-1 ${className}`,
2173
- style: layout === "fixed" ? {
2174
- bottom: "79px"
2175
- /* Match player height exactly */
2176
- } : void 0,
2574
+ className: `track-list-sidebar ${positionClasses} rounded-2xl bg-[var(--color-surface)] pb-1 ${className2}`,
2575
+ style: layout === "fixed" ? { top: "63.5px", maxHeight: "calc(100vh - 63.5px - 79px)" } : void 0,
2177
2576
  children: [
2178
- /* @__PURE__ */ jsxs18("div", { className: "header flex h-16 px-4 py-2 text-[var(--color-text)]", children: [
2179
- /* @__PURE__ */ jsxs18("div", { className: "origin flex text-xl ", children: [
2180
- /* @__PURE__ */ jsx25("div", { className: "from h-auto flex flex-col justify-center items-center mr-2", children: "From" }),
2181
- /* @__PURE__ */ jsx25("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__ */ jsx25("div", { className: "name max-w-full truncate", children: trackList.origin.label }) })
2577
+ /* @__PURE__ */ jsxs21("div", { className: "header flex h-16 items-center px-4 py-2 text-[var(--color-text)]", children: [
2578
+ /* @__PURE__ */ jsxs21("div", { className: "min-w-0 flex flex-1 flex-col justify-center", children: [
2579
+ /* @__PURE__ */ jsxs21("div", { className: "origin flex text-xl", children: [
2580
+ /* @__PURE__ */ jsx28("div", { className: "from h-auto flex flex-col justify-center items-center mr-2", children: "From" }),
2581
+ /* @__PURE__ */ jsx28("div", { className: "name-container flex flex-col justify-center items-center text-[var(--color-text)] font-bold pr-2 max-w-[27.5rem]", children: /* @__PURE__ */ jsx28("div", { className: "name max-w-full truncate", children: trackList.origin.label }) })
2582
+ ] }),
2583
+ /* @__PURE__ */ jsxs21("div", { className: "info flex items-center text-sm text-[var(--color-text-muted)]", children: [
2584
+ trackList.origin.type === TrackListOriginType2.GENRE_PLAYLIST ? "Genre \u2022 " : trackList.origin.type === TrackListOriginType2.ARTIST ? "Artist \u2022 " : `${trackNoun.singular} playlist \u2022 `,
2585
+ `${trackList.total} ${trackList.total > 1 ? trackNoun.plural : trackNoun.singular}`
2586
+ ] })
2182
2587
  ] }),
2183
- /* @__PURE__ */ jsxs18("div", { className: "info flex flex-col justify-center items-center pt-1 mr-2 text-[var(--color-text-muted)]", children: [
2184
- trackList.origin.type === TrackListOriginType2.GENRE_PLAYLIST ? "\u2022 Genre playlist \u2022 " : trackList.origin.type === TrackListOriginType2.ARTIST ? "\u2022 Artist \u2022 " : `\u2022 ${trackNoun.singular} playlist \u2022 `,
2185
- `${trackList.total} ${trackList.total > 1 ? trackNoun.plural : trackNoun.singular} \u2022`
2186
- ] }),
2187
- /* @__PURE__ */ jsx25(
2588
+ (isCollapsed && onExpand || !isCollapsed && onMinimize) && /* @__PURE__ */ jsx28(
2188
2589
  "button",
2189
2590
  {
2190
2591
  type: "button",
2191
- "aria-label": "Close track list",
2192
- className: "flex-grow flex flex-col items-end justify-center h-full p-3 cursor-pointer text-inherit",
2193
- onClick: hideTrackListSidebar,
2194
- children: "\u2715"
2592
+ "aria-label": isCollapsed ? "Show tracks" : "Hide tracks",
2593
+ title: isCollapsed ? "Show tracks" : "Hide tracks",
2594
+ "aria-expanded": !isCollapsed,
2595
+ className: `ml-2 flex h-8 w-8 shrink-0 items-center justify-center rounded-md hover:bg-[var(--color-surface-muted)] ${isCollapsed ? "text-[var(--color-text-muted)]" : "text-white"}`,
2596
+ onClick: isCollapsed ? onExpand : onMinimize,
2597
+ children: /* @__PURE__ */ jsx28(ListMusic, { "aria-hidden": "true", className: "h-5 w-5" })
2195
2598
  }
2196
2599
  )
2197
2600
  ] }),
2198
- /* @__PURE__ */ jsxs18(
2601
+ !isCollapsed && /* @__PURE__ */ jsxs21(
2199
2602
  "ul",
2200
2603
  {
2201
- 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",
2604
+ ref: trackListRef,
2605
+ onScroll: handleTrackListScroll,
2606
+ onWheel: markUserScrollIntent,
2607
+ onTouchMove: markUserScrollIntent,
2608
+ onPointerDown: markUserScrollIntent,
2609
+ className: layout === "inline" ? "track-list flex-1 min-h-0 overflow-auto list-none p-0 m-0" : "track-list flex-1 min-h-0 overflow-auto list-none p-0 m-0",
2202
2610
  children: [
2203
- trackList.tracks.map((track, index) => /* @__PURE__ */ jsx25("li", { children: /* @__PURE__ */ jsx25(
2611
+ trackList.tracks.map((track, index) => /* @__PURE__ */ jsx28("li", { "data-track-uuid": track.uuid, children: /* @__PURE__ */ jsx28(
2204
2612
  TrackItem,
2205
2613
  {
2206
2614
  track,
@@ -2210,7 +2618,7 @@ function TrackListSidebar({
2210
2618
  renderArtistLink
2211
2619
  }
2212
2620
  ) }, track.uuid)),
2213
- hasMore && /* @__PURE__ */ jsx25("li", { ref: loadMoreSentinelRef, "aria-hidden": "true", className: "h-px" })
2621
+ hasMore && /* @__PURE__ */ jsx28("li", { ref: loadMoreSentinelRef, "aria-hidden": "true", className: "h-px" })
2214
2622
  ]
2215
2623
  }
2216
2624
  )
@@ -2220,47 +2628,167 @@ function TrackListSidebar({
2220
2628
  }
2221
2629
 
2222
2630
  // src/genre-tree/PlayerPanel.tsx
2223
- import { useState as useState5 } from "react";
2224
- import { X as X2 } from "lucide-react";
2631
+ import { useEffect as useEffect7, useState as useState6 } from "react";
2225
2632
  import { twMerge as twMerge2 } from "tailwind-merge";
2226
2633
  import {
2227
- usePlayer as usePlayer9,
2634
+ usePlayer as usePlayer10,
2228
2635
  PlayerVideoSurface
2229
2636
  } from "@behindthemusictree/app-kit/player";
2230
2637
  import {
2231
- useTrackListSidebarVisibility as useTrackListSidebarVisibility2
2638
+ useTrackListSidebarVisibility
2232
2639
  } from "@behindthemusictree/app-kit/genre-tree";
2233
- import { jsx as jsx26, jsxs as jsxs19 } from "react/jsx-runtime";
2640
+
2641
+ // src/genre-tree/PlayerControls.tsx
2642
+ import { Pause as Pause2, Play as Play3, SkipBack, SkipForward, Volume2, VolumeX } from "lucide-react";
2643
+ import { formatTime, useTrackList as useTrackList6 } from "@behindthemusictree/app-kit/genre-tree";
2644
+ import { PlayStates as PlayStates3, useCurrentTime, usePlayer as usePlayer9 } from "@behindthemusictree/app-kit/player";
2645
+ import { jsx as jsx29, jsxs as jsxs22 } from "react/jsx-runtime";
2646
+ function PlayerControls() {
2647
+ const {
2648
+ duration,
2649
+ handlePlayPauseAction,
2650
+ isLoading,
2651
+ playState,
2652
+ playerTrackObject,
2653
+ seekTo,
2654
+ setVolume,
2655
+ volume
2656
+ } = usePlayer9();
2657
+ const { hasNextTrack, hasPreviousTrack, playNextTrack, playPreviousTrack } = useTrackList6();
2658
+ const currentTime = useCurrentTime();
2659
+ const isBusy = isLoading || playState === PlayStates3.LOADING;
2660
+ const isReady = !!playerTrackObject?.isReady && !isBusy && !playerTrackObject.loadError;
2661
+ const safeDuration = Number.isFinite(duration) && duration > 0 ? duration : 0;
2662
+ const safeCurrentTime = Number.isFinite(currentTime) ? Math.max(0, Math.min(currentTime, safeDuration)) : 0;
2663
+ const isPlaying = playState === PlayStates3.PLAYING;
2664
+ return /* @__PURE__ */ jsxs22(
2665
+ "section",
2666
+ {
2667
+ "aria-label": "Player controls",
2668
+ className: "player-controls flex shrink-0 flex-col gap-2 border-b border-zinc-800 px-3 py-2 text-zinc-200",
2669
+ children: [
2670
+ /* @__PURE__ */ jsxs22("div", { className: "flex min-w-0 items-center gap-2 text-xs tabular-nums text-zinc-400", children: [
2671
+ /* @__PURE__ */ jsx29("span", { "aria-label": "Elapsed time", children: formatTime(safeCurrentTime) }),
2672
+ /* @__PURE__ */ jsx29(
2673
+ "input",
2674
+ {
2675
+ "aria-label": "Seek",
2676
+ className: "h-1 min-w-0 flex-1 cursor-pointer accent-white disabled:cursor-not-allowed disabled:opacity-40",
2677
+ type: "range",
2678
+ min: 0,
2679
+ max: safeDuration,
2680
+ step: 0.1,
2681
+ value: safeCurrentTime,
2682
+ disabled: !isReady || safeDuration <= 0,
2683
+ onChange: (event) => seekTo(Number(event.currentTarget.value))
2684
+ }
2685
+ ),
2686
+ /* @__PURE__ */ jsx29("span", { "aria-label": "Duration", children: formatTime(safeDuration) })
2687
+ ] }),
2688
+ /* @__PURE__ */ jsxs22("div", { className: "grid min-h-9 grid-cols-[1fr_auto_1fr] items-center gap-3", children: [
2689
+ /* @__PURE__ */ jsxs22("div", { className: "col-start-2 flex items-center justify-center gap-2", children: [
2690
+ /* @__PURE__ */ jsx29(
2691
+ "button",
2692
+ {
2693
+ type: "button",
2694
+ "aria-label": "Previous track",
2695
+ title: "Previous track",
2696
+ disabled: !isReady || !hasPreviousTrack,
2697
+ onClick: playPreviousTrack,
2698
+ className: "flex h-8 w-8 shrink-0 items-center justify-center rounded-full text-zinc-300 hover:bg-zinc-800 hover:text-white disabled:cursor-not-allowed disabled:opacity-40",
2699
+ children: /* @__PURE__ */ jsx29(SkipBack, { "aria-hidden": "true", className: "h-4 w-4" })
2700
+ }
2701
+ ),
2702
+ /* @__PURE__ */ jsx29(
2703
+ "button",
2704
+ {
2705
+ type: "button",
2706
+ "aria-label": isPlaying ? "Pause" : "Play",
2707
+ title: isPlaying ? "Pause" : "Play",
2708
+ disabled: !isReady,
2709
+ onClick: handlePlayPauseAction,
2710
+ className: "flex h-9 w-9 shrink-0 items-center justify-center rounded-full bg-white text-zinc-950 hover:bg-zinc-200 disabled:cursor-not-allowed disabled:opacity-40",
2711
+ children: isPlaying ? /* @__PURE__ */ jsx29(Pause2, { "aria-hidden": "true", className: "h-4 w-4", fill: "currentColor" }) : /* @__PURE__ */ jsx29(Play3, { "aria-hidden": "true", className: "h-4 w-4", fill: "currentColor" })
2712
+ }
2713
+ ),
2714
+ /* @__PURE__ */ jsx29(
2715
+ "button",
2716
+ {
2717
+ type: "button",
2718
+ "aria-label": "Next track",
2719
+ title: "Next track",
2720
+ disabled: !isReady || !hasNextTrack,
2721
+ onClick: playNextTrack,
2722
+ className: "flex h-8 w-8 shrink-0 items-center justify-center rounded-full text-zinc-300 hover:bg-zinc-800 hover:text-white disabled:cursor-not-allowed disabled:opacity-40",
2723
+ children: /* @__PURE__ */ jsx29(SkipForward, { "aria-hidden": "true", className: "h-4 w-4" })
2724
+ }
2725
+ )
2726
+ ] }),
2727
+ /* @__PURE__ */ jsxs22("label", { className: "col-start-3 flex w-[4.2rem] shrink-0 items-center gap-[0.35rem] justify-self-end text-zinc-400 sm:w-[4.9rem]", children: [
2728
+ volume === 0 ? /* @__PURE__ */ jsx29(VolumeX, { "aria-hidden": "true", className: "h-[0.7rem] w-[0.7rem] shrink-0" }) : /* @__PURE__ */ jsx29(Volume2, { "aria-hidden": "true", className: "h-[0.7rem] w-[0.7rem] shrink-0" }),
2729
+ /* @__PURE__ */ jsx29("span", { className: "sr-only", children: "Volume" }),
2730
+ /* @__PURE__ */ jsx29(
2731
+ "input",
2732
+ {
2733
+ "aria-label": "Volume",
2734
+ className: "h-[3px] min-w-0 flex-1 cursor-pointer accent-white disabled:cursor-not-allowed disabled:opacity-40",
2735
+ type: "range",
2736
+ min: 0,
2737
+ max: 100,
2738
+ step: 1,
2739
+ value: volume,
2740
+ disabled: !isReady,
2741
+ onChange: (event) => setVolume(Number(event.currentTarget.value))
2742
+ }
2743
+ )
2744
+ ] })
2745
+ ] })
2746
+ ]
2747
+ }
2748
+ );
2749
+ }
2750
+
2751
+ // src/genre-tree/PlayerPanel.tsx
2752
+ import { jsx as jsx30, jsxs as jsxs23 } from "react/jsx-runtime";
2234
2753
  var PANEL_ID = "player-panel";
2235
- var iconButtonClassName = "flex h-6 w-6 shrink-0 items-center justify-center p-0 rounded-md hover:bg-white/10";
2236
2754
  var DEFAULT_EXTENDED_WIDTH = "calc(100vw - var(--layout-center-left) - var(--layout-gutter))";
2237
2755
  function PlayerPanel({
2238
- className,
2756
+ className: className2,
2239
2757
  isVisible = true,
2240
- onClose,
2241
2758
  expanded,
2242
2759
  onExpandedChange,
2243
2760
  extendedWidth = DEFAULT_EXTENDED_WIDTH,
2244
2761
  renderArtistLink
2245
2762
  }) {
2246
- const { playerTrackObject } = usePlayer9();
2247
- const { toggleTrackListSidebar, isTrackListSidebarVisible } = useTrackListSidebarVisibility2();
2248
- const [uncontrolledExtended, setUncontrolledExtended] = useState5(false);
2763
+ const { playerTrackObject } = usePlayer10();
2764
+ const { toggleTrackListSidebar, isTrackListSidebarVisible } = useTrackListSidebarVisibility();
2765
+ const [lastLoadedTrackTitle, setLastLoadedTrackTitle] = useState6("");
2766
+ const loadedTrackTitle = playerTrackObject?.track.title;
2767
+ useEffect7(() => {
2768
+ if (loadedTrackTitle) setLastLoadedTrackTitle(loadedTrackTitle);
2769
+ }, [loadedTrackTitle]);
2770
+ const [uncontrolledExtended, setUncontrolledExtended] = useState6(false);
2249
2771
  const isExtendRequested = expanded ?? uncontrolledExtended;
2250
2772
  const setIsExtendRequested = (next) => {
2251
2773
  setUncontrolledExtended(next);
2252
2774
  onExpandedChange?.(next);
2253
2775
  };
2254
2776
  const isExtended = isExtendRequested && isTrackListSidebarVisible;
2255
- const tabEdge = !isTrackListSidebarVisible ? null : isExtended ? {
2256
- label: "Shrink song list panel",
2257
- onClick: () => setIsExtendRequested(false)
2258
- } : { label: "Hide song list", onClick: toggleTrackListSidebar };
2259
- const tabCenter = !isTrackListSidebarVisible ? { label: "Show song list", onClick: toggleTrackListSidebar } : isExtended ? null : {
2260
- label: "Extend song list panel",
2261
- onClick: () => setIsExtendRequested(true)
2777
+ const hideSongList = () => {
2778
+ setIsExtendRequested(false);
2779
+ if (isTrackListSidebarVisible) toggleTrackListSidebar();
2262
2780
  };
2263
- return /* @__PURE__ */ jsxs19(
2781
+ const tabEdge = isTrackListSidebarVisible && isExtended ? {
2782
+ label: "Shrink player panel",
2783
+ onClick: () => setIsExtendRequested(false),
2784
+ expanded: true
2785
+ } : null;
2786
+ const tabCenter = isTrackListSidebarVisible && !isExtended ? {
2787
+ label: "Extend player panel",
2788
+ onClick: () => setIsExtendRequested(true),
2789
+ expanded: false
2790
+ } : null;
2791
+ return /* @__PURE__ */ jsxs23(
2264
2792
  "div",
2265
2793
  {
2266
2794
  id: PANEL_ID,
@@ -2269,11 +2797,11 @@ function PlayerPanel({
2269
2797
  !isExtended && "w-[var(--layout-right-w)]",
2270
2798
  isTrackListSidebarVisible && "h-[calc(100vh-var(--layout-band-top)-3.5rem)]",
2271
2799
  !isVisible && "hidden",
2272
- className
2800
+ className2
2273
2801
  ),
2274
2802
  style: isExtended ? { width: extendedWidth } : void 0,
2275
2803
  children: [
2276
- /* @__PURE__ */ jsx26(
2804
+ /* @__PURE__ */ jsx30(
2277
2805
  PanelEdgeTab,
2278
2806
  {
2279
2807
  side: "right",
@@ -2283,7 +2811,7 @@ function PlayerPanel({
2283
2811
  className: "absolute right-full top-1/2 -translate-y-1/2"
2284
2812
  }
2285
2813
  ),
2286
- /* @__PURE__ */ jsxs19(
2814
+ /* @__PURE__ */ jsxs23(
2287
2815
  "div",
2288
2816
  {
2289
2817
  className: twMerge2(
@@ -2291,29 +2819,21 @@ function PlayerPanel({
2291
2819
  isExtended ? "flex-row" : "flex-col"
2292
2820
  ),
2293
2821
  children: [
2294
- /* @__PURE__ */ jsxs19("div", { className: "flex w-[var(--layout-right-w)] shrink-0 flex-col", children: [
2295
- /* @__PURE__ */ jsxs19("div", { className: "flex shrink-0 items-center justify-between gap-2 border-b border-zinc-800 p-2", children: [
2296
- /* @__PURE__ */ jsx26("span", { className: "min-w-0 truncate text-sm font-semibold", children: playerTrackObject?.track?.title }),
2297
- /* @__PURE__ */ jsx26("div", { className: "flex shrink-0 items-center gap-1", children: /* @__PURE__ */ jsx26(
2298
- "button",
2299
- {
2300
- type: "button",
2301
- onClick: onClose,
2302
- className: iconButtonClassName,
2303
- "aria-label": "Close player",
2304
- children: /* @__PURE__ */ jsx26(X2, { size: 16 })
2305
- }
2306
- ) })
2822
+ /* @__PURE__ */ jsxs23("div", { className: "flex w-[var(--layout-right-w)] shrink-0 flex-col", children: [
2823
+ /* @__PURE__ */ jsx30("div", { className: "flex shrink-0 items-center justify-between gap-2 border-b border-zinc-800 p-2", children: /* @__PURE__ */ jsx30("span", { className: "min-w-0 truncate text-sm font-semibold", children: loadedTrackTitle || lastLoadedTrackTitle }) }),
2824
+ /* @__PURE__ */ jsxs23("div", { className: "relative w-full shrink-0", children: [
2825
+ /* @__PURE__ */ jsx30(PlayerVideoSurface, { className: "aspect-video w-full bg-black" }),
2826
+ playerTrackObject?.loadError && /* @__PURE__ */ jsx30("span", { className: "absolute bottom-2 left-2 right-2 text-xs text-red-400", children: playerTrackObject.loadError })
2307
2827
  ] }),
2308
- /* @__PURE__ */ jsxs19("div", { className: "relative w-full shrink-0", children: [
2309
- /* @__PURE__ */ jsx26(PlayerVideoSurface, { className: "aspect-video w-full bg-black" }),
2310
- playerTrackObject?.loadError && /* @__PURE__ */ jsx26("span", { className: "absolute bottom-2 left-2 right-2 text-xs text-red-400", children: playerTrackObject.loadError })
2311
- ] })
2828
+ /* @__PURE__ */ jsx30(PlayerControls, {})
2312
2829
  ] }),
2313
- isTrackListSidebarVisible && /* @__PURE__ */ jsx26(
2830
+ /* @__PURE__ */ jsx30(
2314
2831
  TrackListSidebar,
2315
2832
  {
2316
2833
  layout: "inline",
2834
+ isCollapsed: !isTrackListSidebarVisible,
2835
+ onExpand: toggleTrackListSidebar,
2836
+ onMinimize: hideSongList,
2317
2837
  className: twMerge2(
2318
2838
  "min-h-0 min-w-0 flex-1",
2319
2839
  isExtended && "border-l border-zinc-800"
@@ -2333,11 +2853,11 @@ function PlayerPanel({
2333
2853
  import { v4 as uuidv4 } from "uuid";
2334
2854
  import { AiFillStar, AiOutlineStar } from "react-icons/ai";
2335
2855
  import { FORM_RATING_NULL_VALUE } from "@behindthemusictree/app-kit/genre-tree";
2336
- import { jsx as jsx27, jsxs as jsxs20 } from "react/jsx-runtime";
2856
+ import { jsx as jsx31, jsxs as jsxs24 } from "react/jsx-runtime";
2337
2857
  function Rating({ rating, handleChange }) {
2338
2858
  const instanceId = uuidv4();
2339
- return /* @__PURE__ */ jsx27("div", { className: "flex items-center", children: [...Array(6)].map((_, i) => /* @__PURE__ */ jsxs20("div", { className: `relative ${i === 0 ? "mr-4" : "mr-1"}`, children: [
2340
- /* @__PURE__ */ jsx27(
2859
+ return /* @__PURE__ */ jsx31("div", { className: "flex items-center", children: [...Array(6)].map((_, i) => /* @__PURE__ */ jsxs24("div", { className: `relative ${i === 0 ? "mr-4" : "mr-1"}`, children: [
2860
+ /* @__PURE__ */ jsx31(
2341
2861
  "input",
2342
2862
  {
2343
2863
  id: `star-${instanceId}-${i}`,
@@ -2349,16 +2869,16 @@ function Rating({ rating, handleChange }) {
2349
2869
  onChange: handleChange
2350
2870
  }
2351
2871
  ),
2352
- /* @__PURE__ */ jsx27("label", { htmlFor: `star-${instanceId}-${i}`, className: "cursor-pointer", children: i === 0 ? /* @__PURE__ */ jsxs20("div", { className: "flex", children: [
2353
- /* @__PURE__ */ jsx27(
2872
+ /* @__PURE__ */ jsx31("label", { htmlFor: `star-${instanceId}-${i}`, className: "cursor-pointer", children: i === 0 ? /* @__PURE__ */ jsxs24("div", { className: "flex", children: [
2873
+ /* @__PURE__ */ jsx31(
2354
2874
  AiOutlineStar,
2355
2875
  {
2356
2876
  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)]"}`
2357
2877
  }
2358
2878
  ),
2359
- /* @__PURE__ */ jsx27("span", { className: "flex-grow w-8" }),
2879
+ /* @__PURE__ */ jsx31("span", { className: "flex-grow w-8" }),
2360
2880
  " "
2361
- ] }) : /* @__PURE__ */ jsx27(
2881
+ ] }) : /* @__PURE__ */ jsx31(
2362
2882
  AiFillStar,
2363
2883
  {
2364
2884
  className: `w-7 h-7 absolute top-0 left-0 ${(rating ?? -1) >= i ? "text-[var(--color-accent-rating)]" : "text-[var(--color-text-muted)]"}`
@@ -2382,4 +2902,4 @@ export {
2382
2902
  GenreTreeViewSkeleton2 as GenreTreeViewSkeleton,
2383
2903
  GenreTreeOutlineSkeleton
2384
2904
  };
2385
- //# sourceMappingURL=chunk-KPWV4LEJ.js.map
2905
+ //# sourceMappingURL=chunk-5PQWJRDZ.js.map