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