@behindthemusictree/app-kit-ui 26.1.0 → 29.0.0

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