@behindthemusictree/app-kit-ui 40.1.0 → 41.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.
@@ -15,6 +15,9 @@ import {
15
15
  GenreTreeViewSkeleton,
16
16
  useGenreNodeRelations
17
17
  } from "@behindthemusictree/genre-tree-view";
18
+ import {
19
+ toGenreUuid
20
+ } from "@behindthemusictree/app-kit/genre-tree";
18
21
  import { useGenreTreeViewState, useArtist as useArtist2, useUpdateGenre as useUpdateGenre2, genreUpdateMutationKey } from "@behindthemusictree/app-kit/genre-tree";
19
22
  import { usePopup as usePopup4 } from "@behindthemusictree/app-kit/popup";
20
23
 
@@ -1160,6 +1163,29 @@ function ClampedText({ children, className: className2 = "" }) {
1160
1163
  ] });
1161
1164
  }
1162
1165
 
1166
+ // src/genre-tree/CommonsPhoto.tsx
1167
+ import { jsx as jsx20, jsxs as jsxs12 } from "react/jsx-runtime";
1168
+ function ExternalLink({ href, children }) {
1169
+ return /* @__PURE__ */ jsx20("a", { href, target: "_blank", rel: "noopener noreferrer", className: "underline-offset-2 hover:underline", children });
1170
+ }
1171
+ function CommonsPhoto({
1172
+ image,
1173
+ alt,
1174
+ creditLabel = "Photo"
1175
+ }) {
1176
+ return /* @__PURE__ */ jsxs12("figure", { className: "flex flex-col gap-1.5", children: [
1177
+ /* @__PURE__ */ jsx20("img", { src: image.url, alt, className: "h-64 w-full object-contain bg-[var(--color-surface-muted)] rounded" }),
1178
+ /* @__PURE__ */ jsxs12("figcaption", { className: "text-caption text-[var(--color-text-muted)]", children: [
1179
+ creditLabel,
1180
+ ": ",
1181
+ /* @__PURE__ */ jsx20(ExternalLink, { href: image.pageUrl, children: image.author ?? "Wikimedia Commons" }),
1182
+ ",",
1183
+ " ",
1184
+ image.licenseUrl ? /* @__PURE__ */ jsx20(ExternalLink, { href: image.licenseUrl, children: image.license }) : image.license
1185
+ ] })
1186
+ ] });
1187
+ }
1188
+
1163
1189
  // src/genre-tree/GenrePlaylistTracks.tsx
1164
1190
  import { useEffect as useEffect4, useState as useState7 } from "react";
1165
1191
  import { useInfiniteQuery } from "@tanstack/react-query";
@@ -1182,15 +1208,15 @@ import { TracksFailureAlreadyReportedError as TracksFailureAlreadyReportedError2
1182
1208
 
1183
1209
  // src/genre-tree/MusicBrainzRecordingLink.tsx
1184
1210
  import { musicBrainzRecordingUrl } from "@behindthemusictree/app-kit/genre-tree";
1185
- import { Fragment as Fragment8, jsx as jsx20 } from "react/jsx-runtime";
1211
+ import { Fragment as Fragment8, jsx as jsx21 } from "react/jsx-runtime";
1186
1212
  function MusicBrainzRecordingLink({
1187
1213
  song,
1188
1214
  children,
1189
1215
  className: className2 = ""
1190
1216
  }) {
1191
1217
  const url = musicBrainzRecordingUrl(song);
1192
- if (!url) return /* @__PURE__ */ jsx20(Fragment8, { children });
1193
- return /* @__PURE__ */ jsx20(
1218
+ if (!url) return /* @__PURE__ */ jsx21(Fragment8, { children });
1219
+ return /* @__PURE__ */ jsx21(
1194
1220
  "a",
1195
1221
  {
1196
1222
  href: url,
@@ -1207,7 +1233,7 @@ function MusicBrainzRecordingLink({
1207
1233
  // src/genre-tree/GenrePlaylistTracks.tsx
1208
1234
  import { ArtistNames } from "@behindthemusictree/app-kit/genre-tree";
1209
1235
  import { capitalize, useTrackNoun } from "@behindthemusictree/app-kit/track-noun";
1210
- import { jsx as jsx21, jsxs as jsxs12 } from "react/jsx-runtime";
1236
+ import { jsx as jsx22, jsxs as jsxs13 } from "react/jsx-runtime";
1211
1237
  var CATALOGUE_PAGE_SIZE = 100;
1212
1238
  var SongPageSchema = PaginatedResponseSchema(SongDetailedSchema).extend({
1213
1239
  overallTotal: z.number().nullable(),
@@ -1294,35 +1320,35 @@ function GenrePlaylistTracks({
1294
1320
  started.catch((error) => {
1295
1321
  console.error("Failed to play genre playlist:", error);
1296
1322
  if (error instanceof TracksFailureAlreadyReportedError2) return;
1297
- showPopup(/* @__PURE__ */ jsx21(InternalErrorPopup, { errorCode: error instanceof AppError2 ? error.code : ErrorCode2.CLIENT_UNKNOWN }));
1323
+ showPopup(/* @__PURE__ */ jsx22(InternalErrorPopup, { errorCode: error instanceof AppError2 ? error.code : ErrorCode2.CLIENT_UNKNOWN }));
1298
1324
  });
1299
1325
  };
1300
- return /* @__PURE__ */ jsx21(PanelSection, { title: `${capitalize(trackNoun.plural)}${total !== void 0 ? ` (${total})` : ""}`, children: /* @__PURE__ */ jsxs12("div", { children: [
1326
+ return /* @__PURE__ */ jsx22(PanelSection, { title: `${capitalize(trackNoun.plural)}${total !== void 0 ? ` (${total})` : ""}`, children: /* @__PURE__ */ jsxs13("div", { children: [
1301
1327
  renderTracksToolbar?.(),
1302
- scope === "me" && !genrePlaylist ? /* @__PURE__ */ jsx21("p", { children: "\u2014" }) : isPending ? /* @__PURE__ */ jsx21(Skeleton, { className: "h-4 w-3/4", "aria-busy": "true" }) : isError && tracks.length === 0 ? /* @__PURE__ */ jsxs12("p", { role: "alert", children: [
1328
+ scope === "me" && !genrePlaylist ? /* @__PURE__ */ jsx22("p", { children: "\u2014" }) : isPending ? /* @__PURE__ */ jsx22(Skeleton, { className: "h-4 w-3/4", "aria-busy": "true" }) : isError && tracks.length === 0 ? /* @__PURE__ */ jsxs13("p", { role: "alert", children: [
1303
1329
  "Couldn't load ",
1304
1330
  trackNoun.plural,
1305
1331
  ".",
1306
1332
  " ",
1307
- /* @__PURE__ */ jsx21("button", { type: "button", className: "underline", onClick: () => void refetch(), children: "Retry" })
1308
- ] }) : tracks.length === 0 ? /* @__PURE__ */ jsx21("p", { children: "\u2014" }) : /* @__PURE__ */ jsxs12("ul", { className: "mt-1 max-h-64 overflow-y-auto text-body [container-type:inline-size]", "aria-label": `${genreName} ${trackNoun.plural}`, children: [
1333
+ /* @__PURE__ */ jsx22("button", { type: "button", className: "underline", onClick: () => void refetch(), children: "Retry" })
1334
+ ] }) : tracks.length === 0 ? /* @__PURE__ */ jsx22("p", { children: "\u2014" }) : /* @__PURE__ */ jsxs13("ul", { className: "mt-1 max-h-64 overflow-y-auto text-body [container-type:inline-size]", "aria-label": `${genreName} ${trackNoun.plural}`, children: [
1309
1335
  tracks.map((track, index) => {
1310
1336
  const label = track.artists?.length ? `${track.title} \u2014 ${track.artists.map((artist2) => artist2.name).join(", ")}` : track.title;
1311
1337
  const isCurrent = playerTrackObject?.track.id === track.uuid;
1312
1338
  const isPlaying = isCurrent && playState === PlayStates.PLAYING;
1313
- const text = /* @__PURE__ */ jsxs12("span", { className: TEXT_CELL, children: [
1314
- /* @__PURE__ */ jsx21("span", { className: TITLE_CELL, children: renderSongLink ? renderSongLink(track) : isSong(track) ? /* @__PURE__ */ jsx21(MusicBrainzRecordingLink, { song: track, className: "hover:underline focus-visible:underline", children: track.title }) : track.title }),
1315
- track.artists?.length ? /* @__PURE__ */ jsx21("span", { className: SEPARATOR, children: " \u2014 " }) : null,
1316
- /* @__PURE__ */ jsx21("span", { className: COLUMN_CELL, children: track.artists?.length ? /* @__PURE__ */ jsx21(ArtistNames, { artists: track.artists, renderArtistLink }) : null }),
1317
- track.album && /* @__PURE__ */ jsx21("span", { className: ALBUM_CELL, children: track.album.name })
1339
+ const text = /* @__PURE__ */ jsxs13("span", { className: TEXT_CELL, children: [
1340
+ /* @__PURE__ */ jsx22("span", { className: TITLE_CELL, children: renderSongLink ? renderSongLink(track) : isSong(track) ? /* @__PURE__ */ jsx22(MusicBrainzRecordingLink, { song: track, className: "hover:underline focus-visible:underline", children: track.title }) : track.title }),
1341
+ track.artists?.length ? /* @__PURE__ */ jsx22("span", { className: SEPARATOR, children: " \u2014 " }) : null,
1342
+ /* @__PURE__ */ jsx22("span", { className: COLUMN_CELL, children: track.artists?.length ? /* @__PURE__ */ jsx22(ArtistNames, { artists: track.artists, renderArtistLink }) : null }),
1343
+ track.album && /* @__PURE__ */ jsx22("span", { className: ALBUM_CELL, children: track.album.name })
1318
1344
  ] });
1319
1345
  const leading = renderSongLeading?.(track, index);
1320
1346
  const actions = renderSongActions?.(track);
1321
1347
  const isUnplayable = isSong(track) && songUnplayableReason(track) !== null;
1322
1348
  if (isUnplayable) {
1323
- return /* @__PURE__ */ jsxs12("li", { className: "flex min-h-8 items-center gap-1 px-1 opacity-50", children: [
1349
+ return /* @__PURE__ */ jsxs13("li", { className: "flex min-h-8 items-center gap-1 px-1 opacity-50", children: [
1324
1350
  leading,
1325
- /* @__PURE__ */ jsx21(
1351
+ /* @__PURE__ */ jsx22(
1326
1352
  "button",
1327
1353
  {
1328
1354
  type: "button",
@@ -1330,27 +1356,27 @@ function GenrePlaylistTracks({
1330
1356
  "aria-disabled": "true",
1331
1357
  "aria-label": label,
1332
1358
  className: "flex h-6 w-6 shrink-0 items-center justify-center rounded-full opacity-0 cursor-not-allowed",
1333
- children: /* @__PURE__ */ jsx21(FaPlay, { size: 10 })
1359
+ children: /* @__PURE__ */ jsx22(FaPlay, { size: 10 })
1334
1360
  }
1335
1361
  ),
1336
1362
  text,
1337
1363
  actions
1338
1364
  ] }, track.uuid);
1339
1365
  }
1340
- return /* @__PURE__ */ jsxs12(
1366
+ return /* @__PURE__ */ jsxs13(
1341
1367
  "li",
1342
1368
  {
1343
1369
  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)]" : ""}`,
1344
1370
  children: [
1345
1371
  leading,
1346
- /* @__PURE__ */ jsx21(
1372
+ /* @__PURE__ */ jsx22(
1347
1373
  "button",
1348
1374
  {
1349
1375
  type: "button",
1350
1376
  "aria-label": `${isPlaying ? "Pause" : "Play"} ${label}`,
1351
1377
  className: `flex h-6 w-6 shrink-0 items-center justify-center rounded-full cursor-pointer group-hover:opacity-100 focus-visible:opacity-100 ${isCurrent && playState !== PlayStates.STOPPED ? "opacity-100" : "opacity-0 [@media(pointer:coarse)]:opacity-100"}`,
1352
1378
  onClick: () => isCurrent ? handlePlayPauseAction() : play(track.uuid),
1353
- children: isCurrent && playState === PlayStates.LOADING ? /* @__PURE__ */ jsx21(RingLoader, { size: 10 }) : isPlaying ? /* @__PURE__ */ jsx21(FaPause, { size: 10 }) : /* @__PURE__ */ jsx21(FaPlay, { size: 10 })
1379
+ children: isCurrent && playState === PlayStates.LOADING ? /* @__PURE__ */ jsx22(RingLoader, { size: 10 }) : isPlaying ? /* @__PURE__ */ jsx22(FaPause, { size: 10 }) : /* @__PURE__ */ jsx22(FaPlay, { size: 10 })
1354
1380
  }
1355
1381
  ),
1356
1382
  text,
@@ -1360,7 +1386,7 @@ function GenrePlaylistTracks({
1360
1386
  track.uuid
1361
1387
  );
1362
1388
  }),
1363
- hasNextPage && /* @__PURE__ */ jsx21("li", { ref: setSentinel, "aria-hidden": "true", className: "h-4" })
1389
+ hasNextPage && /* @__PURE__ */ jsx22("li", { ref: setSentinel, "aria-hidden": "true", className: "h-4" })
1364
1390
  ] })
1365
1391
  ] }) });
1366
1392
  }
@@ -1368,7 +1394,7 @@ function GenrePlaylistTracks({
1368
1394
  // src/genre-tree/GenreDetailExtras.tsx
1369
1395
  import { useTrackNoun as useTrackNoun2 } from "@behindthemusictree/app-kit/track-noun";
1370
1396
  import { SiWikidata, SiWikipedia } from "react-icons/si";
1371
- import { Fragment as Fragment9, jsx as jsx22, jsxs as jsxs13 } from "react/jsx-runtime";
1397
+ import { Fragment as Fragment9, jsx as jsx23, jsxs as jsxs14 } from "react/jsx-runtime";
1372
1398
  function GenreDetailExtras({
1373
1399
  genreUuid,
1374
1400
  genrePlaylist,
@@ -1394,17 +1420,18 @@ function GenreDetailExtras({
1394
1420
  criteriaOverviewSchema
1395
1421
  );
1396
1422
  if (isPending) {
1397
- return /* @__PURE__ */ jsxs13(Fragment9, { children: [
1398
- /* @__PURE__ */ jsx22(PanelSection2, { title: "Summary", children: /* @__PURE__ */ jsx22("div", { "aria-busy": "true", "data-testid": "genre-detail-extras-skeleton", children: /* @__PURE__ */ jsx22(Skeleton2, { className: "h-4 w-3/4" }) }) }),
1423
+ return /* @__PURE__ */ jsxs14(Fragment9, { children: [
1424
+ /* @__PURE__ */ jsx23(PanelSection2, { title: "Summary", children: /* @__PURE__ */ jsx23("div", { "aria-busy": "true", "data-testid": "genre-detail-extras-skeleton", children: /* @__PURE__ */ jsx23(Skeleton2, { className: "h-4 w-3/4" }) }) }),
1399
1425
  family
1400
1426
  ] });
1401
1427
  }
1402
- if (!overview) return /* @__PURE__ */ jsx22(Fragment9, { children: family });
1403
- const { summary, summarySourceUrl, essentialSongs } = overview;
1428
+ if (!overview) return /* @__PURE__ */ jsx23(Fragment9, { children: family });
1429
+ const { summary, summarySourceUrl, essentialSongs, image } = overview;
1404
1430
  const wikidataUrl = wikidataId ? `https://www.wikidata.org/wiki/${encodeURIComponent(wikidataId)}` : null;
1405
- return /* @__PURE__ */ jsxs13(Fragment9, { children: [
1406
- (summarySourceUrl || wikidataUrl) && /* @__PURE__ */ jsx22(PanelSection2, { title: "Links", children: /* @__PURE__ */ jsxs13("div", { className: "flex flex-wrap gap-x-3 gap-y-1 text-caption text-[var(--color-text)]", children: [
1407
- summarySourceUrl && /* @__PURE__ */ jsxs13(
1431
+ return /* @__PURE__ */ jsxs14(Fragment9, { children: [
1432
+ image && /* @__PURE__ */ jsx23(CommonsPhoto, { image, alt: overview.name, creditLabel: "Image" }),
1433
+ (summarySourceUrl || wikidataUrl) && /* @__PURE__ */ jsx23(PanelSection2, { title: "Links", children: /* @__PURE__ */ jsxs14("div", { className: "flex flex-wrap gap-x-3 gap-y-1 text-caption text-[var(--color-text)]", children: [
1434
+ summarySourceUrl && /* @__PURE__ */ jsxs14(
1408
1435
  "a",
1409
1436
  {
1410
1437
  href: summarySourceUrl,
@@ -1412,12 +1439,12 @@ function GenreDetailExtras({
1412
1439
  rel: "noopener noreferrer",
1413
1440
  className: "inline-flex items-center gap-1.5 text-[var(--color-text-muted)] hover:text-[var(--color-text)]",
1414
1441
  children: [
1415
- /* @__PURE__ */ jsx22(SiWikipedia, { "aria-hidden": "true", className: "h-3.5 w-3.5" }),
1442
+ /* @__PURE__ */ jsx23(SiWikipedia, { "aria-hidden": "true", className: "h-3.5 w-3.5" }),
1416
1443
  "Wikipedia"
1417
1444
  ]
1418
1445
  }
1419
1446
  ),
1420
- wikidataUrl && /* @__PURE__ */ jsxs13(
1447
+ wikidataUrl && /* @__PURE__ */ jsxs14(
1421
1448
  "a",
1422
1449
  {
1423
1450
  href: wikidataUrl,
@@ -1425,17 +1452,17 @@ function GenreDetailExtras({
1425
1452
  rel: "noopener noreferrer",
1426
1453
  className: "inline-flex items-center gap-1.5 text-[var(--color-text-muted)] hover:text-[var(--color-text)]",
1427
1454
  children: [
1428
- /* @__PURE__ */ jsx22(SiWikidata, { "aria-hidden": "true", className: "h-3.5 w-3.5" }),
1455
+ /* @__PURE__ */ jsx23(SiWikidata, { "aria-hidden": "true", className: "h-3.5 w-3.5" }),
1429
1456
  "Wikidata"
1430
1457
  ]
1431
1458
  }
1432
1459
  )
1433
1460
  ] }) }),
1434
- /* @__PURE__ */ jsx22(PanelSection2, { title: "Summary", children: renderSummary ? renderSummary(overview) : /* @__PURE__ */ jsx22(ClampedText, { className: "text-body text-[var(--color-text)]", children: summary ?? "\u2014" }, genreUuid) }),
1461
+ /* @__PURE__ */ jsx23(PanelSection2, { title: "Summary", children: renderSummary ? renderSummary(overview) : /* @__PURE__ */ jsx23(ClampedText, { className: "text-body text-[var(--color-text)]", children: summary ?? "\u2014" }, genreUuid) }),
1435
1462
  family,
1436
1463
  renderGenreDetailExtras?.(overview),
1437
- (renderEssentialTracks || essentialSongs) && /* @__PURE__ */ jsx22(PanelSection2, { title: `Essential ${trackNoun.plural}`, children: renderEssentialTracks ? renderEssentialTracks(overview) : essentialSongs && essentialSongs.length > 0 ? /* @__PURE__ */ jsx22("ul", { className: "text-body text-[var(--color-text)]", children: essentialSongs.map((track) => /* @__PURE__ */ jsx22("li", { className: "py-1", children: track.title }, track.uuid)) }) : /* @__PURE__ */ jsx22("p", { className: "text-body text-[var(--color-text)]", children: "\u2014" }) }),
1438
- /* @__PURE__ */ jsx22(
1464
+ (renderEssentialTracks || essentialSongs) && /* @__PURE__ */ jsx23(PanelSection2, { title: `Essential ${trackNoun.plural}`, children: renderEssentialTracks ? renderEssentialTracks(overview) : essentialSongs && essentialSongs.length > 0 ? /* @__PURE__ */ jsx23("ul", { className: "text-body text-[var(--color-text)]", children: essentialSongs.map((track) => /* @__PURE__ */ jsx23("li", { className: "py-1", children: track.title }, track.uuid)) }) : /* @__PURE__ */ jsx23("p", { className: "text-body text-[var(--color-text)]", children: "\u2014" }) }),
1465
+ /* @__PURE__ */ jsx23(
1439
1466
  GenrePlaylistTracks,
1440
1467
  {
1441
1468
  genreUuid,
@@ -1453,10 +1480,10 @@ function GenreDetailExtras({
1453
1480
  }
1454
1481
 
1455
1482
  // src/genre-tree/GenreInfoPanelBody.tsx
1456
- import { Fragment as Fragment10, jsx as jsx23, jsxs as jsxs14 } from "react/jsx-runtime";
1483
+ import { Fragment as Fragment10, jsx as jsx24, jsxs as jsxs15 } from "react/jsx-runtime";
1457
1484
  import { createElement } from "react";
1458
1485
  function GenreNodeActionBand({ genrePlaylist, playback, ...bandProps }) {
1459
- return /* @__PURE__ */ jsx23(GenreActionBand, { ...bandProps, playActions: /* @__PURE__ */ jsx23(GenrePlayActions, { genrePlaylist, playback }) });
1486
+ return /* @__PURE__ */ jsx24(GenreActionBand, { ...bandProps, playActions: /* @__PURE__ */ jsx24(GenrePlayActions, { genrePlaylist, playback }) });
1460
1487
  }
1461
1488
  function GenreInfoPanelBody({
1462
1489
  toolbar,
@@ -1473,9 +1500,9 @@ function GenreInfoPanelBody({
1473
1500
  }) {
1474
1501
  const summary = scope === "reference" ? GenreCatalogueCountsSchema2.parse(genrePlaylist ?? catalogue) : void 0;
1475
1502
  const selection = summary ? defaultGenreMusicSelection2(summary) : void 0;
1476
- return /* @__PURE__ */ jsxs14(Fragment10, { children: [
1503
+ return /* @__PURE__ */ jsxs15(Fragment10, { children: [
1477
1504
  toolbar,
1478
- genrePlaylist ? /* @__PURE__ */ jsx23(GenreNodeActionBand, { ...band, genrePlaylist, playback }) : /* @__PURE__ */ jsx23(GenreActionBand, { ...band }),
1505
+ genrePlaylist ? /* @__PURE__ */ jsx24(GenreNodeActionBand, { ...band, genrePlaylist, playback }) : /* @__PURE__ */ jsx24(GenreActionBand, { ...band }),
1479
1506
  /* @__PURE__ */ createElement(
1480
1507
  GenreDetailExtras,
1481
1508
  {
@@ -1495,7 +1522,7 @@ function GenreInfoPanelBody({
1495
1522
  }
1496
1523
 
1497
1524
  // src/genre-tree/GenreDetailPanel.tsx
1498
- import { Fragment as Fragment11, jsx as jsx24, jsxs as jsxs15 } from "react/jsx-runtime";
1525
+ import { Fragment as Fragment11, jsx as jsx25, jsxs as jsxs16 } from "react/jsx-runtime";
1499
1526
  var RELATIVE_ITEM_COUNT = 1;
1500
1527
  function toNode(genre, parentId) {
1501
1528
  return {
@@ -1520,7 +1547,8 @@ function useGenreDetailContent({
1520
1547
  expanded,
1521
1548
  treeStyles
1522
1549
  }) {
1523
- const { data: detail, isPending, isError } = useFetchGenreDetail(genreUuid, scope, getBackendBaseUrl);
1550
+ const result = useFetchGenreDetail(genreUuid, scope, getBackendBaseUrl);
1551
+ const detail = result.status === "success" ? result.genre : void 0;
1524
1552
  const defaultPlayback = useGenrePlaylistPlayback({ scope, genrePlaylists: [], artist: extras?.artist });
1525
1553
  const playback = hostPlayback ?? defaultPlayback;
1526
1554
  const view = useMemo6(() => {
@@ -1549,22 +1577,27 @@ function useGenreDetailContent({
1549
1577
  )
1550
1578
  };
1551
1579
  }, [detail, scope, treeStyles]);
1552
- if (isError) return {
1580
+ if (result.status === "not-found") return {
1581
+ name: null,
1582
+ title: "Genre not found",
1583
+ content: /* @__PURE__ */ jsx25("p", { className: "text-[var(--color-text-muted)]", children: "This genre does not exist." })
1584
+ };
1585
+ if (result.status === "error") return {
1553
1586
  name: null,
1554
1587
  title: "Genre unavailable",
1555
- content: /* @__PURE__ */ jsx24("p", { role: "alert", className: "text-body text-[var(--color-text)]", children: "This genre could not be loaded." })
1588
+ content: /* @__PURE__ */ jsx25("p", { role: "alert", className: "text-body text-[var(--color-text)]", children: "This genre could not be loaded." })
1556
1589
  };
1557
- if (isPending || !detail || !view) return {
1590
+ if (!detail || !view) return {
1558
1591
  name: null,
1559
1592
  title: "Loading genre\u2026",
1560
- content: /* @__PURE__ */ jsxs15("div", { className: "flex flex-col gap-3", "aria-busy": "true", "data-testid": "genre-detail-panel-skeleton", children: [
1561
- /* @__PURE__ */ jsx24(Skeleton3, { className: "h-6 w-2/3" }),
1562
- /* @__PURE__ */ jsx24(Skeleton3, { className: "h-4 w-full" }),
1563
- /* @__PURE__ */ jsx24(Skeleton3, { className: "h-4 w-3/4" })
1593
+ content: /* @__PURE__ */ jsxs16("div", { className: "flex flex-col gap-3", "aria-busy": "true", "data-testid": "genre-detail-panel-skeleton", children: [
1594
+ /* @__PURE__ */ jsx25(Skeleton3, { className: "h-6 w-2/3" }),
1595
+ /* @__PURE__ */ jsx25(Skeleton3, { className: "h-4 w-full" }),
1596
+ /* @__PURE__ */ jsx25(Skeleton3, { className: "h-4 w-3/4" })
1564
1597
  ] })
1565
1598
  };
1566
1599
  const selectGenre = (nodeId) => onSelectGenre(GenreUuidSchema.parse(nodeId));
1567
- const family = /* @__PURE__ */ jsx24(
1600
+ const family = /* @__PURE__ */ jsx25(
1568
1601
  GenreNodeFamily,
1569
1602
  {
1570
1603
  node: view.node,
@@ -1575,17 +1608,17 @@ function useGenreDetailContent({
1575
1608
  onSelectNode: selectGenre
1576
1609
  }
1577
1610
  );
1578
- const ancestors = /* @__PURE__ */ jsx24(GenreNodeAncestors, { ancestorNodes: view.ancestorNodes, onSelectNode: selectGenre });
1611
+ const ancestors = /* @__PURE__ */ jsx25(GenreNodeAncestors, { ancestorNodes: view.ancestorNodes, onSelectNode: selectGenre });
1579
1612
  return {
1580
1613
  name: detail.name,
1581
1614
  title: detail.name,
1582
1615
  headerAccent: { background: view.fill, color: view.textColor },
1583
1616
  aside: expanded ? ancestors : void 0,
1584
- content: /* @__PURE__ */ jsxs15(Fragment11, { children: [
1585
- /* @__PURE__ */ jsx24(
1617
+ content: /* @__PURE__ */ jsxs16(Fragment11, { children: [
1618
+ /* @__PURE__ */ jsx25(
1586
1619
  GenreInfoPanelBody,
1587
1620
  {
1588
- family: /* @__PURE__ */ jsxs15(Fragment11, { children: [
1621
+ family: /* @__PURE__ */ jsxs16(Fragment11, { children: [
1589
1622
  family,
1590
1623
  !expanded && ancestors
1591
1624
  ] }),
@@ -1629,7 +1662,7 @@ function GenreDetailPanel({
1629
1662
  expanded,
1630
1663
  band: { onZoomIn, shareUrl, shareTarget: shareUrl ? shareTarget : void 0 }
1631
1664
  });
1632
- return /* @__PURE__ */ jsx24(
1665
+ return /* @__PURE__ */ jsx25(
1633
1666
  DetailPanel,
1634
1667
  {
1635
1668
  side: "left",
@@ -1642,7 +1675,7 @@ function GenreDetailPanel({
1642
1675
  edgeTab,
1643
1676
  aside: panel.aside,
1644
1677
  titleInSearch,
1645
- headerActions: onExpandedChange ? /* @__PURE__ */ jsx24(IconButton, { "aria-label": expanded ? "Shrink panel" : "Extend panel", onClick: () => onExpandedChange(!expanded), children: expanded ? /* @__PURE__ */ jsx24(Minimize2, { "aria-hidden": "true", className: "h-4 w-4" }) : /* @__PURE__ */ jsx24(Maximize2, { "aria-hidden": "true", className: "h-4 w-4" }) }) : void 0,
1678
+ headerActions: onExpandedChange ? /* @__PURE__ */ jsx25(IconButton, { "aria-label": expanded ? "Shrink panel" : "Extend panel", onClick: () => onExpandedChange(!expanded), children: expanded ? /* @__PURE__ */ jsx25(Minimize2, { "aria-hidden": "true", className: "h-4 w-4" }) : /* @__PURE__ */ jsx25(Maximize2, { "aria-hidden": "true", className: "h-4 w-4" }) }) : void 0,
1646
1679
  children: panel.content
1647
1680
  }
1648
1681
  );
@@ -1654,7 +1687,7 @@ import {
1654
1687
  getReparentCandidates,
1655
1688
  useUpdateGenre
1656
1689
  } from "@behindthemusictree/app-kit/genre-tree";
1657
- import { jsx as jsx25, jsxs as jsxs16 } from "react/jsx-runtime";
1690
+ import { jsx as jsx26, jsxs as jsxs17 } from "react/jsx-runtime";
1658
1691
  function ChangeParentPopup({
1659
1692
  genrePlaylist,
1660
1693
  genrePlaylists,
@@ -1689,12 +1722,12 @@ function ChangeParentPopup({
1689
1722
  window.addEventListener("keydown", handleKeyDown);
1690
1723
  return () => window.removeEventListener("keydown", handleKeyDown);
1691
1724
  }, [onClose]);
1692
- return /* @__PURE__ */ jsxs16(BasePopup_default, { title: `Change parent of ${genrePlaylist.name}`, onClose, showCancelButton: true, fitContent: true, children: [
1693
- /* @__PURE__ */ jsxs16("p", { className: "mb-3 text-body text-[var(--color-text-muted)]", children: [
1725
+ return /* @__PURE__ */ jsxs17(BasePopup_default, { title: `Change parent of ${genrePlaylist.name}`, onClose, showCancelButton: true, fitContent: true, children: [
1726
+ /* @__PURE__ */ jsxs17("p", { className: "mb-3 text-body text-[var(--color-text-muted)]", children: [
1694
1727
  "Current parent: ",
1695
1728
  genrePlaylist.parent?.name ?? "none"
1696
1729
  ] }),
1697
- /* @__PURE__ */ jsx25("div", { "aria-busy": isPending, className: isPending ? "pointer-events-none opacity-60" : void 0, children: /* @__PURE__ */ jsx25(
1730
+ /* @__PURE__ */ jsx26("div", { "aria-busy": isPending, className: isPending ? "pointer-events-none opacity-60" : void 0, children: /* @__PURE__ */ jsx26(
1698
1731
  GenreSearch,
1699
1732
  {
1700
1733
  genrePlaylists: candidates,
@@ -1702,7 +1735,7 @@ function ChangeParentPopup({
1702
1735
  placeholder: "Search a new parent\u2026"
1703
1736
  }
1704
1737
  ) }),
1705
- formErrors.length > 0 && /* @__PURE__ */ jsx25("p", { role: "alert", className: "mt-3 text-body text-[var(--color-red-500)]", children: formErrors.map(({ message }) => message).join(" ") })
1738
+ formErrors.length > 0 && /* @__PURE__ */ jsx26("p", { role: "alert", className: "mt-3 text-body text-[var(--color-red-500)]", children: formErrors.map(({ message }) => message).join(" ") })
1706
1739
  ] });
1707
1740
  }
1708
1741
 
@@ -1715,7 +1748,7 @@ import { Pause as Pause2, Play as Play2 } from "lucide-react";
1715
1748
  import { ActionBand as ActionBand2, ActionButton as ActionButton4, PanelBleed as PanelBleed2 } from "@behindthemusictree/ui";
1716
1749
  import { TrackListOriginType as TrackListOriginType2, useTrackList as useTrackList3 } from "@behindthemusictree/app-kit/genre-tree";
1717
1750
  import { usePlayer as usePlayer6 } from "@behindthemusictree/app-kit/player";
1718
- import { jsx as jsx26, jsxs as jsxs17 } from "react/jsx-runtime";
1751
+ import { jsx as jsx27, jsxs as jsxs18 } from "react/jsx-runtime";
1719
1752
  function ArtistActionBand({ artist, share }) {
1720
1753
  const { trackList, playNewTrackListFromArtist } = useTrackList3();
1721
1754
  const { isPlaying, setIsPlaying } = usePlayer6();
@@ -1731,11 +1764,11 @@ function ArtistActionBand({ artist, share }) {
1731
1764
  console.error("Failed to load artist tracks:", error);
1732
1765
  });
1733
1766
  };
1734
- return /* @__PURE__ */ jsx26(PanelBleed2, { children: /* @__PURE__ */ jsxs17(ActionBand2, { children: [
1735
- /* @__PURE__ */ jsx26(
1767
+ return /* @__PURE__ */ jsx27(PanelBleed2, { children: /* @__PURE__ */ jsxs18(ActionBand2, { children: [
1768
+ /* @__PURE__ */ jsx27(
1736
1769
  ActionButton4,
1737
1770
  {
1738
- icon: playing ? /* @__PURE__ */ jsx26(Pause2, { "aria-hidden": "true", className: "w-5 h-5" }) : /* @__PURE__ */ jsx26(Play2, { "aria-hidden": "true", className: "w-5 h-5" }),
1771
+ icon: playing ? /* @__PURE__ */ jsx27(Pause2, { "aria-hidden": "true", className: "w-5 h-5" }) : /* @__PURE__ */ jsx27(Play2, { "aria-hidden": "true", className: "w-5 h-5" }),
1739
1772
  label: playing ? "Pause" : "Play",
1740
1773
  caption: String(artist.playableTracksCount),
1741
1774
  disabled: artist.playableTracksCount === 0,
@@ -1763,7 +1796,7 @@ import {
1763
1796
  SiYoutube
1764
1797
  } from "react-icons/si";
1765
1798
  import { Chip } from "@behindthemusictree/ui";
1766
- import { jsx as jsx27 } from "react/jsx-runtime";
1799
+ import { jsx as jsx28 } from "react/jsx-runtime";
1767
1800
  var LINK_KINDS = {
1768
1801
  official: { label: "Official website", Icon: Globe },
1769
1802
  wikipedia: { label: "Wikipedia", Icon: SiWikipedia2 },
@@ -1800,7 +1833,7 @@ function artistLinks(artist) {
1800
1833
  return links;
1801
1834
  }
1802
1835
  function ArtistLinks({ links }) {
1803
- return /* @__PURE__ */ jsx27("ul", { className: "flex flex-wrap gap-1.5", children: links.map(({ key, url, Icon, label }) => /* @__PURE__ */ jsx27("li", { children: /* @__PURE__ */ jsx27(Chip, { href: url, icon: /* @__PURE__ */ jsx27(Icon, { className: "h-3.5 w-3.5 shrink-0", "aria-hidden": "true" }), children: label }) }, key)) });
1836
+ return /* @__PURE__ */ jsx28("ul", { className: "flex flex-wrap gap-1.5", children: links.map(({ key, url, Icon, label }) => /* @__PURE__ */ jsx28("li", { children: /* @__PURE__ */ jsx28(Chip, { href: url, icon: /* @__PURE__ */ jsx28(Icon, { className: "h-3.5 w-3.5 shrink-0", "aria-hidden": "true" }), children: label }) }, key)) });
1804
1837
  }
1805
1838
 
1806
1839
  // src/genre-tree/artist/formatYears.ts
@@ -1811,7 +1844,7 @@ function formatYears(beginYear, endYear, ended) {
1811
1844
  }
1812
1845
 
1813
1846
  // src/genre-tree/artist/ArtistHeader.tsx
1814
- import { jsx as jsx28 } from "react/jsx-runtime";
1847
+ import { jsx as jsx29 } from "react/jsx-runtime";
1815
1848
  var countryNames = new Intl.DisplayNames(["en"], { type: "region" });
1816
1849
  function ArtistHeader({ artist }) {
1817
1850
  const fields = [];
@@ -1821,35 +1854,23 @@ function ArtistHeader({ artist }) {
1821
1854
  const years = formatYears(artist.beginYear, artist.endYear, artist.ended);
1822
1855
  if (years) fields.push({ label: "Active", value: years });
1823
1856
  const links = artistLinks(artist);
1824
- if (links.length > 0) fields.push({ label: "Links", value: /* @__PURE__ */ jsx28(ArtistLinks, { links }) });
1857
+ if (links.length > 0) fields.push({ label: "Links", value: /* @__PURE__ */ jsx29(ArtistLinks, { links }) });
1825
1858
  if (fields.length === 0) return null;
1826
- return /* @__PURE__ */ jsx28(FieldList, { items: fields });
1859
+ return /* @__PURE__ */ jsx29(FieldList, { items: fields });
1827
1860
  }
1828
1861
 
1829
1862
  // src/genre-tree/artist/ArtistPhoto.tsx
1830
- import { jsx as jsx29, jsxs as jsxs18 } from "react/jsx-runtime";
1831
- function ExternalLink({ href, children }) {
1832
- return /* @__PURE__ */ jsx29("a", { href, target: "_blank", rel: "noopener noreferrer", className: "underline-offset-2 hover:underline", children });
1833
- }
1863
+ import { jsx as jsx30 } from "react/jsx-runtime";
1834
1864
  function ArtistPhoto({ image, name }) {
1835
- return /* @__PURE__ */ jsxs18("figure", { className: "flex flex-col gap-1.5", children: [
1836
- /* @__PURE__ */ jsx29("img", { src: image.url, alt: name, className: "h-64 w-full object-contain bg-[var(--color-surface-muted)] rounded" }),
1837
- /* @__PURE__ */ jsxs18("figcaption", { className: "text-caption text-[var(--color-text-muted)]", children: [
1838
- "Photo: ",
1839
- /* @__PURE__ */ jsx29(ExternalLink, { href: image.pageUrl, children: image.author ?? "Wikimedia Commons" }),
1840
- ",",
1841
- " ",
1842
- image.licenseUrl ? /* @__PURE__ */ jsx29(ExternalLink, { href: image.licenseUrl, children: image.license }) : image.license
1843
- ] })
1844
- ] });
1865
+ return /* @__PURE__ */ jsx30(CommonsPhoto, { image, alt: name });
1845
1866
  }
1846
1867
 
1847
1868
  // src/genre-tree/artist/ArtistMembers.tsx
1848
1869
  import { PanelGroup, PanelSection as PanelSection3 } from "@behindthemusictree/ui";
1849
- import { jsx as jsx30, jsxs as jsxs19 } from "react/jsx-runtime";
1870
+ import { jsx as jsx31, jsxs as jsxs19 } from "react/jsx-runtime";
1850
1871
  function MembershipList({ title, memberships, renderArtistLink }) {
1851
1872
  if (memberships.length === 0) return null;
1852
- return /* @__PURE__ */ jsx30(PanelGroup, { title, children: /* @__PURE__ */ jsx30("ul", { children: memberships.map((membership, index) => {
1873
+ return /* @__PURE__ */ jsx31(PanelGroup, { title, children: /* @__PURE__ */ jsx31("ul", { children: memberships.map((membership, index) => {
1853
1874
  const details = [
1854
1875
  formatYears(membership.beginYear, membership.endYear, membership.ended),
1855
1876
  ...membership.roles
@@ -1858,7 +1879,7 @@ function MembershipList({ title, memberships, renderArtistLink }) {
1858
1879
  return (
1859
1880
  // A member can have several periods in the same group.
1860
1881
  /* @__PURE__ */ jsxs19("li", { className: "truncate py-0.5", title: detailsText || void 0, children: [
1861
- /* @__PURE__ */ jsx30("span", { className: "font-medium text-[var(--color-text)]", children: renderArtistLink(membership.artist) }),
1882
+ /* @__PURE__ */ jsx31("span", { className: "font-medium text-[var(--color-text)]", children: renderArtistLink(membership.artist) }),
1862
1883
  detailsText && /* @__PURE__ */ jsxs19("span", { className: "text-[var(--color-text-muted)]", children: [
1863
1884
  " \u2014 ",
1864
1885
  detailsText
@@ -1871,8 +1892,8 @@ function ArtistMembers({ title, memberships, renderArtistLink }) {
1871
1892
  if (memberships.length === 0) return null;
1872
1893
  const isCurrent = (membership) => !membership.ended && membership.endYear === null;
1873
1894
  return /* @__PURE__ */ jsxs19(PanelSection3, { title, children: [
1874
- /* @__PURE__ */ jsx30(MembershipList, { title: "Current", memberships: memberships.filter(isCurrent), renderArtistLink }),
1875
- /* @__PURE__ */ jsx30(
1895
+ /* @__PURE__ */ jsx31(MembershipList, { title: "Current", memberships: memberships.filter(isCurrent), renderArtistLink }),
1896
+ /* @__PURE__ */ jsx31(
1876
1897
  MembershipList,
1877
1898
  {
1878
1899
  title: "Past",
@@ -1885,7 +1906,7 @@ function ArtistMembers({ title, memberships, renderArtistLink }) {
1885
1906
 
1886
1907
  // src/genre-tree/artist/ArtistDiscography.tsx
1887
1908
  import { PanelGroup as PanelGroup2, PanelSection as PanelSection4 } from "@behindthemusictree/ui";
1888
- import { jsx as jsx31, jsxs as jsxs20 } from "react/jsx-runtime";
1909
+ import { jsx as jsx32, jsxs as jsxs20 } from "react/jsx-runtime";
1889
1910
  var GROUPS = ["Albums", "EPs", "Singles", "Live", "Compilations", "Other"];
1890
1911
  var PRIMARY_GROUPS = { album: "Albums", ep: "EPs", single: "Singles" };
1891
1912
  function discographyGroup({ primaryType, secondaryTypes }) {
@@ -1905,12 +1926,12 @@ function byYear(a, b) {
1905
1926
  function ArtistDiscography({ albums }) {
1906
1927
  if (albums.length === 0) return null;
1907
1928
  const sorted = [...albums].sort(byYear);
1908
- return /* @__PURE__ */ jsx31(PanelSection4, { title: "Discography", children: GROUPS.map((group) => {
1929
+ return /* @__PURE__ */ jsx32(PanelSection4, { title: "Discography", children: GROUPS.map((group) => {
1909
1930
  const groupAlbums = sorted.filter((album) => discographyGroup(album) === group);
1910
1931
  if (groupAlbums.length === 0) return null;
1911
- return /* @__PURE__ */ jsx31(PanelGroup2, { title: group, children: /* @__PURE__ */ jsx31("ul", { children: groupAlbums.map((album) => /* @__PURE__ */ jsxs20("li", { className: "grid grid-cols-[2.5rem_minmax(0,1fr)] gap-2 py-0.5", children: [
1912
- /* @__PURE__ */ jsx31("span", { className: "tabular-nums text-[var(--color-text-muted)]", children: album.year ?? "\u2014" }),
1913
- /* @__PURE__ */ jsx31("span", { className: "truncate text-[var(--color-text)]", title: album.name, children: album.name })
1932
+ return /* @__PURE__ */ jsx32(PanelGroup2, { title: group, children: /* @__PURE__ */ jsx32("ul", { children: groupAlbums.map((album) => /* @__PURE__ */ jsxs20("li", { className: "grid grid-cols-[2.5rem_minmax(0,1fr)] gap-2 py-0.5", children: [
1933
+ /* @__PURE__ */ jsx32("span", { className: "tabular-nums text-[var(--color-text-muted)]", children: album.year ?? "\u2014" }),
1934
+ /* @__PURE__ */ jsx32("span", { className: "truncate text-[var(--color-text)]", title: album.name, children: album.name })
1914
1935
  ] }, album.uuid)) }) }, group);
1915
1936
  }) });
1916
1937
  }
@@ -1936,7 +1957,7 @@ import { useTrackList as useTrackList4 } from "@behindthemusictree/app-kit/genre
1936
1957
  import { FaPlay as FaPlay2, FaPause as FaPause2 } from "react-icons/fa";
1937
1958
  import { usePlayer as usePlayer7 } from "@behindthemusictree/app-kit/player";
1938
1959
  import { PlayStates as PlayStates2 } from "@behindthemusictree/app-kit/player";
1939
- import { jsx as jsx32, jsxs as jsxs21 } from "react/jsx-runtime";
1960
+ import { jsx as jsx33, jsxs as jsxs21 } from "react/jsx-runtime";
1940
1961
  function TrackPositionPlayPause({
1941
1962
  position,
1942
1963
  uuid,
@@ -1957,7 +1978,7 @@ function TrackPositionPlayPause({
1957
1978
  style: { minWidth: "4rem", maxWidth: "4rem" },
1958
1979
  onClick: handlePlayPauseClick,
1959
1980
  children: [
1960
- /* @__PURE__ */ jsx32("div", { className: "track-position-play-pause__status group-hover:hidden flex items-center justify-center", children: isAwaitingPlayback ? /* @__PURE__ */ jsx32("span", { className: "track-position-play-pause__spinner", role: "status", "aria-label": "Loading track" }) : isCurrentTrackActive ? /* @__PURE__ */ jsxs21(
1981
+ /* @__PURE__ */ jsx33("div", { className: "track-position-play-pause__status group-hover:hidden flex items-center justify-center", children: isAwaitingPlayback ? /* @__PURE__ */ jsx33("span", { className: "track-position-play-pause__spinner", role: "status", "aria-label": "Loading track" }) : isCurrentTrackActive ? /* @__PURE__ */ jsxs21(
1961
1982
  "span",
1962
1983
  {
1963
1984
  className: "track-position-play-pause__bars",
@@ -1965,18 +1986,18 @@ function TrackPositionPlayPause({
1965
1986
  role: "img",
1966
1987
  "aria-label": isPlaying ? "Playing" : "Paused",
1967
1988
  children: [
1968
- /* @__PURE__ */ jsx32("span", { className: "track-position-play-pause__bar" }),
1969
- /* @__PURE__ */ jsx32("span", { className: "track-position-play-pause__bar" }),
1970
- /* @__PURE__ */ jsx32("span", { className: "track-position-play-pause__bar" })
1989
+ /* @__PURE__ */ jsx33("span", { className: "track-position-play-pause__bar" }),
1990
+ /* @__PURE__ */ jsx33("span", { className: "track-position-play-pause__bar" }),
1991
+ /* @__PURE__ */ jsx33("span", { className: "track-position-play-pause__bar" })
1971
1992
  ]
1972
1993
  }
1973
1994
  ) : position }),
1974
- /* @__PURE__ */ jsx32(
1995
+ /* @__PURE__ */ jsx33(
1975
1996
  "div",
1976
1997
  {
1977
1998
  className: `hidden group-hover:flex items-center justify-center ${isCurrentTrackActive ? "" : "absolute inset-0"}`,
1978
1999
  "aria-hidden": "true",
1979
- children: isAwaitingPlayback ? /* @__PURE__ */ jsx32("span", { className: "track-position-play-pause__spinner" }) : isPlaying ? /* @__PURE__ */ jsx32(FaPause2, {}) : /* @__PURE__ */ jsx32(FaPlay2, {})
2000
+ children: isAwaitingPlayback ? /* @__PURE__ */ jsx33("span", { className: "track-position-play-pause__spinner" }) : isPlaying ? /* @__PURE__ */ jsx33(FaPause2, {}) : /* @__PURE__ */ jsx33(FaPlay2, {})
1980
2001
  }
1981
2002
  )
1982
2003
  ]
@@ -1987,7 +2008,7 @@ function TrackPositionPlayPause({
1987
2008
  // src/genre-tree/track-list-sidebar/TrackItem.tsx
1988
2009
  import { isSong as isSong2, songUnplayableReason as songUnplayableReason2 } from "@behindthemusictree/app-kit/genre-tree";
1989
2010
  import { ArtistNames as ArtistNames2 } from "@behindthemusictree/app-kit/genre-tree";
1990
- import { jsx as jsx33, jsxs as jsxs22 } from "react/jsx-runtime";
2011
+ import { jsx as jsx34, jsxs as jsxs22 } from "react/jsx-runtime";
1991
2012
  function TrackItem({
1992
2013
  track,
1993
2014
  position,
@@ -2016,7 +2037,7 @@ function TrackItem({
2016
2037
  {
2017
2038
  className: `track-item flex h-14 text-[var(--color-text)] ${isUnplayable ? "opacity-50" : "hover:bg-[var(--color-surface-muted)] group"}`,
2018
2039
  children: [
2019
- isUnplayable ? /* @__PURE__ */ jsx33(
2040
+ isUnplayable ? /* @__PURE__ */ jsx34(
2020
2041
  "div",
2021
2042
  {
2022
2043
  className: "flex items-center justify-center text-lg w-16",
@@ -2024,7 +2045,7 @@ function TrackItem({
2024
2045
  "aria-disabled": "true",
2025
2046
  children: position
2026
2047
  }
2027
- ) : /* @__PURE__ */ jsx33(
2048
+ ) : /* @__PURE__ */ jsx34(
2028
2049
  TrackPositionPlayPause,
2029
2050
  {
2030
2051
  position,
@@ -2034,11 +2055,11 @@ function TrackItem({
2034
2055
  }
2035
2056
  ),
2036
2057
  /* @__PURE__ */ jsxs22("div", { className: "title-artist-container flex flex-col items-start justify-center min-w-0 flex-1", children: [
2037
- /* @__PURE__ */ jsx33("div", { className: "flex max-w-full items-center gap-2", children: /* @__PURE__ */ jsx33("div", { className: "title text-lg font-bold text-[var(--color-text)] max-w-full truncate", children: renderSongLink ? renderSongLink(track) : isSong2(track) ? /* @__PURE__ */ jsx33(MusicBrainzRecordingLink, { song: track, className: "hover:underline focus-visible:underline", children: track.title }) : track.title }) }),
2038
- track.artists && track.artists.length > 0 ? /* @__PURE__ */ jsx33("div", { className: "artist text-base max-w-full truncate text-[var(--color-text-muted)]", children: /* @__PURE__ */ jsx33(ArtistNames2, { artists: track.artists, renderArtistLink }) }) : ""
2058
+ /* @__PURE__ */ jsx34("div", { className: "flex max-w-full items-center gap-2", children: /* @__PURE__ */ jsx34("div", { className: "title text-lg font-bold text-[var(--color-text)] max-w-full truncate", children: renderSongLink ? renderSongLink(track) : isSong2(track) ? /* @__PURE__ */ jsx34(MusicBrainzRecordingLink, { song: track, className: "hover:underline focus-visible:underline", children: track.title }) : track.title }) }),
2059
+ track.artists && track.artists.length > 0 ? /* @__PURE__ */ jsx34("div", { className: "artist text-base max-w-full truncate text-[var(--color-text-muted)]", children: /* @__PURE__ */ jsx34(ArtistNames2, { artists: track.artists, renderArtistLink }) }) : ""
2039
2060
  ] }),
2040
- /* @__PURE__ */ jsx33("div", { className: "genre-name-container flex items-center justify-start w-[21.6%] min-w-0 shrink-0", children: track.genre ? /* @__PURE__ */ jsx33("div", { className: "genre-name max-w-full whitespace-normal break-words p-1 text-caption font-bold", children: track.genre.name }) : "" }),
2041
- renderDuration && /* @__PURE__ */ jsx33(
2061
+ /* @__PURE__ */ jsx34("div", { className: "genre-name-container flex items-center justify-start w-[21.6%] min-w-0 shrink-0", children: track.genre ? /* @__PURE__ */ jsx34("div", { className: "genre-name max-w-full whitespace-normal break-words p-1 text-caption font-bold", children: track.genre.name }) : "" }),
2062
+ renderDuration && /* @__PURE__ */ jsx34(
2042
2063
  "div",
2043
2064
  {
2044
2065
  className: "duration flex text-base w-16 items-center justify-center",
@@ -2046,14 +2067,14 @@ function TrackItem({
2046
2067
  children: renderDuration(track)
2047
2068
  }
2048
2069
  ),
2049
- renderActions && /* @__PURE__ */ jsx33("div", { className: "edit flex text-base w-6 items-center justify-center mr-2 cursor-pointer", children: renderActions(track) })
2070
+ renderActions && /* @__PURE__ */ jsx34("div", { className: "edit flex text-base w-6 items-center justify-center mr-2 cursor-pointer", children: renderActions(track) })
2050
2071
  ]
2051
2072
  }
2052
2073
  );
2053
2074
  }
2054
2075
 
2055
2076
  // src/genre-tree/artist/ArtistSongs.tsx
2056
- import { Fragment as Fragment12, jsx as jsx34, jsxs as jsxs23 } from "react/jsx-runtime";
2077
+ import { Fragment as Fragment12, jsx as jsx35, jsxs as jsxs23 } from "react/jsx-runtime";
2057
2078
  var SongPageSchema2 = PaginatedResponseSchema2(SongDetailedSchema2);
2058
2079
  var SORTS = [
2059
2080
  { ordering: "title", label: "Title" },
@@ -2086,8 +2107,8 @@ function ArtistSongs({ artist, getBackendBaseUrl, renderArtistLink, renderSongLi
2086
2107
  event.stopPropagation();
2087
2108
  void play(song.uuid);
2088
2109
  };
2089
- return /* @__PURE__ */ jsx34(PanelSection5, { title: "Songs", children: isPending ? /* @__PURE__ */ jsx34(Skeleton4, { className: "h-14", "aria-busy": "true" }) : isError ? /* @__PURE__ */ jsx34("p", { className: "text-red-600", children: "Could not load songs." }) : data.results.length === 0 ? /* @__PURE__ */ jsx34("p", { className: "text-[var(--color-text-muted)]", children: "No songs yet." }) : /* @__PURE__ */ jsxs23(Fragment12, { children: [
2090
- /* @__PURE__ */ jsx34("div", { className: "flex flex-wrap items-center justify-end gap-2 mb-2", children: /* @__PURE__ */ jsx34("div", { role: "group", "aria-label": "Sort songs by", className: "flex gap-1 text-caption", children: SORTS.map((sort) => /* @__PURE__ */ jsx34(
2110
+ return /* @__PURE__ */ jsx35(PanelSection5, { title: "Songs", children: isPending ? /* @__PURE__ */ jsx35(Skeleton4, { className: "h-14", "aria-busy": "true" }) : isError ? /* @__PURE__ */ jsx35("p", { className: "text-red-600", children: "Could not load songs." }) : data.results.length === 0 ? /* @__PURE__ */ jsx35("p", { className: "text-[var(--color-text-muted)]", children: "No songs yet." }) : /* @__PURE__ */ jsxs23(Fragment12, { children: [
2111
+ /* @__PURE__ */ jsx35("div", { className: "flex flex-wrap items-center justify-end gap-2 mb-2", children: /* @__PURE__ */ jsx35("div", { role: "group", "aria-label": "Sort songs by", className: "flex gap-1 text-caption", children: SORTS.map((sort) => /* @__PURE__ */ jsx35(
2091
2112
  "button",
2092
2113
  {
2093
2114
  type: "button",
@@ -2101,7 +2122,7 @@ function ArtistSongs({ artist, getBackendBaseUrl, renderArtistLink, renderSongLi
2101
2122
  },
2102
2123
  sort.ordering
2103
2124
  )) }) }),
2104
- /* @__PURE__ */ jsx34("ul", { className: "overflow-hidden mb-2 bg-zinc-950 rounded-lg", children: data.results.map((song, index) => /* @__PURE__ */ jsx34("li", { onClickCapture: (event) => handleRowClickCapture(event, song), children: /* @__PURE__ */ jsx34(
2125
+ /* @__PURE__ */ jsx35("ul", { className: "overflow-hidden mb-2 bg-zinc-950 rounded-lg", children: data.results.map((song, index) => /* @__PURE__ */ jsx35("li", { onClickCapture: (event) => handleRowClickCapture(event, song), children: /* @__PURE__ */ jsx35(
2105
2126
  TrackItem,
2106
2127
  {
2107
2128
  track: song,
@@ -2110,12 +2131,12 @@ function ArtistSongs({ artist, getBackendBaseUrl, renderArtistLink, renderSongLi
2110
2131
  renderSongLink
2111
2132
  }
2112
2133
  ) }, song.uuid)) }),
2113
- /* @__PURE__ */ jsx34(Pagination, { currentPage: page, totalPages: data.totalPages, onPageChange: setPage })
2134
+ /* @__PURE__ */ jsx35(Pagination, { currentPage: page, totalPages: data.totalPages, onPageChange: setPage })
2114
2135
  ] }) });
2115
2136
  }
2116
2137
 
2117
2138
  // src/genre-tree/artist/useArtistPanel.tsx
2118
- import { Fragment as Fragment13, jsx as jsx35, jsxs as jsxs24 } from "react/jsx-runtime";
2139
+ import { Fragment as Fragment13, jsx as jsx36, jsxs as jsxs24 } from "react/jsx-runtime";
2119
2140
  function useArtistPanel({
2120
2141
  uuid,
2121
2142
  scope,
@@ -2137,25 +2158,25 @@ function useArtistPanel({
2137
2158
  ...panel,
2138
2159
  title: "Artist",
2139
2160
  content: /* @__PURE__ */ jsxs24("div", { "aria-busy": "true", className: "flex flex-col gap-2.5", children: [
2140
- /* @__PURE__ */ jsx35(Skeleton5, { className: "h-3 w-16" }),
2141
- /* @__PURE__ */ jsx35(Skeleton5, { className: "h-4 w-32" }),
2142
- /* @__PURE__ */ jsx35(Skeleton5, { className: "h-3 w-16" }),
2143
- /* @__PURE__ */ jsx35(Skeleton5, { className: "h-4 w-24" }),
2144
- /* @__PURE__ */ jsx35(Skeleton5, { className: "h-16 mt-2.5" }),
2145
- /* @__PURE__ */ jsx35(Skeleton5, { className: "h-16" })
2161
+ /* @__PURE__ */ jsx36(Skeleton5, { className: "h-3 w-16" }),
2162
+ /* @__PURE__ */ jsx36(Skeleton5, { className: "h-4 w-32" }),
2163
+ /* @__PURE__ */ jsx36(Skeleton5, { className: "h-3 w-16" }),
2164
+ /* @__PURE__ */ jsx36(Skeleton5, { className: "h-4 w-24" }),
2165
+ /* @__PURE__ */ jsx36(Skeleton5, { className: "h-16 mt-2.5" }),
2166
+ /* @__PURE__ */ jsx36(Skeleton5, { className: "h-16" })
2146
2167
  ] })
2147
2168
  };
2148
2169
  case "not-found":
2149
2170
  return {
2150
2171
  ...panel,
2151
2172
  title: "Artist not found",
2152
- content: /* @__PURE__ */ jsx35("p", { className: "text-[var(--color-text-muted)]", children: "This artist does not exist." })
2173
+ content: /* @__PURE__ */ jsx36("p", { className: "text-[var(--color-text-muted)]", children: "This artist does not exist." })
2153
2174
  };
2154
2175
  case "error":
2155
2176
  return {
2156
2177
  ...panel,
2157
2178
  title: "Artist",
2158
- content: /* @__PURE__ */ jsx35("p", { className: "text-red-600", children: "Could not load this artist." })
2179
+ content: /* @__PURE__ */ jsx36("p", { className: "text-red-600", children: "Could not load this artist." })
2159
2180
  };
2160
2181
  case "success": {
2161
2182
  const { artist } = result;
@@ -2165,18 +2186,18 @@ function useArtistPanel({
2165
2186
  ...panel,
2166
2187
  title: artist.name,
2167
2188
  content: /* @__PURE__ */ jsxs24(Fragment13, { children: [
2168
- /* @__PURE__ */ jsx35(
2189
+ /* @__PURE__ */ jsx36(
2169
2190
  ArtistActionBand,
2170
2191
  {
2171
2192
  artist,
2172
- share: shareTarget && shareUrl ? /* @__PURE__ */ jsx35(ShareActionButton, { target: shareTarget, url: shareUrl }) : void 0
2193
+ share: shareTarget && shareUrl ? /* @__PURE__ */ jsx36(ShareActionButton, { target: shareTarget, url: shareUrl }) : void 0
2173
2194
  }
2174
2195
  ),
2175
- artist.image && /* @__PURE__ */ jsx35(ArtistPhoto, { image: artist.image, name: artist.name }),
2176
- /* @__PURE__ */ jsx35(ArtistHeader, { artist }),
2177
- /* @__PURE__ */ jsx35(ArtistMembers, { title: "Members", memberships: artist.members, renderArtistLink }),
2178
- /* @__PURE__ */ jsx35(ArtistMembers, { title: "Member of", memberships: artist.memberOf, renderArtistLink }),
2179
- artist.tracksCount > 0 && /* @__PURE__ */ jsx35(
2196
+ artist.image && /* @__PURE__ */ jsx36(ArtistPhoto, { image: artist.image, name: artist.name }),
2197
+ /* @__PURE__ */ jsx36(ArtistHeader, { artist }),
2198
+ /* @__PURE__ */ jsx36(ArtistMembers, { title: "Members", memberships: artist.members, renderArtistLink }),
2199
+ /* @__PURE__ */ jsx36(ArtistMembers, { title: "Member of", memberships: artist.memberOf, renderArtistLink }),
2200
+ artist.tracksCount > 0 && /* @__PURE__ */ jsx36(
2180
2201
  ArtistSongs,
2181
2202
  {
2182
2203
  artist: { uuid: artist.uuid, name: artist.name },
@@ -2186,7 +2207,7 @@ function useArtistPanel({
2186
2207
  }
2187
2208
  )
2188
2209
  ] }),
2189
- extraContent: artist.albums.length > 0 ? /* @__PURE__ */ jsx35(ArtistDiscography, { albums: artist.albums }) : void 0
2210
+ extraContent: artist.albums.length > 0 ? /* @__PURE__ */ jsx36(ArtistDiscography, { albums: artist.albums }) : void 0
2190
2211
  };
2191
2212
  }
2192
2213
  }
@@ -2210,7 +2231,7 @@ import {
2210
2231
  useSongHistory,
2211
2232
  useTrackList as useTrackList6
2212
2233
  } from "@behindthemusictree/app-kit/genre-tree";
2213
- import { Fragment as Fragment14, jsx as jsx36, jsxs as jsxs25 } from "react/jsx-runtime";
2234
+ import { Fragment as Fragment14, jsx as jsx37, jsxs as jsxs25 } from "react/jsx-runtime";
2214
2235
  var numberFormat = new Intl.NumberFormat("en");
2215
2236
  var dateFormat = new Intl.DateTimeFormat("en", { dateStyle: "medium" });
2216
2237
  var externalLink = "text-[var(--color-primary)] hover:underline focus-visible:underline";
@@ -2218,11 +2239,11 @@ function SongHistory({ uuid, getBackendBaseUrl }) {
2218
2239
  const [open, setOpen] = useState9(false);
2219
2240
  const history = useSongHistory(uuid, getBackendBaseUrl, open);
2220
2241
  let body = null;
2221
- if (history.status === "loading") body = /* @__PURE__ */ jsx36(Skeleton6, { className: "h-10", "aria-busy": "true" });
2222
- else if (history.status === "error") body = /* @__PURE__ */ jsx36("p", { className: "text-red-600", children: "Could not load the history." });
2242
+ if (history.status === "loading") body = /* @__PURE__ */ jsx37(Skeleton6, { className: "h-10", "aria-busy": "true" });
2243
+ else if (history.status === "error") body = /* @__PURE__ */ jsx37("p", { className: "text-red-600", children: "Could not load the history." });
2223
2244
  else if (history.status === "success")
2224
- body = history.entries.length === 0 ? /* @__PURE__ */ jsx36("p", { className: "text-[var(--color-text-muted)]", children: "No changes yet." }) : /* @__PURE__ */ jsx36("ul", { className: "divide-y divide-[var(--color-border)]", children: history.entries.map((entry) => /* @__PURE__ */ jsxs25("li", { className: "py-1.5 text-body", children: [
2225
- /* @__PURE__ */ jsx36("span", { className: "font-medium", children: entry.action }),
2245
+ body = history.entries.length === 0 ? /* @__PURE__ */ jsx37("p", { className: "text-[var(--color-text-muted)]", children: "No changes yet." }) : /* @__PURE__ */ jsx37("ul", { className: "divide-y divide-[var(--color-border)]", children: history.entries.map((entry) => /* @__PURE__ */ jsxs25("li", { className: "py-1.5 text-body", children: [
2246
+ /* @__PURE__ */ jsx37("span", { className: "font-medium", children: entry.action }),
2226
2247
  entry.oldValue !== null || entry.newValue !== null ? /* @__PURE__ */ jsxs25("span", { className: "text-[var(--color-text-muted)]", children: [
2227
2248
  " ",
2228
2249
  "\u2014 ",
@@ -2236,9 +2257,9 @@ function SongHistory({ uuid, getBackendBaseUrl }) {
2236
2257
  dateFormat.format(new Date(entry.createdOn))
2237
2258
  ] })
2238
2259
  ] }, entry.uuid)) });
2239
- return /* @__PURE__ */ jsx36(PanelBleed3, { padded: true, className: "border-t border-[var(--color-border)]", children: /* @__PURE__ */ jsxs25("details", { onToggle: (event) => setOpen(event.currentTarget.open), className: "pb-2.5", children: [
2240
- /* @__PURE__ */ jsx36("summary", { className: `mt-2.5 cursor-pointer ${PANEL_LABEL_CLASS}`, children: "History" }),
2241
- /* @__PURE__ */ jsx36("div", { className: "mt-1.5", children: body })
2260
+ return /* @__PURE__ */ jsx37(PanelBleed3, { padded: true, className: "border-t border-[var(--color-border)]", children: /* @__PURE__ */ jsxs25("details", { onToggle: (event) => setOpen(event.currentTarget.open), className: "pb-2.5", children: [
2261
+ /* @__PURE__ */ jsx37("summary", { className: `mt-2.5 cursor-pointer ${PANEL_LABEL_CLASS}`, children: "History" }),
2262
+ /* @__PURE__ */ jsx37("div", { className: "mt-1.5", children: body })
2242
2263
  ] }) });
2243
2264
  }
2244
2265
  function SongPanelContent(props) {
@@ -2257,13 +2278,13 @@ function SongPanelContent(props) {
2257
2278
  const genre = song.genre;
2258
2279
  fields.push({
2259
2280
  label: "Genre",
2260
- value: onSelectGenre ? /* @__PURE__ */ jsx36("button", { type: "button", onClick: () => onSelectGenre(genre), className: "hover:underline", children: genre.name }) : path ? /* @__PURE__ */ jsx36("span", { className: "flex flex-wrap items-center gap-x-1", children: path.map((genrePlaylist, index) => /* @__PURE__ */ jsxs25("span", { children: [
2261
- index > 0 && /* @__PURE__ */ jsx36("span", { "aria-hidden": "true", children: "\u203A " }),
2262
- /* @__PURE__ */ jsx36("button", { type: "button", onClick: () => props.onSelectGenrePlaylist?.(genrePlaylist), className: "hover:underline", children: genrePlaylist.name })
2281
+ value: onSelectGenre ? /* @__PURE__ */ jsx37("button", { type: "button", onClick: () => onSelectGenre(genre), className: "hover:underline", children: genre.name }) : path ? /* @__PURE__ */ jsx37("span", { className: "flex flex-wrap items-center gap-x-1", children: path.map((genrePlaylist, index) => /* @__PURE__ */ jsxs25("span", { children: [
2282
+ index > 0 && /* @__PURE__ */ jsx37("span", { "aria-hidden": "true", children: "\u203A " }),
2283
+ /* @__PURE__ */ jsx37("button", { type: "button", onClick: () => props.onSelectGenrePlaylist?.(genrePlaylist), className: "hover:underline", children: genrePlaylist.name })
2263
2284
  ] }, genrePlaylist.uuid)) }) : song.genre.name
2264
2285
  });
2265
2286
  } else {
2266
- fields.push({ label: "Genre", value: /* @__PURE__ */ jsx36("span", { className: "text-[var(--color-text-muted)]", children: "No genre" }) });
2287
+ fields.push({ label: "Genre", value: /* @__PURE__ */ jsx37("span", { className: "text-[var(--color-text-muted)]", children: "No genre" }) });
2267
2288
  }
2268
2289
  if (song.firstReleaseYear) fields.push({ label: "First released", value: song.firstReleaseYear });
2269
2290
  const viewCount = song.youtubeVideo?.viewCount;
@@ -2273,11 +2294,11 @@ function SongPanelContent(props) {
2273
2294
  const links = [];
2274
2295
  if (musicBrainzUrl)
2275
2296
  links.push(
2276
- /* @__PURE__ */ jsx36("a", { href: musicBrainzUrl, target: "_blank", rel: "noopener noreferrer", className: externalLink, children: "MusicBrainz" }, "mb")
2297
+ /* @__PURE__ */ jsx37("a", { href: musicBrainzUrl, target: "_blank", rel: "noopener noreferrer", className: externalLink, children: "MusicBrainz" }, "mb")
2277
2298
  );
2278
2299
  if (song.youtubeVideo)
2279
2300
  links.push(
2280
- /* @__PURE__ */ jsx36(
2301
+ /* @__PURE__ */ jsx37(
2281
2302
  "a",
2282
2303
  {
2283
2304
  href: `https://www.youtube.com/watch?v=${encodeURIComponent(song.youtubeVideo.id)}`,
@@ -2292,31 +2313,31 @@ function SongPanelContent(props) {
2292
2313
  if (links.length > 0)
2293
2314
  fields.push({
2294
2315
  label: "Links",
2295
- value: /* @__PURE__ */ jsx36("span", { className: "flex gap-3", children: links })
2316
+ value: /* @__PURE__ */ jsx37("span", { className: "flex gap-3", children: links })
2296
2317
  });
2297
2318
  return /* @__PURE__ */ jsxs25(Fragment14, { children: [
2298
- /* @__PURE__ */ jsx36(PanelBleed3, { children: /* @__PURE__ */ jsxs25(ActionBand3, { children: [
2299
- /* @__PURE__ */ jsx36(
2319
+ /* @__PURE__ */ jsx37(PanelBleed3, { children: /* @__PURE__ */ jsxs25(ActionBand3, { children: [
2320
+ /* @__PURE__ */ jsx37(
2300
2321
  ActionButton5,
2301
2322
  {
2302
- icon: isPlaying ? /* @__PURE__ */ jsx36(Pause3, { "aria-hidden": "true", className: "w-5 h-5" }) : /* @__PURE__ */ jsx36(Play3, { "aria-hidden": "true", className: "w-5 h-5" }),
2323
+ icon: isPlaying ? /* @__PURE__ */ jsx37(Pause3, { "aria-hidden": "true", className: "w-5 h-5" }) : /* @__PURE__ */ jsx37(Play3, { "aria-hidden": "true", className: "w-5 h-5" }),
2303
2324
  label: isPlaying ? "Pause" : "Play",
2304
2325
  disabled: !!unplayableReason,
2305
2326
  title: unplayableReason ?? void 0,
2306
2327
  onClick: () => isCurrent ? handlePlayPauseAction() : playNewTrackListFromTrackUuid(song, scope)
2307
2328
  }
2308
2329
  ),
2309
- shareUrl && /* @__PURE__ */ jsx36(ShareActionButton, { target: shareTarget, url: shareUrl })
2330
+ shareUrl && /* @__PURE__ */ jsx37(ShareActionButton, { target: shareTarget, url: shareUrl })
2310
2331
  ] }) }),
2311
- song.artists && song.artists.length > 0 && /* @__PURE__ */ jsx36("p", { className: "text-[var(--color-text-muted)]", children: /* @__PURE__ */ jsx36(ArtistNames3, { artists: song.artists, renderArtistLink }) }),
2312
- /* @__PURE__ */ jsx36(FieldList2, { items: fields }),
2313
- song.essentialGenres && song.essentialGenres.length > 0 && /* @__PURE__ */ jsx36(PanelSection6, { title: "Essential in", children: /* @__PURE__ */ jsx36("ul", { children: song.essentialGenres.map((genre) => /* @__PURE__ */ jsx36("li", { className: "py-0.5", children: genre.name }, genre.uuid)) }) }),
2314
- /* @__PURE__ */ jsx36(SongHistory, { uuid: song.uuid, getBackendBaseUrl })
2332
+ song.artists && song.artists.length > 0 && /* @__PURE__ */ jsx37("p", { className: "text-[var(--color-text-muted)]", children: /* @__PURE__ */ jsx37(ArtistNames3, { artists: song.artists, renderArtistLink }) }),
2333
+ /* @__PURE__ */ jsx37(FieldList2, { items: fields }),
2334
+ song.essentialGenres && song.essentialGenres.length > 0 && /* @__PURE__ */ jsx37(PanelSection6, { title: "Essential in", children: /* @__PURE__ */ jsx37("ul", { children: song.essentialGenres.map((genre) => /* @__PURE__ */ jsx37("li", { className: "py-0.5", children: genre.name }, genre.uuid)) }) }),
2335
+ /* @__PURE__ */ jsx37(SongHistory, { uuid: song.uuid, getBackendBaseUrl })
2315
2336
  ] });
2316
2337
  }
2317
2338
 
2318
2339
  // src/genre-tree/song/useSongPanel.tsx
2319
- import { jsx as jsx37, jsxs as jsxs26 } from "react/jsx-runtime";
2340
+ import { jsx as jsx38, jsxs as jsxs26 } from "react/jsx-runtime";
2320
2341
  function useSongPanel({
2321
2342
  uuid,
2322
2343
  scope,
@@ -2338,23 +2359,23 @@ function useSongPanel({
2338
2359
  ...panel,
2339
2360
  title: "Song",
2340
2361
  content: /* @__PURE__ */ jsxs26("div", { "aria-busy": "true", className: "flex flex-col gap-2.5", children: [
2341
- /* @__PURE__ */ jsx37(Skeleton7, { className: "h-3 w-16" }),
2342
- /* @__PURE__ */ jsx37(Skeleton7, { className: "h-4 w-32" }),
2343
- /* @__PURE__ */ jsx37(Skeleton7, { className: "h-3 w-16" }),
2344
- /* @__PURE__ */ jsx37(Skeleton7, { className: "h-4 w-24" })
2362
+ /* @__PURE__ */ jsx38(Skeleton7, { className: "h-3 w-16" }),
2363
+ /* @__PURE__ */ jsx38(Skeleton7, { className: "h-4 w-32" }),
2364
+ /* @__PURE__ */ jsx38(Skeleton7, { className: "h-3 w-16" }),
2365
+ /* @__PURE__ */ jsx38(Skeleton7, { className: "h-4 w-24" })
2345
2366
  ] })
2346
2367
  };
2347
2368
  case "not-found":
2348
2369
  return {
2349
2370
  ...panel,
2350
2371
  title: "Song not found",
2351
- content: /* @__PURE__ */ jsx37("p", { className: "text-[var(--color-text-muted)]", children: "This song does not exist." })
2372
+ content: /* @__PURE__ */ jsx38("p", { className: "text-[var(--color-text-muted)]", children: "This song does not exist." })
2352
2373
  };
2353
2374
  case "error":
2354
2375
  return {
2355
2376
  ...panel,
2356
2377
  title: "Song",
2357
- content: /* @__PURE__ */ jsx37("p", { className: "text-red-600", children: "Could not load this song." })
2378
+ content: /* @__PURE__ */ jsx38("p", { className: "text-red-600", children: "Could not load this song." })
2358
2379
  };
2359
2380
  case "success": {
2360
2381
  const { song } = result;
@@ -2362,7 +2383,7 @@ function useSongPanel({
2362
2383
  return {
2363
2384
  ...panel,
2364
2385
  title: song.title,
2365
- content: /* @__PURE__ */ jsx37(
2386
+ content: /* @__PURE__ */ jsx38(
2366
2387
  SongPanelContent,
2367
2388
  {
2368
2389
  song,
@@ -2415,12 +2436,12 @@ function useEntityPanel({
2415
2436
  // src/genre-tree/SizeMetricToggle.tsx
2416
2437
  import { MdMusicNote, MdVisibility } from "react-icons/md";
2417
2438
  import { SegmentedControl } from "@behindthemusictree/ui";
2418
- import { jsx as jsx38 } from "react/jsx-runtime";
2439
+ import { jsx as jsx39 } from "react/jsx-runtime";
2419
2440
  function SizeMetricToggle({
2420
2441
  value,
2421
2442
  onChange
2422
2443
  }) {
2423
- return /* @__PURE__ */ jsx38(
2444
+ return /* @__PURE__ */ jsx39(
2424
2445
  SegmentedControl,
2425
2446
  {
2426
2447
  variant: "floating",
@@ -2428,15 +2449,15 @@ function SizeMetricToggle({
2428
2449
  value,
2429
2450
  onChange,
2430
2451
  options: [
2431
- { value: "songs", label: /* @__PURE__ */ jsx38(MdMusicNote, { size: 18, "aria-hidden": true }), "aria-label": "Size by songs", title: "Node size by song count" },
2432
- { value: "views", label: /* @__PURE__ */ jsx38(MdVisibility, { size: 18, "aria-hidden": true }), "aria-label": "Size by views", title: "Node size by song views" }
2452
+ { value: "songs", label: /* @__PURE__ */ jsx39(MdMusicNote, { size: 18, "aria-hidden": true }), "aria-label": "Size by songs", title: "Node size by song count" },
2453
+ { value: "views", label: /* @__PURE__ */ jsx39(MdVisibility, { size: 18, "aria-hidden": true }), "aria-label": "Size by views", title: "Node size by song views" }
2433
2454
  ]
2434
2455
  }
2435
2456
  );
2436
2457
  }
2437
2458
 
2438
2459
  // src/genre-tree/GenreTreeView.tsx
2439
- import { Fragment as Fragment15, jsx as jsx39, jsxs as jsxs27 } from "react/jsx-runtime";
2460
+ import { Fragment as Fragment15, jsx as jsx40, jsxs as jsxs27 } from "react/jsx-runtime";
2440
2461
  var NO_POP_ROOT_TITLE = "This genre tree has no 'Mainstream Pop' root yet";
2441
2462
  var REGIONS_CLASS = "[--gtv-center-left:var(--layout-center-left)] [--gtv-center-right:var(--layout-center-right)] [--gtv-center-top:var(--layout-band-top)]";
2442
2463
  var noop = () => {
@@ -2588,14 +2609,14 @@ function GenreTreeView({
2588
2609
  const openSongRef = useRef3(openSong);
2589
2610
  openSongRef.current = openSong;
2590
2611
  const resolvedRenderSongLink = useCallback6(
2591
- (song) => renderSongLink ? renderSongLink(song) : /* @__PURE__ */ jsx39("button", { type: "button", onClick: () => openSongRef.current(song.uuid), className: "hover:underline", children: song.title }),
2612
+ (song) => renderSongLink ? renderSongLink(song) : /* @__PURE__ */ jsx40("button", { type: "button", onClick: () => openSongRef.current(song.uuid), className: "hover:underline", children: song.title }),
2592
2613
  [renderSongLink]
2593
2614
  );
2594
2615
  const [expandedSongUuid, setExpandedSongUuid] = useState10(null);
2595
2616
  const [expandedArtistUuid, setExpandedArtistUuid] = useState10(null);
2596
2617
  const artistBaseUrl = artistSearchBaseUrl ?? getBackendBaseUrl;
2597
2618
  const songBaseUrl = getBackendBaseUrl;
2598
- const resolvedRenderArtistLink = renderArtistLink ?? ((artist) => artist.isSpecialPurpose ? artist.name : /* @__PURE__ */ jsx39("button", { type: "button", onClick: () => openArtist(artist.uuid), className: "hover:underline", children: artist.name }));
2619
+ const resolvedRenderArtistLink = renderArtistLink ?? ((artist) => artist.isSpecialPurpose ? artist.name : /* @__PURE__ */ jsx40("button", { type: "button", onClick: () => openArtist(artist.uuid), className: "hover:underline", children: artist.name }));
2599
2620
  const getGenrePath = (genre) => {
2600
2621
  const path = [];
2601
2622
  let current = visibleGenrePlaylists.find((playlist) => playlist.criteria?.uuid === genre.uuid);
@@ -2672,9 +2693,10 @@ function GenreTreeView({
2672
2693
  resolveStyle
2673
2694
  );
2674
2695
  const treeStyles = useMemo8(() => toGenreNodeStyles(relations, visibleGenrePlaylists), [relations, visibleGenrePlaylists]);
2675
- const [selectionHistory, setSelectionHistory] = useState10({ current: selectedGenreUuid, previous: null });
2676
- if (selectionHistory.current !== selectedGenreUuid) {
2677
- setSelectionHistory({ current: selectedGenreUuid, previous: selectionHistory.current ?? selectionHistory.previous });
2696
+ const validSelectedGenreUuid = toGenreUuid(selectedGenreUuid);
2697
+ const [selectionHistory, setSelectionHistory] = useState10({ current: validSelectedGenreUuid, previous: null });
2698
+ if (selectionHistory.current !== validSelectedGenreUuid) {
2699
+ setSelectionHistory({ current: validSelectedGenreUuid, previous: selectionHistory.current ?? selectionHistory.previous });
2678
2700
  }
2679
2701
  const handlePrevious = selectionHistory.previous ? () => selectGenre(selectionHistory.previous) : void 0;
2680
2702
  const { showPopup, hidePopup } = usePopup4();
@@ -2706,7 +2728,7 @@ function GenreTreeView({
2706
2728
  const canToggleSide = !readOnly && criteria !== void 0 && criteria !== null && parentPlaylist !== void 0 && parentPlaylist.parent === null;
2707
2729
  const side = criteria?.side ?? "core";
2708
2730
  const popSibling = canToggleSide && side === "core" ? visibleGenrePlaylists.find((sibling) => sibling.parent?.uuid === parentPlaylist.uuid && sibling.uuid !== genrePlaylist?.uuid && sibling.criteria?.side === "pop") : void 0;
2709
- const shareTarget = selectedGenreUuid ? { kind: "genre", genreUuid: selectedGenreUuid, name: criteria?.name ?? "Genre", scope, artistUuid } : void 0;
2731
+ const shareTarget = validSelectedGenreUuid ? { kind: "genre", genreUuid: validSelectedGenreUuid, name: criteria?.name ?? "Genre", scope, artistUuid } : void 0;
2710
2732
  const shareUrl = shareTarget ? getShareUrlRef.current?.(shareTarget) ?? void 0 : void 0;
2711
2733
  const genreContent = useGenreDetailContent({
2712
2734
  genreUuid: selectedGenreUuid,
@@ -2728,7 +2750,7 @@ function GenreTreeView({
2728
2750
  onAddChild: readOnly || !criteria ? void 0 : () => handleGenreCreationAction(criteria),
2729
2751
  onRename: readOnly || !criteria ? void 0 : () => handleGenreRenameAction(criteria),
2730
2752
  onChangeParent: readOnly || !genrePlaylist || !criteria ? void 0 : () => showPopup(
2731
- /* @__PURE__ */ jsx39(
2753
+ /* @__PURE__ */ jsx40(
2732
2754
  ChangeParentPopup,
2733
2755
  {
2734
2756
  genrePlaylist: { ...genrePlaylist, criteria },
@@ -2772,7 +2794,7 @@ function GenreTreeView({
2772
2794
  onClose: otherPanel.onClose,
2773
2795
  searchName: otherPanel.title
2774
2796
  };
2775
- } else if (selectedGenreUuid) {
2797
+ } else if (selectedGenreUuid !== null) {
2776
2798
  activePanel = {
2777
2799
  title: genreContent.title,
2778
2800
  content: genreContent.content,
@@ -2789,19 +2811,19 @@ function GenreTreeView({
2789
2811
  edge: activePanel.expanded ? { label: "Shrink panel", onClick: () => activePanel.onExpandedChange?.(false) } : isCollapsed ? null : { label: "Hide panel", onClick: () => changeCollapsed(true) },
2790
2812
  center: activePanel.expanded ? null : isCollapsed ? { label: "Show panel", onClick: () => changeCollapsed(false) } : activePanel.onExpandedChange ? { label: "Extend panel", onClick: () => activePanel.onExpandedChange?.(true) } : null
2791
2813
  } : void 0;
2792
- const expandButton = placement === "default" && activePanel?.onExpandedChange ? /* @__PURE__ */ jsx39(
2814
+ const expandButton = placement === "default" && activePanel?.onExpandedChange ? /* @__PURE__ */ jsx40(
2793
2815
  IconButton2,
2794
2816
  {
2795
2817
  "aria-label": activePanel.expanded ? "Shrink panel" : "Extend panel",
2796
2818
  style: activePanel.headerAccent && { color: "inherit" },
2797
2819
  onClick: () => activePanel.onExpandedChange?.(!activePanel.expanded),
2798
- children: activePanel.expanded ? /* @__PURE__ */ jsx39(Minimize22, { "aria-hidden": "true", className: "h-4 w-4" }) : /* @__PURE__ */ jsx39(Maximize22, { "aria-hidden": "true", className: "h-4 w-4" })
2820
+ children: activePanel.expanded ? /* @__PURE__ */ jsx40(Minimize22, { "aria-hidden": "true", className: "h-4 w-4" }) : /* @__PURE__ */ jsx40(Maximize22, { "aria-hidden": "true", className: "h-4 w-4" })
2799
2821
  }
2800
2822
  ) : null;
2801
2823
  const renderArtistFilter = () => {
2802
2824
  if (isLoading || !filterArtist) return null;
2803
2825
  return /* @__PURE__ */ jsxs27("span", { className: "inline-flex items-center overflow-hidden rounded-full bg-[var(--color-neutral-100)] text-body", children: [
2804
- /* @__PURE__ */ jsx39(
2826
+ /* @__PURE__ */ jsx40(
2805
2827
  "button",
2806
2828
  {
2807
2829
  type: "button",
@@ -2813,7 +2835,7 @@ function GenreTreeView({
2813
2835
  children: filterArtist.name
2814
2836
  }
2815
2837
  ),
2816
- /* @__PURE__ */ jsx39(
2838
+ /* @__PURE__ */ jsx40(
2817
2839
  "button",
2818
2840
  {
2819
2841
  type: "button",
@@ -2826,7 +2848,7 @@ function GenreTreeView({
2826
2848
  ] });
2827
2849
  };
2828
2850
  const actions = /* @__PURE__ */ jsxs27(Fragment15, { children: [
2829
- !isLoading && !isError && /* @__PURE__ */ jsx39(
2851
+ !isLoading && !isError && /* @__PURE__ */ jsx40(
2830
2852
  GenreTreeSearch,
2831
2853
  {
2832
2854
  artistBaseUrl,
@@ -2859,9 +2881,9 @@ function GenreTreeView({
2859
2881
  extraGroups: extraSearchGroups
2860
2882
  }
2861
2883
  ),
2862
- !isLoading && !isError && filterArtist && /* @__PURE__ */ jsx39("div", { className: "hidden w-full justify-center max-md:flex", children: renderArtistFilter() }),
2884
+ !isLoading && !isError && filterArtist && /* @__PURE__ */ jsx40("div", { className: "hidden w-full justify-center max-md:flex", children: renderArtistFilter() }),
2863
2885
  /* @__PURE__ */ jsxs27("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: [
2864
- !isLoading && !isError && !isControlled && /* @__PURE__ */ jsx39(
2886
+ !isLoading && !isError && !isControlled && /* @__PURE__ */ jsx40(
2865
2887
  SegmentedControl2,
2866
2888
  {
2867
2889
  "aria-label": "Tree view mode",
@@ -2875,13 +2897,13 @@ function GenreTreeView({
2875
2897
  ]
2876
2898
  }
2877
2899
  ),
2878
- !isLoading && !isError && showDisplayOptions && /* @__PURE__ */ jsx39(
2900
+ !isLoading && !isError && showDisplayOptions && /* @__PURE__ */ jsx40(
2879
2901
  "div",
2880
2902
  {
2881
2903
  className: "flex items-center gap-1",
2882
2904
  role: "group",
2883
2905
  "aria-label": "Tree display options",
2884
- children: !isRotationControlled && viewMode !== "stacked" && viewMode !== "outline" && /* @__PURE__ */ jsx39(
2906
+ children: !isRotationControlled && viewMode !== "stacked" && viewMode !== "outline" && /* @__PURE__ */ jsx40(
2885
2907
  Button,
2886
2908
  {
2887
2909
  variant: allowWheelRotation ? "default" : "outline",
@@ -2892,7 +2914,7 @@ function GenreTreeView({
2892
2914
  )
2893
2915
  }
2894
2916
  ),
2895
- !isLoading && !isError && !readOnly && showAddRoot && /* @__PURE__ */ jsx39(
2917
+ !isLoading && !isError && !readOnly && showAddRoot && /* @__PURE__ */ jsx40(
2896
2918
  IconTextButton,
2897
2919
  {
2898
2920
  icon: Plus2,
@@ -2908,32 +2930,32 @@ function GenreTreeView({
2908
2930
  "data-left-panel": isPanelShown && !isCollapsed ? void 0 : "hidden",
2909
2931
  className: ["genre-tree-view relative flex flex-col h-full", placement === "regions" && REGIONS_CLASS, className2].filter(Boolean).join(" "),
2910
2932
  children: [
2911
- /* @__PURE__ */ jsx39(
2933
+ /* @__PURE__ */ jsx40(
2912
2934
  "div",
2913
2935
  {
2914
2936
  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))] md:left-[var(--layout-panel-inset)] md:top-[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"}`,
2915
2937
  children: actions
2916
2938
  }
2917
2939
  ),
2918
- !isLoading && !isError && filterArtist && /* @__PURE__ */ jsx39(
2940
+ !isLoading && !isError && filterArtist && /* @__PURE__ */ jsx40(
2919
2941
  "div",
2920
2942
  {
2921
2943
  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))] md:top-[var(--layout-panel-inset)]" : "top-5"}`,
2922
2944
  children: renderArtistFilter()
2923
2945
  }
2924
2946
  ),
2925
- /* @__PURE__ */ jsx39("div", { className: "content-container flex-1 min-h-0 flex flex-row gap-4", children: /* @__PURE__ */ jsxs27("div", { className: "tree-view-container relative flex-1 min-w-0 flex flex-col h-full", children: [
2926
- !isLoading && !isError && viewMode === "outline" && floatingControlsLeading && /* @__PURE__ */ jsx39("div", { className: "absolute bottom-3 right-3 z-10", children: floatingControlsLeading }),
2927
- isLoading ? /* @__PURE__ */ jsx39(GenreTreeViewSkeleton, { viewMode }) : isError ? /* @__PURE__ */ jsxs27("p", { role: "alert", className: "m-auto text-center", children: [
2947
+ /* @__PURE__ */ jsx40("div", { className: "content-container flex-1 min-h-0 flex flex-row gap-4", children: /* @__PURE__ */ jsxs27("div", { className: "tree-view-container relative flex-1 min-w-0 flex flex-col h-full", children: [
2948
+ !isLoading && !isError && viewMode === "outline" && floatingControlsLeading && /* @__PURE__ */ jsx40("div", { className: "absolute bottom-3 right-3 z-10", children: floatingControlsLeading }),
2949
+ isLoading ? /* @__PURE__ */ jsx40(GenreTreeViewSkeleton, { viewMode }) : isError ? /* @__PURE__ */ jsxs27("p", { role: "alert", className: "m-auto text-center", children: [
2928
2950
  "Couldn't load the genre tree.",
2929
2951
  " ",
2930
- /* @__PURE__ */ jsx39("button", { type: "button", className: "underline", onClick: refetch, children: "Retry" })
2952
+ /* @__PURE__ */ jsx40("button", { type: "button", className: "underline", onClick: refetch, children: "Retry" })
2931
2953
  ] }) : viewMode === "wheel" ? (
2932
2954
  // The info panel's top edge aligns with the tree's; the search bar floats `--layout-panel-inset` below it,
2933
2955
  // and the header inset (the bar's height plus that inset) extends the header's fill behind the bar,
2934
2956
  // Google Maps style; the 8px bottom padding mirrors the inset above the bar, and the hidden title's height is zeroed.
2935
2957
  // The panel's own Zoom in button is hidden: the search bar carries it.
2936
- /* @__PURE__ */ jsx39("div", { className: "tree-container search-overlay relative flex-1 w-full min-h-0 ", children: /* @__PURE__ */ jsx39(GenreTreeWheelHandoff, { skeleton: /* @__PURE__ */ jsx39(GenreTreeWheelSkeleton, {}), onRevealedChange: setIsWheelRevealed, children: /* @__PURE__ */ jsx39(
2958
+ /* @__PURE__ */ jsx40("div", { className: "tree-container search-overlay relative flex-1 w-full min-h-0 ", children: /* @__PURE__ */ jsx40(GenreTreeWheelHandoff, { skeleton: /* @__PURE__ */ jsx40(GenreTreeWheelSkeleton, {}), onRevealedChange: setIsWheelRevealed, children: /* @__PURE__ */ jsx40(
2937
2959
  GenrePlaylistTreeWheel,
2938
2960
  {
2939
2961
  scope,
@@ -2953,7 +2975,7 @@ function GenreTreeView({
2953
2975
  busy,
2954
2976
  floatingControlsLeading: floatingControlsLeading || canSizeByViews ? /* @__PURE__ */ jsxs27("div", { className: "flex items-center gap-2", children: [
2955
2977
  floatingControlsLeading,
2956
- canSizeByViews && /* @__PURE__ */ jsx39(SizeMetricToggle, { value: sizeMetric, onChange: changeSizeMetric })
2978
+ canSizeByViews && /* @__PURE__ */ jsx40(SizeMetricToggle, { value: sizeMetric, onChange: changeSizeMetric })
2957
2979
  ] }) : void 0,
2958
2980
  defaultViewport,
2959
2981
  viewportRestore,
@@ -2962,7 +2984,7 @@ function GenreTreeView({
2962
2984
  ) }) })
2963
2985
  ) : viewMode === "pop-core" ? (
2964
2986
  // Same as the wheel: the header inset extends its fill behind the floating search bar, Google Maps style.
2965
- /* @__PURE__ */ jsx39("div", { className: "tree-container search-overlay relative flex-1 w-full min-h-0 ", children: /* @__PURE__ */ jsx39(GenreTreeWheelHandoff, { skeleton: /* @__PURE__ */ jsx39(GenreTreeWheelSkeleton, {}), onRevealedChange: setIsWheelRevealed, children: /* @__PURE__ */ jsx39(
2987
+ /* @__PURE__ */ jsx40("div", { className: "tree-container search-overlay relative flex-1 w-full min-h-0 ", children: /* @__PURE__ */ jsx40(GenreTreeWheelHandoff, { skeleton: /* @__PURE__ */ jsx40(GenreTreeWheelSkeleton, {}), onRevealedChange: setIsWheelRevealed, children: /* @__PURE__ */ jsx40(
2966
2988
  GenrePlaylistTreeWheelRadialPopCore,
2967
2989
  {
2968
2990
  scope,
@@ -2982,7 +3004,7 @@ function GenreTreeView({
2982
3004
  busy,
2983
3005
  floatingControlsLeading: floatingControlsLeading || canSizeByViews ? /* @__PURE__ */ jsxs27("div", { className: "flex items-center gap-2", children: [
2984
3006
  floatingControlsLeading,
2985
- canSizeByViews && /* @__PURE__ */ jsx39(SizeMetricToggle, { value: sizeMetric, onChange: changeSizeMetric })
3007
+ canSizeByViews && /* @__PURE__ */ jsx40(SizeMetricToggle, { value: sizeMetric, onChange: changeSizeMetric })
2986
3008
  ] }) : void 0,
2987
3009
  defaultViewport,
2988
3010
  viewportRestore,
@@ -2990,7 +3012,7 @@ function GenreTreeView({
2990
3012
  ...focus
2991
3013
  }
2992
3014
  ) }) })
2993
- ) : viewMode === "outline" ? /* @__PURE__ */ jsx39("div", { className: "tree-container flex-1 min-h-0 w-full overflow-y-auto relative", children: /* @__PURE__ */ jsx39(
3015
+ ) : viewMode === "outline" ? /* @__PURE__ */ jsx40("div", { className: "tree-container flex-1 min-h-0 w-full overflow-y-auto relative", children: /* @__PURE__ */ jsx40(
2994
3016
  GenrePlaylistTreeWheelRadialPopCore,
2995
3017
  {
2996
3018
  outline: true,
@@ -3007,13 +3029,13 @@ function GenreTreeView({
3007
3029
  selectedNodeId,
3008
3030
  readOnly
3009
3031
  }
3010
- ) }) : /* @__PURE__ */ jsx39("div", { className: "tree-container relative flex flex-col gap-4 w-full pt-14 overflow-x-auto overflow-y-auto text-[var(--color-text)]", children: Object.entries(groupedGenrePlaylistsByRoot).map(
3032
+ ) }) : /* @__PURE__ */ jsx40("div", { className: "tree-container relative flex flex-col gap-4 w-full pt-14 overflow-x-auto overflow-y-auto text-[var(--color-text)]", children: Object.entries(groupedGenrePlaylistsByRoot).map(
3011
3033
  ([uuid, genrePlaylistTreePerRoot]) => {
3012
- return /* @__PURE__ */ jsx39(
3034
+ return /* @__PURE__ */ jsx40(
3013
3035
  "div",
3014
3036
  {
3015
3037
  className: "tree-per-root-container relative shrink-0 h-[28rem] mt-2 mr-16 p-2 bg-[var(--color-neutral-100)] rounded-lg",
3016
- children: /* @__PURE__ */ jsx39("div", { className: "graph-container relative z-10 h-full", children: /* @__PURE__ */ jsx39(
3038
+ children: /* @__PURE__ */ jsx40("div", { className: "graph-container relative z-10 h-full", children: /* @__PURE__ */ jsx40(
3017
3039
  GenrePlaylistTreePerRoot,
3018
3040
  {
3019
3041
  scope,
@@ -3040,7 +3062,7 @@ function GenreTreeView({
3040
3062
  );
3041
3063
  }
3042
3064
  ) }),
3043
- /* @__PURE__ */ jsx39(
3065
+ /* @__PURE__ */ jsx40(
3044
3066
  DetailPanel2,
3045
3067
  {
3046
3068
  ref: panelRef,
@@ -3081,7 +3103,7 @@ import { ListMusic } from "lucide-react";
3081
3103
  import { useTrackList as useTrackList7 } from "@behindthemusictree/app-kit/genre-tree";
3082
3104
  import { TrackListOriginType as TrackListOriginType3 } from "@behindthemusictree/app-kit/genre-tree";
3083
3105
  import { useTrackNoun as useTrackNoun3 } from "@behindthemusictree/app-kit/track-noun";
3084
- import { jsx as jsx40, jsxs as jsxs28 } from "react/jsx-runtime";
3106
+ import { jsx as jsx41, jsxs as jsxs28 } from "react/jsx-runtime";
3085
3107
  var useBrowserLayoutEffect = typeof window === "undefined" ? useEffect7 : useLayoutEffect2;
3086
3108
  function TrackListSidebar({
3087
3109
  className: className2 = "",
@@ -3193,15 +3215,15 @@ function TrackListSidebar({
3193
3215
  /* @__PURE__ */ jsxs28("div", { className: "header flex h-16 items-center px-4 py-2 text-[var(--color-text)]", children: [
3194
3216
  /* @__PURE__ */ jsxs28("div", { className: "min-w-0 flex flex-1 flex-col justify-center", children: [
3195
3217
  /* @__PURE__ */ jsxs28("div", { className: "origin flex text-xl", children: [
3196
- /* @__PURE__ */ jsx40("div", { className: "from h-auto flex flex-col justify-center items-center mr-2", children: "From" }),
3197
- /* @__PURE__ */ jsx40("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__ */ jsx40("div", { className: "name max-w-full truncate", children: trackList.origin.label }) })
3218
+ /* @__PURE__ */ jsx41("div", { className: "from h-auto flex flex-col justify-center items-center mr-2", children: "From" }),
3219
+ /* @__PURE__ */ jsx41("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__ */ jsx41("div", { className: "name max-w-full truncate", children: trackList.origin.label }) })
3198
3220
  ] }),
3199
3221
  /* @__PURE__ */ jsxs28("div", { className: "info flex items-center text-body text-[var(--color-text-muted)]", children: [
3200
3222
  trackList.origin.type === TrackListOriginType3.GENRE_PLAYLIST ? "Genre \u2022 " : trackList.origin.type === TrackListOriginType3.ARTIST ? "Artist \u2022 " : `${trackNoun.singular} playlist \u2022 `,
3201
3223
  `${trackList.total} ${trackList.total > 1 ? trackNoun.plural : trackNoun.singular}`
3202
3224
  ] })
3203
3225
  ] }),
3204
- (isCollapsed && onExpand || !isCollapsed && onMinimize) && /* @__PURE__ */ jsx40(
3226
+ (isCollapsed && onExpand || !isCollapsed && onMinimize) && /* @__PURE__ */ jsx41(
3205
3227
  "button",
3206
3228
  {
3207
3229
  type: "button",
@@ -3210,7 +3232,7 @@ function TrackListSidebar({
3210
3232
  "aria-expanded": !isCollapsed,
3211
3233
  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"}`,
3212
3234
  onClick: isCollapsed ? onExpand : onMinimize,
3213
- children: /* @__PURE__ */ jsx40(ListMusic, { "aria-hidden": "true", className: "h-5 w-5" })
3235
+ children: /* @__PURE__ */ jsx41(ListMusic, { "aria-hidden": "true", className: "h-5 w-5" })
3214
3236
  }
3215
3237
  )
3216
3238
  ] }),
@@ -3224,7 +3246,7 @@ function TrackListSidebar({
3224
3246
  onPointerDown: markUserScrollIntent,
3225
3247
  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",
3226
3248
  children: [
3227
- trackList.tracks.map((track, index) => /* @__PURE__ */ jsx40("li", { "data-track-uuid": track.uuid, children: /* @__PURE__ */ jsx40(
3249
+ trackList.tracks.map((track, index) => /* @__PURE__ */ jsx41("li", { "data-track-uuid": track.uuid, children: /* @__PURE__ */ jsx41(
3228
3250
  TrackItem,
3229
3251
  {
3230
3252
  track,
@@ -3235,7 +3257,7 @@ function TrackListSidebar({
3235
3257
  renderSongLink
3236
3258
  }
3237
3259
  ) }, track.uuid)),
3238
- hasMore && /* @__PURE__ */ jsx40("li", { ref: loadMoreSentinelRef, "aria-hidden": "true", className: "h-px" })
3260
+ hasMore && /* @__PURE__ */ jsx41("li", { ref: loadMoreSentinelRef, "aria-hidden": "true", className: "h-px" })
3239
3261
  ]
3240
3262
  }
3241
3263
  )
@@ -3260,7 +3282,7 @@ import { PanelEdgeTab } from "@behindthemusictree/ui";
3260
3282
  import { Pause as Pause4, Play as Play4, SkipBack, SkipForward, Volume2, VolumeX } from "lucide-react";
3261
3283
  import { formatTime, useTrackList as useTrackList8 } from "@behindthemusictree/app-kit/genre-tree";
3262
3284
  import { PlayStates as PlayStates4, useCurrentTime, usePlayer as usePlayer11 } from "@behindthemusictree/app-kit/player";
3263
- import { jsx as jsx41, jsxs as jsxs29 } from "react/jsx-runtime";
3285
+ import { jsx as jsx42, jsxs as jsxs29 } from "react/jsx-runtime";
3264
3286
  function PlayerControls() {
3265
3287
  const {
3266
3288
  duration,
@@ -3286,8 +3308,8 @@ function PlayerControls() {
3286
3308
  className: "player-controls flex shrink-0 flex-col gap-2 border-b border-zinc-800 px-3 py-2 text-zinc-200",
3287
3309
  children: [
3288
3310
  /* @__PURE__ */ jsxs29("div", { className: "flex min-w-0 items-center gap-2 text-xs tabular-nums text-zinc-400", children: [
3289
- /* @__PURE__ */ jsx41("span", { "aria-label": "Elapsed time", children: formatTime(safeCurrentTime) }),
3290
- /* @__PURE__ */ jsx41(
3311
+ /* @__PURE__ */ jsx42("span", { "aria-label": "Elapsed time", children: formatTime(safeCurrentTime) }),
3312
+ /* @__PURE__ */ jsx42(
3291
3313
  "input",
3292
3314
  {
3293
3315
  "aria-label": "Seek",
@@ -3301,11 +3323,11 @@ function PlayerControls() {
3301
3323
  onChange: (event) => seekTo(Number(event.currentTarget.value))
3302
3324
  }
3303
3325
  ),
3304
- /* @__PURE__ */ jsx41("span", { "aria-label": "Duration", children: formatTime(safeDuration) })
3326
+ /* @__PURE__ */ jsx42("span", { "aria-label": "Duration", children: formatTime(safeDuration) })
3305
3327
  ] }),
3306
3328
  /* @__PURE__ */ jsxs29("div", { className: "grid min-h-9 grid-cols-[1fr_auto_1fr] items-center gap-3", children: [
3307
3329
  /* @__PURE__ */ jsxs29("div", { className: "col-start-2 flex items-center justify-center gap-2", children: [
3308
- /* @__PURE__ */ jsx41(
3330
+ /* @__PURE__ */ jsx42(
3309
3331
  "button",
3310
3332
  {
3311
3333
  type: "button",
@@ -3314,10 +3336,10 @@ function PlayerControls() {
3314
3336
  disabled: !isReady || !hasPreviousTrack,
3315
3337
  onClick: playPreviousTrack,
3316
3338
  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",
3317
- children: /* @__PURE__ */ jsx41(SkipBack, { "aria-hidden": "true", className: "h-4 w-4" })
3339
+ children: /* @__PURE__ */ jsx42(SkipBack, { "aria-hidden": "true", className: "h-4 w-4" })
3318
3340
  }
3319
3341
  ),
3320
- /* @__PURE__ */ jsx41(
3342
+ /* @__PURE__ */ jsx42(
3321
3343
  "button",
3322
3344
  {
3323
3345
  type: "button",
@@ -3326,10 +3348,10 @@ function PlayerControls() {
3326
3348
  disabled: !isReady,
3327
3349
  onClick: handlePlayPauseAction,
3328
3350
  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",
3329
- children: isPlaying ? /* @__PURE__ */ jsx41(Pause4, { "aria-hidden": "true", className: "h-4 w-4", fill: "currentColor" }) : /* @__PURE__ */ jsx41(Play4, { "aria-hidden": "true", className: "h-4 w-4", fill: "currentColor" })
3351
+ children: isPlaying ? /* @__PURE__ */ jsx42(Pause4, { "aria-hidden": "true", className: "h-4 w-4", fill: "currentColor" }) : /* @__PURE__ */ jsx42(Play4, { "aria-hidden": "true", className: "h-4 w-4", fill: "currentColor" })
3330
3352
  }
3331
3353
  ),
3332
- /* @__PURE__ */ jsx41(
3354
+ /* @__PURE__ */ jsx42(
3333
3355
  "button",
3334
3356
  {
3335
3357
  type: "button",
@@ -3338,14 +3360,14 @@ function PlayerControls() {
3338
3360
  disabled: !isReady || !hasNextTrack,
3339
3361
  onClick: playNextTrack,
3340
3362
  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",
3341
- children: /* @__PURE__ */ jsx41(SkipForward, { "aria-hidden": "true", className: "h-4 w-4" })
3363
+ children: /* @__PURE__ */ jsx42(SkipForward, { "aria-hidden": "true", className: "h-4 w-4" })
3342
3364
  }
3343
3365
  )
3344
3366
  ] }),
3345
3367
  /* @__PURE__ */ jsxs29("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: [
3346
- volume === 0 ? /* @__PURE__ */ jsx41(VolumeX, { "aria-hidden": "true", className: "h-[0.7rem] w-[0.7rem] shrink-0" }) : /* @__PURE__ */ jsx41(Volume2, { "aria-hidden": "true", className: "h-[0.7rem] w-[0.7rem] shrink-0" }),
3347
- /* @__PURE__ */ jsx41("span", { className: "sr-only", children: "Volume" }),
3348
- /* @__PURE__ */ jsx41(
3368
+ volume === 0 ? /* @__PURE__ */ jsx42(VolumeX, { "aria-hidden": "true", className: "h-[0.7rem] w-[0.7rem] shrink-0" }) : /* @__PURE__ */ jsx42(Volume2, { "aria-hidden": "true", className: "h-[0.7rem] w-[0.7rem] shrink-0" }),
3369
+ /* @__PURE__ */ jsx42("span", { className: "sr-only", children: "Volume" }),
3370
+ /* @__PURE__ */ jsx42(
3349
3371
  "input",
3350
3372
  {
3351
3373
  "aria-label": "Volume",
@@ -3367,7 +3389,7 @@ function PlayerControls() {
3367
3389
  }
3368
3390
 
3369
3391
  // src/genre-tree/PlayerPanel.tsx
3370
- import { jsx as jsx42, jsxs as jsxs30 } from "react/jsx-runtime";
3392
+ import { jsx as jsx43, jsxs as jsxs30 } from "react/jsx-runtime";
3371
3393
  var PANEL_ID = "player-panel";
3372
3394
  var DEFAULT_EXTENDED_WIDTH = "calc(100vw - var(--layout-center-left) - var(--layout-gutter))";
3373
3395
  function PlayerPanel({
@@ -3420,7 +3442,7 @@ function PlayerPanel({
3420
3442
  ),
3421
3443
  style: isExtended ? { width: extendedWidth } : void 0,
3422
3444
  children: [
3423
- /* @__PURE__ */ jsx42(
3445
+ /* @__PURE__ */ jsx43(
3424
3446
  PanelEdgeTab,
3425
3447
  {
3426
3448
  side: "right",
@@ -3439,19 +3461,19 @@ function PlayerPanel({
3439
3461
  ),
3440
3462
  children: [
3441
3463
  /* @__PURE__ */ jsxs30("div", { className: "flex w-[var(--layout-right-w)] shrink-0 flex-col", children: [
3442
- /* @__PURE__ */ jsx42("div", { className: "flex shrink-0 items-center justify-between gap-2 border-b border-zinc-800 p-2", children: /* @__PURE__ */ jsx42("span", { className: "min-w-0 truncate text-sm font-semibold", children: loadedTrackTitle || lastLoadedTrackTitle }) }),
3464
+ /* @__PURE__ */ jsx43("div", { className: "flex shrink-0 items-center justify-between gap-2 border-b border-zinc-800 p-2", children: /* @__PURE__ */ jsx43("span", { className: "min-w-0 truncate text-sm font-semibold", children: loadedTrackTitle || lastLoadedTrackTitle }) }),
3443
3465
  /* @__PURE__ */ jsxs30("div", { className: "relative w-full shrink-0", children: [
3444
- /* @__PURE__ */ jsx42(PlayerVideoSurface, { className: "aspect-video w-full bg-black" }),
3466
+ /* @__PURE__ */ jsx43(PlayerVideoSurface, { className: "aspect-video w-full bg-black" }),
3445
3467
  playerTrackObject?.loadError && /* @__PURE__ */ jsxs30("span", { className: "absolute bottom-2 left-2 right-2 text-xs text-red-400", children: [
3446
3468
  playerTrackObject.loadError,
3447
- /* @__PURE__ */ jsx42("button", { type: "button", className: "ml-2 underline", onClick: () => {
3469
+ /* @__PURE__ */ jsx43("button", { type: "button", className: "ml-2 underline", onClick: () => {
3448
3470
  void retryPlayback();
3449
3471
  }, children: "Retry" })
3450
3472
  ] })
3451
3473
  ] }),
3452
- /* @__PURE__ */ jsx42(PlayerControls, {})
3474
+ /* @__PURE__ */ jsx43(PlayerControls, {})
3453
3475
  ] }),
3454
- /* @__PURE__ */ jsx42(
3476
+ /* @__PURE__ */ jsx43(
3455
3477
  TrackListSidebar,
3456
3478
  {
3457
3479
  layout: "inline",
@@ -3478,11 +3500,11 @@ function PlayerPanel({
3478
3500
  import { v4 as uuidv4 } from "uuid";
3479
3501
  import { AiFillStar, AiOutlineStar } from "react-icons/ai";
3480
3502
  import { FORM_RATING_NULL_VALUE } from "@behindthemusictree/app-kit/genre-tree";
3481
- import { jsx as jsx43, jsxs as jsxs31 } from "react/jsx-runtime";
3503
+ import { jsx as jsx44, jsxs as jsxs31 } from "react/jsx-runtime";
3482
3504
  function Rating({ rating, handleChange }) {
3483
3505
  const instanceId = uuidv4();
3484
- return /* @__PURE__ */ jsx43("div", { className: "flex items-center", children: [...Array(6)].map((_, i) => /* @__PURE__ */ jsxs31("div", { className: `relative ${i === 0 ? "mr-4" : "mr-1"}`, children: [
3485
- /* @__PURE__ */ jsx43(
3506
+ return /* @__PURE__ */ jsx44("div", { className: "flex items-center", children: [...Array(6)].map((_, i) => /* @__PURE__ */ jsxs31("div", { className: `relative ${i === 0 ? "mr-4" : "mr-1"}`, children: [
3507
+ /* @__PURE__ */ jsx44(
3486
3508
  "input",
3487
3509
  {
3488
3510
  id: `star-${instanceId}-${i}`,
@@ -3494,16 +3516,16 @@ function Rating({ rating, handleChange }) {
3494
3516
  onChange: handleChange
3495
3517
  }
3496
3518
  ),
3497
- /* @__PURE__ */ jsx43("label", { htmlFor: `star-${instanceId}-${i}`, className: "cursor-pointer", children: i === 0 ? /* @__PURE__ */ jsxs31("div", { className: "flex", children: [
3498
- /* @__PURE__ */ jsx43(
3519
+ /* @__PURE__ */ jsx44("label", { htmlFor: `star-${instanceId}-${i}`, className: "cursor-pointer", children: i === 0 ? /* @__PURE__ */ jsxs31("div", { className: "flex", children: [
3520
+ /* @__PURE__ */ jsx44(
3499
3521
  AiOutlineStar,
3500
3522
  {
3501
3523
  className: `w-7 h-7 absolute top-0 left-0 ${rating === FORM_RATING_NULL_VALUE ? "text-[var(--color-accent-rating)]" : "text-[var(--color-text-muted)]"}`
3502
3524
  }
3503
3525
  ),
3504
- /* @__PURE__ */ jsx43("span", { className: "flex-grow w-8" }),
3526
+ /* @__PURE__ */ jsx44("span", { className: "flex-grow w-8" }),
3505
3527
  " "
3506
- ] }) : /* @__PURE__ */ jsx43(
3528
+ ] }) : /* @__PURE__ */ jsx44(
3507
3529
  AiFillStar,
3508
3530
  {
3509
3531
  className: `w-7 h-7 absolute top-0 left-0 ${(rating ?? -1) >= i ? "text-[var(--color-accent-rating)]" : "text-[var(--color-text-muted)]"}`
@@ -3515,7 +3537,7 @@ function Rating({ rating, handleChange }) {
3515
3537
  // src/genre-tree/EntityDetailPanel.tsx
3516
3538
  import { useState as useState12 } from "react";
3517
3539
  import { DetailPanel as DetailPanel3 } from "@behindthemusictree/ui";
3518
- import { Fragment as Fragment16, jsx as jsx44, jsxs as jsxs32 } from "react/jsx-runtime";
3540
+ import { Fragment as Fragment16, jsx as jsx45, jsxs as jsxs32 } from "react/jsx-runtime";
3519
3541
  function EntityDetailPanel({
3520
3542
  scope,
3521
3543
  getBackendBaseUrl,
@@ -3538,8 +3560,8 @@ function EntityDetailPanel({
3538
3560
  getBackendBaseUrl,
3539
3561
  getArtistBackendBaseUrl,
3540
3562
  getShareUrl,
3541
- renderArtistLink: renderArtistLink ?? ((artist) => artist.isSpecialPurpose ? artist.name : /* @__PURE__ */ jsx44("button", { type: "button", onClick: () => onArtistUuidChange(artist.uuid), className: "hover:underline", children: artist.name })),
3542
- renderSongLink: renderSongLink ?? ((song) => /* @__PURE__ */ jsx44("button", { type: "button", onClick: () => onSongUuidChange(song.uuid), className: "hover:underline", children: song.title })),
3563
+ renderArtistLink: renderArtistLink ?? ((artist) => artist.isSpecialPurpose ? artist.name : /* @__PURE__ */ jsx45("button", { type: "button", onClick: () => onArtistUuidChange(artist.uuid), className: "hover:underline", children: artist.name })),
3564
+ renderSongLink: renderSongLink ?? ((song) => /* @__PURE__ */ jsx45("button", { type: "button", onClick: () => onSongUuidChange(song.uuid), className: "hover:underline", children: song.title })),
3543
3565
  song: {
3544
3566
  uuid: songUuid,
3545
3567
  expanded: songUuid !== null && expandedSongUuid === songUuid,
@@ -3555,7 +3577,7 @@ function EntityDetailPanel({
3555
3577
  ...navigation
3556
3578
  });
3557
3579
  if (!panel) return null;
3558
- return /* @__PURE__ */ jsx44(
3580
+ return /* @__PURE__ */ jsx45(
3559
3581
  DetailPanel3,
3560
3582
  {
3561
3583
  side: "left",
@@ -3601,4 +3623,4 @@ export {
3601
3623
  GenreTreeViewSkeleton2 as GenreTreeViewSkeleton,
3602
3624
  GenreTreeOutlineSkeleton
3603
3625
  };
3604
- //# sourceMappingURL=chunk-DPYUE6WD.js.map
3626
+ //# sourceMappingURL=chunk-GR4PCPCU.js.map