@behindthemusictree/app-kit-ui 40.1.0 → 42.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,20 +1735,22 @@ 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
 
1709
1742
  // src/genre-tree/artist/useArtistPanel.tsx
1710
1743
  import { Skeleton as Skeleton5 } from "@behindthemusictree/ui";
1711
- import { useArtist } from "@behindthemusictree/app-kit/genre-tree";
1744
+ import {
1745
+ useArtist
1746
+ } from "@behindthemusictree/app-kit/genre-tree";
1712
1747
 
1713
1748
  // src/genre-tree/artist/ArtistActionBand.tsx
1714
1749
  import { Pause as Pause2, Play as Play2 } from "lucide-react";
1715
1750
  import { ActionBand as ActionBand2, ActionButton as ActionButton4, PanelBleed as PanelBleed2 } from "@behindthemusictree/ui";
1716
1751
  import { TrackListOriginType as TrackListOriginType2, useTrackList as useTrackList3 } from "@behindthemusictree/app-kit/genre-tree";
1717
1752
  import { usePlayer as usePlayer6 } from "@behindthemusictree/app-kit/player";
1718
- import { jsx as jsx26, jsxs as jsxs17 } from "react/jsx-runtime";
1753
+ import { jsx as jsx27, jsxs as jsxs18 } from "react/jsx-runtime";
1719
1754
  function ArtistActionBand({ artist, share }) {
1720
1755
  const { trackList, playNewTrackListFromArtist } = useTrackList3();
1721
1756
  const { isPlaying, setIsPlaying } = usePlayer6();
@@ -1731,11 +1766,11 @@ function ArtistActionBand({ artist, share }) {
1731
1766
  console.error("Failed to load artist tracks:", error);
1732
1767
  });
1733
1768
  };
1734
- return /* @__PURE__ */ jsx26(PanelBleed2, { children: /* @__PURE__ */ jsxs17(ActionBand2, { children: [
1735
- /* @__PURE__ */ jsx26(
1769
+ return /* @__PURE__ */ jsx27(PanelBleed2, { children: /* @__PURE__ */ jsxs18(ActionBand2, { children: [
1770
+ /* @__PURE__ */ jsx27(
1736
1771
  ActionButton4,
1737
1772
  {
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" }),
1773
+ 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
1774
  label: playing ? "Pause" : "Play",
1740
1775
  caption: String(artist.playableTracksCount),
1741
1776
  disabled: artist.playableTracksCount === 0,
@@ -1763,7 +1798,7 @@ import {
1763
1798
  SiYoutube
1764
1799
  } from "react-icons/si";
1765
1800
  import { Chip } from "@behindthemusictree/ui";
1766
- import { jsx as jsx27 } from "react/jsx-runtime";
1801
+ import { jsx as jsx28 } from "react/jsx-runtime";
1767
1802
  var LINK_KINDS = {
1768
1803
  official: { label: "Official website", Icon: Globe },
1769
1804
  wikipedia: { label: "Wikipedia", Icon: SiWikipedia2 },
@@ -1800,7 +1835,7 @@ function artistLinks(artist) {
1800
1835
  return links;
1801
1836
  }
1802
1837
  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)) });
1838
+ 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
1839
  }
1805
1840
 
1806
1841
  // src/genre-tree/artist/formatYears.ts
@@ -1811,45 +1846,59 @@ function formatYears(beginYear, endYear, ended) {
1811
1846
  }
1812
1847
 
1813
1848
  // src/genre-tree/artist/ArtistHeader.tsx
1814
- import { jsx as jsx28 } from "react/jsx-runtime";
1849
+ import { Fragment as Fragment12, jsx as jsx29, jsxs as jsxs19 } from "react/jsx-runtime";
1815
1850
  var countryNames = new Intl.DisplayNames(["en"], { type: "region" });
1816
- function ArtistHeader({ artist }) {
1851
+ function originLabel(artist, origin) {
1852
+ if (origin.source === "area") return "From";
1853
+ if (artist.type === "person") return "Born in";
1854
+ if (artist.type === "group") return "Formed in";
1855
+ return "From";
1856
+ }
1857
+ function OriginValue({
1858
+ origin,
1859
+ country,
1860
+ renderPlaceLink
1861
+ }) {
1862
+ const place = renderPlaceLink && origin.regionId !== null ? renderPlaceLink({ ...origin, regionId: origin.regionId }) : origin.name;
1863
+ return /* @__PURE__ */ jsxs19(Fragment12, { children: [
1864
+ place,
1865
+ country && origin.kind !== "country" && `, ${country}`
1866
+ ] });
1867
+ }
1868
+ function ArtistHeader({
1869
+ artist,
1870
+ renderPlaceLink
1871
+ }) {
1817
1872
  const fields = [];
1818
1873
  if (artist.type) fields.push({ label: "Type", value: artist.type.charAt(0).toUpperCase() + artist.type.slice(1) });
1819
- const country = artist.country && countryNames.of(artist.country);
1820
- if (country) fields.push({ label: "Country", value: country });
1874
+ const country = artist.country ? countryNames.of(artist.country) : void 0;
1875
+ if (artist.origin) {
1876
+ fields.push({
1877
+ label: originLabel(artist, artist.origin),
1878
+ value: /* @__PURE__ */ jsx29(OriginValue, { origin: artist.origin, country, renderPlaceLink })
1879
+ });
1880
+ }
1881
+ if (country && !artist.origin) fields.push({ label: "Country", value: country });
1821
1882
  const years = formatYears(artist.beginYear, artist.endYear, artist.ended);
1822
1883
  if (years) fields.push({ label: "Active", value: years });
1823
1884
  const links = artistLinks(artist);
1824
- if (links.length > 0) fields.push({ label: "Links", value: /* @__PURE__ */ jsx28(ArtistLinks, { links }) });
1885
+ if (links.length > 0) fields.push({ label: "Links", value: /* @__PURE__ */ jsx29(ArtistLinks, { links }) });
1825
1886
  if (fields.length === 0) return null;
1826
- return /* @__PURE__ */ jsx28(FieldList, { items: fields });
1887
+ return /* @__PURE__ */ jsx29(FieldList, { items: fields });
1827
1888
  }
1828
1889
 
1829
1890
  // 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
- }
1891
+ import { jsx as jsx30 } from "react/jsx-runtime";
1834
1892
  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
- ] });
1893
+ return /* @__PURE__ */ jsx30(CommonsPhoto, { image, alt: name });
1845
1894
  }
1846
1895
 
1847
1896
  // src/genre-tree/artist/ArtistMembers.tsx
1848
1897
  import { PanelGroup, PanelSection as PanelSection3 } from "@behindthemusictree/ui";
1849
- import { jsx as jsx30, jsxs as jsxs19 } from "react/jsx-runtime";
1898
+ import { jsx as jsx31, jsxs as jsxs20 } from "react/jsx-runtime";
1850
1899
  function MembershipList({ title, memberships, renderArtistLink }) {
1851
1900
  if (memberships.length === 0) return null;
1852
- return /* @__PURE__ */ jsx30(PanelGroup, { title, children: /* @__PURE__ */ jsx30("ul", { children: memberships.map((membership, index) => {
1901
+ return /* @__PURE__ */ jsx31(PanelGroup, { title, children: /* @__PURE__ */ jsx31("ul", { children: memberships.map((membership, index) => {
1853
1902
  const details = [
1854
1903
  formatYears(membership.beginYear, membership.endYear, membership.ended),
1855
1904
  ...membership.roles
@@ -1857,9 +1906,9 @@ function MembershipList({ title, memberships, renderArtistLink }) {
1857
1906
  const detailsText = details.filter(Boolean).join(" \xB7 ");
1858
1907
  return (
1859
1908
  // A member can have several periods in the same group.
1860
- /* @__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) }),
1862
- detailsText && /* @__PURE__ */ jsxs19("span", { className: "text-[var(--color-text-muted)]", children: [
1909
+ /* @__PURE__ */ jsxs20("li", { className: "truncate py-0.5", title: detailsText || void 0, children: [
1910
+ /* @__PURE__ */ jsx31("span", { className: "font-medium text-[var(--color-text)]", children: renderArtistLink(membership.artist) }),
1911
+ detailsText && /* @__PURE__ */ jsxs20("span", { className: "text-[var(--color-text-muted)]", children: [
1863
1912
  " \u2014 ",
1864
1913
  detailsText
1865
1914
  ] })
@@ -1870,9 +1919,9 @@ function MembershipList({ title, memberships, renderArtistLink }) {
1870
1919
  function ArtistMembers({ title, memberships, renderArtistLink }) {
1871
1920
  if (memberships.length === 0) return null;
1872
1921
  const isCurrent = (membership) => !membership.ended && membership.endYear === null;
1873
- return /* @__PURE__ */ jsxs19(PanelSection3, { title, children: [
1874
- /* @__PURE__ */ jsx30(MembershipList, { title: "Current", memberships: memberships.filter(isCurrent), renderArtistLink }),
1875
- /* @__PURE__ */ jsx30(
1922
+ return /* @__PURE__ */ jsxs20(PanelSection3, { title, children: [
1923
+ /* @__PURE__ */ jsx31(MembershipList, { title: "Current", memberships: memberships.filter(isCurrent), renderArtistLink }),
1924
+ /* @__PURE__ */ jsx31(
1876
1925
  MembershipList,
1877
1926
  {
1878
1927
  title: "Past",
@@ -1885,7 +1934,7 @@ function ArtistMembers({ title, memberships, renderArtistLink }) {
1885
1934
 
1886
1935
  // src/genre-tree/artist/ArtistDiscography.tsx
1887
1936
  import { PanelGroup as PanelGroup2, PanelSection as PanelSection4 } from "@behindthemusictree/ui";
1888
- import { jsx as jsx31, jsxs as jsxs20 } from "react/jsx-runtime";
1937
+ import { jsx as jsx32, jsxs as jsxs21 } from "react/jsx-runtime";
1889
1938
  var GROUPS = ["Albums", "EPs", "Singles", "Live", "Compilations", "Other"];
1890
1939
  var PRIMARY_GROUPS = { album: "Albums", ep: "EPs", single: "Singles" };
1891
1940
  function discographyGroup({ primaryType, secondaryTypes }) {
@@ -1905,12 +1954,12 @@ function byYear(a, b) {
1905
1954
  function ArtistDiscography({ albums }) {
1906
1955
  if (albums.length === 0) return null;
1907
1956
  const sorted = [...albums].sort(byYear);
1908
- return /* @__PURE__ */ jsx31(PanelSection4, { title: "Discography", children: GROUPS.map((group) => {
1957
+ return /* @__PURE__ */ jsx32(PanelSection4, { title: "Discography", children: GROUPS.map((group) => {
1909
1958
  const groupAlbums = sorted.filter((album) => discographyGroup(album) === group);
1910
1959
  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 })
1960
+ return /* @__PURE__ */ jsx32(PanelGroup2, { title: group, children: /* @__PURE__ */ jsx32("ul", { children: groupAlbums.map((album) => /* @__PURE__ */ jsxs21("li", { className: "grid grid-cols-[2.5rem_minmax(0,1fr)] gap-2 py-0.5", children: [
1961
+ /* @__PURE__ */ jsx32("span", { className: "tabular-nums text-[var(--color-text-muted)]", children: album.year ?? "\u2014" }),
1962
+ /* @__PURE__ */ jsx32("span", { className: "truncate text-[var(--color-text)]", title: album.name, children: album.name })
1914
1963
  ] }, album.uuid)) }) }, group);
1915
1964
  }) });
1916
1965
  }
@@ -1936,7 +1985,7 @@ import { useTrackList as useTrackList4 } from "@behindthemusictree/app-kit/genre
1936
1985
  import { FaPlay as FaPlay2, FaPause as FaPause2 } from "react-icons/fa";
1937
1986
  import { usePlayer as usePlayer7 } from "@behindthemusictree/app-kit/player";
1938
1987
  import { PlayStates as PlayStates2 } from "@behindthemusictree/app-kit/player";
1939
- import { jsx as jsx32, jsxs as jsxs21 } from "react/jsx-runtime";
1988
+ import { jsx as jsx33, jsxs as jsxs22 } from "react/jsx-runtime";
1940
1989
  function TrackPositionPlayPause({
1941
1990
  position,
1942
1991
  uuid,
@@ -1950,14 +1999,14 @@ function TrackPositionPlayPause({
1950
1999
  const isPlaying = isThisTrack && isConfirmedPlayable && playState === PlayStates2.PLAYING;
1951
2000
  const isPaused = isThisTrack && isConfirmedPlayable && playState === PlayStates2.PAUSED;
1952
2001
  const isCurrentTrackActive = isPlaying || isPaused;
1953
- return /* @__PURE__ */ jsxs21(
2002
+ return /* @__PURE__ */ jsxs22(
1954
2003
  "div",
1955
2004
  {
1956
2005
  className: "track-position-play-pause relative flex items-center justify-center gap-2 text-lg w-16 cursor-pointer",
1957
2006
  style: { minWidth: "4rem", maxWidth: "4rem" },
1958
2007
  onClick: handlePlayPauseClick,
1959
2008
  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(
2009
+ /* @__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__ */ jsxs22(
1961
2010
  "span",
1962
2011
  {
1963
2012
  className: "track-position-play-pause__bars",
@@ -1965,18 +2014,18 @@ function TrackPositionPlayPause({
1965
2014
  role: "img",
1966
2015
  "aria-label": isPlaying ? "Playing" : "Paused",
1967
2016
  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" })
2017
+ /* @__PURE__ */ jsx33("span", { className: "track-position-play-pause__bar" }),
2018
+ /* @__PURE__ */ jsx33("span", { className: "track-position-play-pause__bar" }),
2019
+ /* @__PURE__ */ jsx33("span", { className: "track-position-play-pause__bar" })
1971
2020
  ]
1972
2021
  }
1973
2022
  ) : position }),
1974
- /* @__PURE__ */ jsx32(
2023
+ /* @__PURE__ */ jsx33(
1975
2024
  "div",
1976
2025
  {
1977
2026
  className: `hidden group-hover:flex items-center justify-center ${isCurrentTrackActive ? "" : "absolute inset-0"}`,
1978
2027
  "aria-hidden": "true",
1979
- children: isAwaitingPlayback ? /* @__PURE__ */ jsx32("span", { className: "track-position-play-pause__spinner" }) : isPlaying ? /* @__PURE__ */ jsx32(FaPause2, {}) : /* @__PURE__ */ jsx32(FaPlay2, {})
2028
+ children: isAwaitingPlayback ? /* @__PURE__ */ jsx33("span", { className: "track-position-play-pause__spinner" }) : isPlaying ? /* @__PURE__ */ jsx33(FaPause2, {}) : /* @__PURE__ */ jsx33(FaPlay2, {})
1980
2029
  }
1981
2030
  )
1982
2031
  ]
@@ -1987,7 +2036,7 @@ function TrackPositionPlayPause({
1987
2036
  // src/genre-tree/track-list-sidebar/TrackItem.tsx
1988
2037
  import { isSong as isSong2, songUnplayableReason as songUnplayableReason2 } from "@behindthemusictree/app-kit/genre-tree";
1989
2038
  import { ArtistNames as ArtistNames2 } from "@behindthemusictree/app-kit/genre-tree";
1990
- import { jsx as jsx33, jsxs as jsxs22 } from "react/jsx-runtime";
2039
+ import { jsx as jsx34, jsxs as jsxs23 } from "react/jsx-runtime";
1991
2040
  function TrackItem({
1992
2041
  track,
1993
2042
  position,
@@ -2011,12 +2060,12 @@ function TrackItem({
2011
2060
  loadTrackForPlayer(track.uuid);
2012
2061
  }
2013
2062
  };
2014
- return /* @__PURE__ */ jsxs22(
2063
+ return /* @__PURE__ */ jsxs23(
2015
2064
  "div",
2016
2065
  {
2017
2066
  className: `track-item flex h-14 text-[var(--color-text)] ${isUnplayable ? "opacity-50" : "hover:bg-[var(--color-surface-muted)] group"}`,
2018
2067
  children: [
2019
- isUnplayable ? /* @__PURE__ */ jsx33(
2068
+ isUnplayable ? /* @__PURE__ */ jsx34(
2020
2069
  "div",
2021
2070
  {
2022
2071
  className: "flex items-center justify-center text-lg w-16",
@@ -2024,7 +2073,7 @@ function TrackItem({
2024
2073
  "aria-disabled": "true",
2025
2074
  children: position
2026
2075
  }
2027
- ) : /* @__PURE__ */ jsx33(
2076
+ ) : /* @__PURE__ */ jsx34(
2028
2077
  TrackPositionPlayPause,
2029
2078
  {
2030
2079
  position,
@@ -2033,12 +2082,12 @@ function TrackItem({
2033
2082
  isSelected: selectedTrack?.uuid === track.uuid
2034
2083
  }
2035
2084
  ),
2036
- /* @__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 }) }) : ""
2085
+ /* @__PURE__ */ jsxs23("div", { className: "title-artist-container flex flex-col items-start justify-center min-w-0 flex-1", children: [
2086
+ /* @__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 }) }),
2087
+ 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
2088
  ] }),
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(
2089
+ /* @__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 }) : "" }),
2090
+ renderDuration && /* @__PURE__ */ jsx34(
2042
2091
  "div",
2043
2092
  {
2044
2093
  className: "duration flex text-base w-16 items-center justify-center",
@@ -2046,14 +2095,14 @@ function TrackItem({
2046
2095
  children: renderDuration(track)
2047
2096
  }
2048
2097
  ),
2049
- renderActions && /* @__PURE__ */ jsx33("div", { className: "edit flex text-base w-6 items-center justify-center mr-2 cursor-pointer", children: renderActions(track) })
2098
+ renderActions && /* @__PURE__ */ jsx34("div", { className: "edit flex text-base w-6 items-center justify-center mr-2 cursor-pointer", children: renderActions(track) })
2050
2099
  ]
2051
2100
  }
2052
2101
  );
2053
2102
  }
2054
2103
 
2055
2104
  // src/genre-tree/artist/ArtistSongs.tsx
2056
- import { Fragment as Fragment12, jsx as jsx34, jsxs as jsxs23 } from "react/jsx-runtime";
2105
+ import { Fragment as Fragment13, jsx as jsx35, jsxs as jsxs24 } from "react/jsx-runtime";
2057
2106
  var SongPageSchema2 = PaginatedResponseSchema2(SongDetailedSchema2);
2058
2107
  var SORTS = [
2059
2108
  { ordering: "title", label: "Title" },
@@ -2086,8 +2135,8 @@ function ArtistSongs({ artist, getBackendBaseUrl, renderArtistLink, renderSongLi
2086
2135
  event.stopPropagation();
2087
2136
  void play(song.uuid);
2088
2137
  };
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(
2138
+ 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__ */ jsxs24(Fragment13, { children: [
2139
+ /* @__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
2140
  "button",
2092
2141
  {
2093
2142
  type: "button",
@@ -2101,7 +2150,7 @@ function ArtistSongs({ artist, getBackendBaseUrl, renderArtistLink, renderSongLi
2101
2150
  },
2102
2151
  sort.ordering
2103
2152
  )) }) }),
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(
2153
+ /* @__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
2154
  TrackItem,
2106
2155
  {
2107
2156
  track: song,
@@ -2110,12 +2159,12 @@ function ArtistSongs({ artist, getBackendBaseUrl, renderArtistLink, renderSongLi
2110
2159
  renderSongLink
2111
2160
  }
2112
2161
  ) }, song.uuid)) }),
2113
- /* @__PURE__ */ jsx34(Pagination, { currentPage: page, totalPages: data.totalPages, onPageChange: setPage })
2162
+ /* @__PURE__ */ jsx35(Pagination, { currentPage: page, totalPages: data.totalPages, onPageChange: setPage })
2114
2163
  ] }) });
2115
2164
  }
2116
2165
 
2117
2166
  // src/genre-tree/artist/useArtistPanel.tsx
2118
- import { Fragment as Fragment13, jsx as jsx35, jsxs as jsxs24 } from "react/jsx-runtime";
2167
+ import { Fragment as Fragment14, jsx as jsx36, jsxs as jsxs25 } from "react/jsx-runtime";
2119
2168
  function useArtistPanel({
2120
2169
  uuid,
2121
2170
  scope,
@@ -2123,6 +2172,7 @@ function useArtistPanel({
2123
2172
  getShareUrl,
2124
2173
  renderArtistLink,
2125
2174
  renderSongLink,
2175
+ renderPlaceLink,
2126
2176
  expanded,
2127
2177
  onExpandedChange,
2128
2178
  onClose
@@ -2136,26 +2186,26 @@ function useArtistPanel({
2136
2186
  return {
2137
2187
  ...panel,
2138
2188
  title: "Artist",
2139
- 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" })
2189
+ content: /* @__PURE__ */ jsxs25("div", { "aria-busy": "true", className: "flex flex-col gap-2.5", children: [
2190
+ /* @__PURE__ */ jsx36(Skeleton5, { className: "h-3 w-16" }),
2191
+ /* @__PURE__ */ jsx36(Skeleton5, { className: "h-4 w-32" }),
2192
+ /* @__PURE__ */ jsx36(Skeleton5, { className: "h-3 w-16" }),
2193
+ /* @__PURE__ */ jsx36(Skeleton5, { className: "h-4 w-24" }),
2194
+ /* @__PURE__ */ jsx36(Skeleton5, { className: "h-16 mt-2.5" }),
2195
+ /* @__PURE__ */ jsx36(Skeleton5, { className: "h-16" })
2146
2196
  ] })
2147
2197
  };
2148
2198
  case "not-found":
2149
2199
  return {
2150
2200
  ...panel,
2151
2201
  title: "Artist not found",
2152
- content: /* @__PURE__ */ jsx35("p", { className: "text-[var(--color-text-muted)]", children: "This artist does not exist." })
2202
+ content: /* @__PURE__ */ jsx36("p", { className: "text-[var(--color-text-muted)]", children: "This artist does not exist." })
2153
2203
  };
2154
2204
  case "error":
2155
2205
  return {
2156
2206
  ...panel,
2157
2207
  title: "Artist",
2158
- content: /* @__PURE__ */ jsx35("p", { className: "text-red-600", children: "Could not load this artist." })
2208
+ content: /* @__PURE__ */ jsx36("p", { className: "text-red-600", children: "Could not load this artist." })
2159
2209
  };
2160
2210
  case "success": {
2161
2211
  const { artist } = result;
@@ -2164,19 +2214,19 @@ function useArtistPanel({
2164
2214
  return {
2165
2215
  ...panel,
2166
2216
  title: artist.name,
2167
- content: /* @__PURE__ */ jsxs24(Fragment13, { children: [
2168
- /* @__PURE__ */ jsx35(
2217
+ content: /* @__PURE__ */ jsxs25(Fragment14, { children: [
2218
+ /* @__PURE__ */ jsx36(
2169
2219
  ArtistActionBand,
2170
2220
  {
2171
2221
  artist,
2172
- share: shareTarget && shareUrl ? /* @__PURE__ */ jsx35(ShareActionButton, { target: shareTarget, url: shareUrl }) : void 0
2222
+ share: shareTarget && shareUrl ? /* @__PURE__ */ jsx36(ShareActionButton, { target: shareTarget, url: shareUrl }) : void 0
2173
2223
  }
2174
2224
  ),
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(
2225
+ artist.image && /* @__PURE__ */ jsx36(ArtistPhoto, { image: artist.image, name: artist.name }),
2226
+ /* @__PURE__ */ jsx36(ArtistHeader, { artist, renderPlaceLink }),
2227
+ /* @__PURE__ */ jsx36(ArtistMembers, { title: "Members", memberships: artist.members, renderArtistLink }),
2228
+ /* @__PURE__ */ jsx36(ArtistMembers, { title: "Member of", memberships: artist.memberOf, renderArtistLink }),
2229
+ artist.tracksCount > 0 && /* @__PURE__ */ jsx36(
2180
2230
  ArtistSongs,
2181
2231
  {
2182
2232
  artist: { uuid: artist.uuid, name: artist.name },
@@ -2186,7 +2236,7 @@ function useArtistPanel({
2186
2236
  }
2187
2237
  )
2188
2238
  ] }),
2189
- extraContent: artist.albums.length > 0 ? /* @__PURE__ */ jsx35(ArtistDiscography, { albums: artist.albums }) : void 0
2239
+ extraContent: artist.albums.length > 0 ? /* @__PURE__ */ jsx36(ArtistDiscography, { albums: artist.albums }) : void 0
2190
2240
  };
2191
2241
  }
2192
2242
  }
@@ -2210,7 +2260,7 @@ import {
2210
2260
  useSongHistory,
2211
2261
  useTrackList as useTrackList6
2212
2262
  } from "@behindthemusictree/app-kit/genre-tree";
2213
- import { Fragment as Fragment14, jsx as jsx36, jsxs as jsxs25 } from "react/jsx-runtime";
2263
+ import { Fragment as Fragment15, jsx as jsx37, jsxs as jsxs26 } from "react/jsx-runtime";
2214
2264
  var numberFormat = new Intl.NumberFormat("en");
2215
2265
  var dateFormat = new Intl.DateTimeFormat("en", { dateStyle: "medium" });
2216
2266
  var externalLink = "text-[var(--color-primary)] hover:underline focus-visible:underline";
@@ -2218,27 +2268,27 @@ function SongHistory({ uuid, getBackendBaseUrl }) {
2218
2268
  const [open, setOpen] = useState9(false);
2219
2269
  const history = useSongHistory(uuid, getBackendBaseUrl, open);
2220
2270
  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." });
2271
+ if (history.status === "loading") body = /* @__PURE__ */ jsx37(Skeleton6, { className: "h-10", "aria-busy": "true" });
2272
+ else if (history.status === "error") body = /* @__PURE__ */ jsx37("p", { className: "text-red-600", children: "Could not load the history." });
2223
2273
  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 }),
2226
- entry.oldValue !== null || entry.newValue !== null ? /* @__PURE__ */ jsxs25("span", { className: "text-[var(--color-text-muted)]", children: [
2274
+ 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__ */ jsxs26("li", { className: "py-1.5 text-body", children: [
2275
+ /* @__PURE__ */ jsx37("span", { className: "font-medium", children: entry.action }),
2276
+ entry.oldValue !== null || entry.newValue !== null ? /* @__PURE__ */ jsxs26("span", { className: "text-[var(--color-text-muted)]", children: [
2227
2277
  " ",
2228
2278
  "\u2014 ",
2229
2279
  entry.oldValue ?? "none",
2230
2280
  " \u2192 ",
2231
2281
  entry.newValue ?? "none"
2232
2282
  ] }) : null,
2233
- /* @__PURE__ */ jsxs25("span", { className: "block text-caption text-[var(--color-text-muted)]", children: [
2283
+ /* @__PURE__ */ jsxs26("span", { className: "block text-caption text-[var(--color-text-muted)]", children: [
2234
2284
  entry.actorPseudo ?? "Anonymous",
2235
2285
  " \xB7 ",
2236
2286
  dateFormat.format(new Date(entry.createdOn))
2237
2287
  ] })
2238
2288
  ] }, 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 })
2289
+ return /* @__PURE__ */ jsx37(PanelBleed3, { padded: true, className: "border-t border-[var(--color-border)]", children: /* @__PURE__ */ jsxs26("details", { onToggle: (event) => setOpen(event.currentTarget.open), className: "pb-2.5", children: [
2290
+ /* @__PURE__ */ jsx37("summary", { className: `mt-2.5 cursor-pointer ${PANEL_LABEL_CLASS}`, children: "History" }),
2291
+ /* @__PURE__ */ jsx37("div", { className: "mt-1.5", children: body })
2242
2292
  ] }) });
2243
2293
  }
2244
2294
  function SongPanelContent(props) {
@@ -2257,13 +2307,13 @@ function SongPanelContent(props) {
2257
2307
  const genre = song.genre;
2258
2308
  fields.push({
2259
2309
  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 })
2310
+ 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__ */ jsxs26("span", { children: [
2311
+ index > 0 && /* @__PURE__ */ jsx37("span", { "aria-hidden": "true", children: "\u203A " }),
2312
+ /* @__PURE__ */ jsx37("button", { type: "button", onClick: () => props.onSelectGenrePlaylist?.(genrePlaylist), className: "hover:underline", children: genrePlaylist.name })
2263
2313
  ] }, genrePlaylist.uuid)) }) : song.genre.name
2264
2314
  });
2265
2315
  } else {
2266
- fields.push({ label: "Genre", value: /* @__PURE__ */ jsx36("span", { className: "text-[var(--color-text-muted)]", children: "No genre" }) });
2316
+ fields.push({ label: "Genre", value: /* @__PURE__ */ jsx37("span", { className: "text-[var(--color-text-muted)]", children: "No genre" }) });
2267
2317
  }
2268
2318
  if (song.firstReleaseYear) fields.push({ label: "First released", value: song.firstReleaseYear });
2269
2319
  const viewCount = song.youtubeVideo?.viewCount;
@@ -2273,11 +2323,11 @@ function SongPanelContent(props) {
2273
2323
  const links = [];
2274
2324
  if (musicBrainzUrl)
2275
2325
  links.push(
2276
- /* @__PURE__ */ jsx36("a", { href: musicBrainzUrl, target: "_blank", rel: "noopener noreferrer", className: externalLink, children: "MusicBrainz" }, "mb")
2326
+ /* @__PURE__ */ jsx37("a", { href: musicBrainzUrl, target: "_blank", rel: "noopener noreferrer", className: externalLink, children: "MusicBrainz" }, "mb")
2277
2327
  );
2278
2328
  if (song.youtubeVideo)
2279
2329
  links.push(
2280
- /* @__PURE__ */ jsx36(
2330
+ /* @__PURE__ */ jsx37(
2281
2331
  "a",
2282
2332
  {
2283
2333
  href: `https://www.youtube.com/watch?v=${encodeURIComponent(song.youtubeVideo.id)}`,
@@ -2292,31 +2342,31 @@ function SongPanelContent(props) {
2292
2342
  if (links.length > 0)
2293
2343
  fields.push({
2294
2344
  label: "Links",
2295
- value: /* @__PURE__ */ jsx36("span", { className: "flex gap-3", children: links })
2345
+ value: /* @__PURE__ */ jsx37("span", { className: "flex gap-3", children: links })
2296
2346
  });
2297
- return /* @__PURE__ */ jsxs25(Fragment14, { children: [
2298
- /* @__PURE__ */ jsx36(PanelBleed3, { children: /* @__PURE__ */ jsxs25(ActionBand3, { children: [
2299
- /* @__PURE__ */ jsx36(
2347
+ return /* @__PURE__ */ jsxs26(Fragment15, { children: [
2348
+ /* @__PURE__ */ jsx37(PanelBleed3, { children: /* @__PURE__ */ jsxs26(ActionBand3, { children: [
2349
+ /* @__PURE__ */ jsx37(
2300
2350
  ActionButton5,
2301
2351
  {
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" }),
2352
+ 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
2353
  label: isPlaying ? "Pause" : "Play",
2304
2354
  disabled: !!unplayableReason,
2305
2355
  title: unplayableReason ?? void 0,
2306
2356
  onClick: () => isCurrent ? handlePlayPauseAction() : playNewTrackListFromTrackUuid(song, scope)
2307
2357
  }
2308
2358
  ),
2309
- shareUrl && /* @__PURE__ */ jsx36(ShareActionButton, { target: shareTarget, url: shareUrl })
2359
+ shareUrl && /* @__PURE__ */ jsx37(ShareActionButton, { target: shareTarget, url: shareUrl })
2310
2360
  ] }) }),
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 })
2361
+ song.artists && song.artists.length > 0 && /* @__PURE__ */ jsx37("p", { className: "text-[var(--color-text-muted)]", children: /* @__PURE__ */ jsx37(ArtistNames3, { artists: song.artists, renderArtistLink }) }),
2362
+ /* @__PURE__ */ jsx37(FieldList2, { items: fields }),
2363
+ 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)) }) }),
2364
+ /* @__PURE__ */ jsx37(SongHistory, { uuid: song.uuid, getBackendBaseUrl })
2315
2365
  ] });
2316
2366
  }
2317
2367
 
2318
2368
  // src/genre-tree/song/useSongPanel.tsx
2319
- import { jsx as jsx37, jsxs as jsxs26 } from "react/jsx-runtime";
2369
+ import { jsx as jsx38, jsxs as jsxs27 } from "react/jsx-runtime";
2320
2370
  function useSongPanel({
2321
2371
  uuid,
2322
2372
  scope,
@@ -2337,24 +2387,24 @@ function useSongPanel({
2337
2387
  return {
2338
2388
  ...panel,
2339
2389
  title: "Song",
2340
- 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" })
2390
+ content: /* @__PURE__ */ jsxs27("div", { "aria-busy": "true", className: "flex flex-col gap-2.5", children: [
2391
+ /* @__PURE__ */ jsx38(Skeleton7, { className: "h-3 w-16" }),
2392
+ /* @__PURE__ */ jsx38(Skeleton7, { className: "h-4 w-32" }),
2393
+ /* @__PURE__ */ jsx38(Skeleton7, { className: "h-3 w-16" }),
2394
+ /* @__PURE__ */ jsx38(Skeleton7, { className: "h-4 w-24" })
2345
2395
  ] })
2346
2396
  };
2347
2397
  case "not-found":
2348
2398
  return {
2349
2399
  ...panel,
2350
2400
  title: "Song not found",
2351
- content: /* @__PURE__ */ jsx37("p", { className: "text-[var(--color-text-muted)]", children: "This song does not exist." })
2401
+ content: /* @__PURE__ */ jsx38("p", { className: "text-[var(--color-text-muted)]", children: "This song does not exist." })
2352
2402
  };
2353
2403
  case "error":
2354
2404
  return {
2355
2405
  ...panel,
2356
2406
  title: "Song",
2357
- content: /* @__PURE__ */ jsx37("p", { className: "text-red-600", children: "Could not load this song." })
2407
+ content: /* @__PURE__ */ jsx38("p", { className: "text-red-600", children: "Could not load this song." })
2358
2408
  };
2359
2409
  case "success": {
2360
2410
  const { song } = result;
@@ -2362,7 +2412,7 @@ function useSongPanel({
2362
2412
  return {
2363
2413
  ...panel,
2364
2414
  title: song.title,
2365
- content: /* @__PURE__ */ jsx37(
2415
+ content: /* @__PURE__ */ jsx38(
2366
2416
  SongPanelContent,
2367
2417
  {
2368
2418
  song,
@@ -2389,6 +2439,7 @@ function useEntityPanel({
2389
2439
  getShareUrl,
2390
2440
  renderArtistLink,
2391
2441
  renderSongLink,
2442
+ renderPlaceLink,
2392
2443
  song,
2393
2444
  artist,
2394
2445
  ...navigation
@@ -2407,7 +2458,8 @@ function useEntityPanel({
2407
2458
  getBackendBaseUrl: getArtistBackendBaseUrl ?? getBackendBaseUrl,
2408
2459
  getShareUrl,
2409
2460
  renderArtistLink,
2410
- renderSongLink
2461
+ renderSongLink,
2462
+ renderPlaceLink
2411
2463
  });
2412
2464
  return songPanel ?? artistPanel;
2413
2465
  }
@@ -2415,12 +2467,12 @@ function useEntityPanel({
2415
2467
  // src/genre-tree/SizeMetricToggle.tsx
2416
2468
  import { MdMusicNote, MdVisibility } from "react-icons/md";
2417
2469
  import { SegmentedControl } from "@behindthemusictree/ui";
2418
- import { jsx as jsx38 } from "react/jsx-runtime";
2470
+ import { jsx as jsx39 } from "react/jsx-runtime";
2419
2471
  function SizeMetricToggle({
2420
2472
  value,
2421
2473
  onChange
2422
2474
  }) {
2423
- return /* @__PURE__ */ jsx38(
2475
+ return /* @__PURE__ */ jsx39(
2424
2476
  SegmentedControl,
2425
2477
  {
2426
2478
  variant: "floating",
@@ -2428,15 +2480,15 @@ function SizeMetricToggle({
2428
2480
  value,
2429
2481
  onChange,
2430
2482
  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" }
2483
+ { value: "songs", label: /* @__PURE__ */ jsx39(MdMusicNote, { size: 18, "aria-hidden": true }), "aria-label": "Size by songs", title: "Node size by song count" },
2484
+ { value: "views", label: /* @__PURE__ */ jsx39(MdVisibility, { size: 18, "aria-hidden": true }), "aria-label": "Size by views", title: "Node size by song views" }
2433
2485
  ]
2434
2486
  }
2435
2487
  );
2436
2488
  }
2437
2489
 
2438
2490
  // src/genre-tree/GenreTreeView.tsx
2439
- import { Fragment as Fragment15, jsx as jsx39, jsxs as jsxs27 } from "react/jsx-runtime";
2491
+ import { Fragment as Fragment16, jsx as jsx40, jsxs as jsxs28 } from "react/jsx-runtime";
2440
2492
  var NO_POP_ROOT_TITLE = "This genre tree has no 'Mainstream Pop' root yet";
2441
2493
  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
2494
  var noop = () => {
@@ -2484,6 +2536,7 @@ function GenreTreeView({
2484
2536
  songSearch = true,
2485
2537
  onSongSelect,
2486
2538
  renderSongLink,
2539
+ renderPlaceLink,
2487
2540
  renderSummary,
2488
2541
  renderEssentialTracks,
2489
2542
  renderSongLeading,
@@ -2588,14 +2641,14 @@ function GenreTreeView({
2588
2641
  const openSongRef = useRef3(openSong);
2589
2642
  openSongRef.current = openSong;
2590
2643
  const resolvedRenderSongLink = useCallback6(
2591
- (song) => renderSongLink ? renderSongLink(song) : /* @__PURE__ */ jsx39("button", { type: "button", onClick: () => openSongRef.current(song.uuid), className: "hover:underline", children: song.title }),
2644
+ (song) => renderSongLink ? renderSongLink(song) : /* @__PURE__ */ jsx40("button", { type: "button", onClick: () => openSongRef.current(song.uuid), className: "hover:underline", children: song.title }),
2592
2645
  [renderSongLink]
2593
2646
  );
2594
2647
  const [expandedSongUuid, setExpandedSongUuid] = useState10(null);
2595
2648
  const [expandedArtistUuid, setExpandedArtistUuid] = useState10(null);
2596
2649
  const artistBaseUrl = artistSearchBaseUrl ?? getBackendBaseUrl;
2597
2650
  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 }));
2651
+ const resolvedRenderArtistLink = renderArtistLink ?? ((artist) => artist.isSpecialPurpose ? artist.name : /* @__PURE__ */ jsx40("button", { type: "button", onClick: () => openArtist(artist.uuid), className: "hover:underline", children: artist.name }));
2599
2652
  const getGenrePath = (genre) => {
2600
2653
  const path = [];
2601
2654
  let current = visibleGenrePlaylists.find((playlist) => playlist.criteria?.uuid === genre.uuid);
@@ -2613,6 +2666,7 @@ function GenreTreeView({
2613
2666
  getShareUrl: getShareUrlRef.current,
2614
2667
  renderArtistLink: resolvedRenderArtistLink,
2615
2668
  renderSongLink: resolvedRenderSongLink,
2669
+ renderPlaceLink,
2616
2670
  song: {
2617
2671
  uuid: songUuid,
2618
2672
  expanded: controlledSongExpanded ?? (songUuid !== null && expandedSongUuid === songUuid),
@@ -2672,9 +2726,10 @@ function GenreTreeView({
2672
2726
  resolveStyle
2673
2727
  );
2674
2728
  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 });
2729
+ const validSelectedGenreUuid = toGenreUuid(selectedGenreUuid);
2730
+ const [selectionHistory, setSelectionHistory] = useState10({ current: validSelectedGenreUuid, previous: null });
2731
+ if (selectionHistory.current !== validSelectedGenreUuid) {
2732
+ setSelectionHistory({ current: validSelectedGenreUuid, previous: selectionHistory.current ?? selectionHistory.previous });
2678
2733
  }
2679
2734
  const handlePrevious = selectionHistory.previous ? () => selectGenre(selectionHistory.previous) : void 0;
2680
2735
  const { showPopup, hidePopup } = usePopup4();
@@ -2706,7 +2761,7 @@ function GenreTreeView({
2706
2761
  const canToggleSide = !readOnly && criteria !== void 0 && criteria !== null && parentPlaylist !== void 0 && parentPlaylist.parent === null;
2707
2762
  const side = criteria?.side ?? "core";
2708
2763
  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;
2764
+ const shareTarget = validSelectedGenreUuid ? { kind: "genre", genreUuid: validSelectedGenreUuid, name: criteria?.name ?? "Genre", scope, artistUuid } : void 0;
2710
2765
  const shareUrl = shareTarget ? getShareUrlRef.current?.(shareTarget) ?? void 0 : void 0;
2711
2766
  const genreContent = useGenreDetailContent({
2712
2767
  genreUuid: selectedGenreUuid,
@@ -2728,7 +2783,7 @@ function GenreTreeView({
2728
2783
  onAddChild: readOnly || !criteria ? void 0 : () => handleGenreCreationAction(criteria),
2729
2784
  onRename: readOnly || !criteria ? void 0 : () => handleGenreRenameAction(criteria),
2730
2785
  onChangeParent: readOnly || !genrePlaylist || !criteria ? void 0 : () => showPopup(
2731
- /* @__PURE__ */ jsx39(
2786
+ /* @__PURE__ */ jsx40(
2732
2787
  ChangeParentPopup,
2733
2788
  {
2734
2789
  genrePlaylist: { ...genrePlaylist, criteria },
@@ -2772,7 +2827,7 @@ function GenreTreeView({
2772
2827
  onClose: otherPanel.onClose,
2773
2828
  searchName: otherPanel.title
2774
2829
  };
2775
- } else if (selectedGenreUuid) {
2830
+ } else if (selectedGenreUuid !== null) {
2776
2831
  activePanel = {
2777
2832
  title: genreContent.title,
2778
2833
  content: genreContent.content,
@@ -2789,19 +2844,19 @@ function GenreTreeView({
2789
2844
  edge: activePanel.expanded ? { label: "Shrink panel", onClick: () => activePanel.onExpandedChange?.(false) } : isCollapsed ? null : { label: "Hide panel", onClick: () => changeCollapsed(true) },
2790
2845
  center: activePanel.expanded ? null : isCollapsed ? { label: "Show panel", onClick: () => changeCollapsed(false) } : activePanel.onExpandedChange ? { label: "Extend panel", onClick: () => activePanel.onExpandedChange?.(true) } : null
2791
2846
  } : void 0;
2792
- const expandButton = placement === "default" && activePanel?.onExpandedChange ? /* @__PURE__ */ jsx39(
2847
+ const expandButton = placement === "default" && activePanel?.onExpandedChange ? /* @__PURE__ */ jsx40(
2793
2848
  IconButton2,
2794
2849
  {
2795
2850
  "aria-label": activePanel.expanded ? "Shrink panel" : "Extend panel",
2796
2851
  style: activePanel.headerAccent && { color: "inherit" },
2797
2852
  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" })
2853
+ 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
2854
  }
2800
2855
  ) : null;
2801
2856
  const renderArtistFilter = () => {
2802
2857
  if (isLoading || !filterArtist) return null;
2803
- return /* @__PURE__ */ jsxs27("span", { className: "inline-flex items-center overflow-hidden rounded-full bg-[var(--color-neutral-100)] text-body", children: [
2804
- /* @__PURE__ */ jsx39(
2858
+ return /* @__PURE__ */ jsxs28("span", { className: "inline-flex items-center overflow-hidden rounded-full bg-[var(--color-neutral-100)] text-body", children: [
2859
+ /* @__PURE__ */ jsx40(
2805
2860
  "button",
2806
2861
  {
2807
2862
  type: "button",
@@ -2813,7 +2868,7 @@ function GenreTreeView({
2813
2868
  children: filterArtist.name
2814
2869
  }
2815
2870
  ),
2816
- /* @__PURE__ */ jsx39(
2871
+ /* @__PURE__ */ jsx40(
2817
2872
  "button",
2818
2873
  {
2819
2874
  type: "button",
@@ -2825,8 +2880,8 @@ function GenreTreeView({
2825
2880
  )
2826
2881
  ] });
2827
2882
  };
2828
- const actions = /* @__PURE__ */ jsxs27(Fragment15, { children: [
2829
- !isLoading && !isError && /* @__PURE__ */ jsx39(
2883
+ const actions = /* @__PURE__ */ jsxs28(Fragment16, { children: [
2884
+ !isLoading && !isError && /* @__PURE__ */ jsx40(
2830
2885
  GenreTreeSearch,
2831
2886
  {
2832
2887
  artistBaseUrl,
@@ -2859,9 +2914,9 @@ function GenreTreeView({
2859
2914
  extraGroups: extraSearchGroups
2860
2915
  }
2861
2916
  ),
2862
- !isLoading && !isError && filterArtist && /* @__PURE__ */ jsx39("div", { className: "hidden w-full justify-center max-md:flex", children: renderArtistFilter() }),
2863
- /* @__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(
2917
+ !isLoading && !isError && filterArtist && /* @__PURE__ */ jsx40("div", { className: "hidden w-full justify-center max-md:flex", children: renderArtistFilter() }),
2918
+ /* @__PURE__ */ jsxs28("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: [
2919
+ !isLoading && !isError && !isControlled && /* @__PURE__ */ jsx40(
2865
2920
  SegmentedControl2,
2866
2921
  {
2867
2922
  "aria-label": "Tree view mode",
@@ -2875,13 +2930,13 @@ function GenreTreeView({
2875
2930
  ]
2876
2931
  }
2877
2932
  ),
2878
- !isLoading && !isError && showDisplayOptions && /* @__PURE__ */ jsx39(
2933
+ !isLoading && !isError && showDisplayOptions && /* @__PURE__ */ jsx40(
2879
2934
  "div",
2880
2935
  {
2881
2936
  className: "flex items-center gap-1",
2882
2937
  role: "group",
2883
2938
  "aria-label": "Tree display options",
2884
- children: !isRotationControlled && viewMode !== "stacked" && viewMode !== "outline" && /* @__PURE__ */ jsx39(
2939
+ children: !isRotationControlled && viewMode !== "stacked" && viewMode !== "outline" && /* @__PURE__ */ jsx40(
2885
2940
  Button,
2886
2941
  {
2887
2942
  variant: allowWheelRotation ? "default" : "outline",
@@ -2892,7 +2947,7 @@ function GenreTreeView({
2892
2947
  )
2893
2948
  }
2894
2949
  ),
2895
- !isLoading && !isError && !readOnly && showAddRoot && /* @__PURE__ */ jsx39(
2950
+ !isLoading && !isError && !readOnly && showAddRoot && /* @__PURE__ */ jsx40(
2896
2951
  IconTextButton,
2897
2952
  {
2898
2953
  icon: Plus2,
@@ -2902,38 +2957,38 @@ function GenreTreeView({
2902
2957
  )
2903
2958
  ] })
2904
2959
  ] });
2905
- return /* @__PURE__ */ jsxs27(
2960
+ return /* @__PURE__ */ jsxs28(
2906
2961
  "div",
2907
2962
  {
2908
2963
  "data-left-panel": isPanelShown && !isCollapsed ? void 0 : "hidden",
2909
2964
  className: ["genre-tree-view relative flex flex-col h-full", placement === "regions" && REGIONS_CLASS, className2].filter(Boolean).join(" "),
2910
2965
  children: [
2911
- /* @__PURE__ */ jsx39(
2966
+ /* @__PURE__ */ jsx40(
2912
2967
  "div",
2913
2968
  {
2914
2969
  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
2970
  children: actions
2916
2971
  }
2917
2972
  ),
2918
- !isLoading && !isError && filterArtist && /* @__PURE__ */ jsx39(
2973
+ !isLoading && !isError && filterArtist && /* @__PURE__ */ jsx40(
2919
2974
  "div",
2920
2975
  {
2921
2976
  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
2977
  children: renderArtistFilter()
2923
2978
  }
2924
2979
  ),
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: [
2980
+ /* @__PURE__ */ jsx40("div", { className: "content-container flex-1 min-h-0 flex flex-row gap-4", children: /* @__PURE__ */ jsxs28("div", { className: "tree-view-container relative flex-1 min-w-0 flex flex-col h-full", children: [
2981
+ !isLoading && !isError && viewMode === "outline" && floatingControlsLeading && /* @__PURE__ */ jsx40("div", { className: "absolute bottom-3 right-3 z-10", children: floatingControlsLeading }),
2982
+ isLoading ? /* @__PURE__ */ jsx40(GenreTreeViewSkeleton, { viewMode }) : isError ? /* @__PURE__ */ jsxs28("p", { role: "alert", className: "m-auto text-center", children: [
2928
2983
  "Couldn't load the genre tree.",
2929
2984
  " ",
2930
- /* @__PURE__ */ jsx39("button", { type: "button", className: "underline", onClick: refetch, children: "Retry" })
2985
+ /* @__PURE__ */ jsx40("button", { type: "button", className: "underline", onClick: refetch, children: "Retry" })
2931
2986
  ] }) : viewMode === "wheel" ? (
2932
2987
  // The info panel's top edge aligns with the tree's; the search bar floats `--layout-panel-inset` below it,
2933
2988
  // and the header inset (the bar's height plus that inset) extends the header's fill behind the bar,
2934
2989
  // Google Maps style; the 8px bottom padding mirrors the inset above the bar, and the hidden title's height is zeroed.
2935
2990
  // 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(
2991
+ /* @__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
2992
  GenrePlaylistTreeWheel,
2938
2993
  {
2939
2994
  scope,
@@ -2951,9 +3006,9 @@ function GenreTreeView({
2951
3006
  allowWheelRotation,
2952
3007
  sizeMetric: isArtistFiltered || !canSizeByViews ? "songs" : deferredSizeMetric,
2953
3008
  busy,
2954
- floatingControlsLeading: floatingControlsLeading || canSizeByViews ? /* @__PURE__ */ jsxs27("div", { className: "flex items-center gap-2", children: [
3009
+ floatingControlsLeading: floatingControlsLeading || canSizeByViews ? /* @__PURE__ */ jsxs28("div", { className: "flex items-center gap-2", children: [
2955
3010
  floatingControlsLeading,
2956
- canSizeByViews && /* @__PURE__ */ jsx39(SizeMetricToggle, { value: sizeMetric, onChange: changeSizeMetric })
3011
+ canSizeByViews && /* @__PURE__ */ jsx40(SizeMetricToggle, { value: sizeMetric, onChange: changeSizeMetric })
2957
3012
  ] }) : void 0,
2958
3013
  defaultViewport,
2959
3014
  viewportRestore,
@@ -2962,7 +3017,7 @@ function GenreTreeView({
2962
3017
  ) }) })
2963
3018
  ) : viewMode === "pop-core" ? (
2964
3019
  // 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(
3020
+ /* @__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
3021
  GenrePlaylistTreeWheelRadialPopCore,
2967
3022
  {
2968
3023
  scope,
@@ -2980,9 +3035,9 @@ function GenreTreeView({
2980
3035
  allowWheelRotation,
2981
3036
  sizeMetric: isArtistFiltered || !canSizeByViews ? "songs" : deferredSizeMetric,
2982
3037
  busy,
2983
- floatingControlsLeading: floatingControlsLeading || canSizeByViews ? /* @__PURE__ */ jsxs27("div", { className: "flex items-center gap-2", children: [
3038
+ floatingControlsLeading: floatingControlsLeading || canSizeByViews ? /* @__PURE__ */ jsxs28("div", { className: "flex items-center gap-2", children: [
2984
3039
  floatingControlsLeading,
2985
- canSizeByViews && /* @__PURE__ */ jsx39(SizeMetricToggle, { value: sizeMetric, onChange: changeSizeMetric })
3040
+ canSizeByViews && /* @__PURE__ */ jsx40(SizeMetricToggle, { value: sizeMetric, onChange: changeSizeMetric })
2986
3041
  ] }) : void 0,
2987
3042
  defaultViewport,
2988
3043
  viewportRestore,
@@ -2990,7 +3045,7 @@ function GenreTreeView({
2990
3045
  ...focus
2991
3046
  }
2992
3047
  ) }) })
2993
- ) : viewMode === "outline" ? /* @__PURE__ */ jsx39("div", { className: "tree-container flex-1 min-h-0 w-full overflow-y-auto relative", children: /* @__PURE__ */ jsx39(
3048
+ ) : viewMode === "outline" ? /* @__PURE__ */ jsx40("div", { className: "tree-container flex-1 min-h-0 w-full overflow-y-auto relative", children: /* @__PURE__ */ jsx40(
2994
3049
  GenrePlaylistTreeWheelRadialPopCore,
2995
3050
  {
2996
3051
  outline: true,
@@ -3007,13 +3062,13 @@ function GenreTreeView({
3007
3062
  selectedNodeId,
3008
3063
  readOnly
3009
3064
  }
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(
3065
+ ) }) : /* @__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
3066
  ([uuid, genrePlaylistTreePerRoot]) => {
3012
- return /* @__PURE__ */ jsx39(
3067
+ return /* @__PURE__ */ jsx40(
3013
3068
  "div",
3014
3069
  {
3015
3070
  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(
3071
+ children: /* @__PURE__ */ jsx40("div", { className: "graph-container relative z-10 h-full", children: /* @__PURE__ */ jsx40(
3017
3072
  GenrePlaylistTreePerRoot,
3018
3073
  {
3019
3074
  scope,
@@ -3040,7 +3095,7 @@ function GenreTreeView({
3040
3095
  );
3041
3096
  }
3042
3097
  ) }),
3043
- /* @__PURE__ */ jsx39(
3098
+ /* @__PURE__ */ jsx40(
3044
3099
  DetailPanel2,
3045
3100
  {
3046
3101
  ref: panelRef,
@@ -3055,7 +3110,7 @@ function GenreTreeView({
3055
3110
  onExpandedChange: activePanel?.onExpandedChange,
3056
3111
  aside: activePanel?.aside,
3057
3112
  edgeTab: detailTab,
3058
- children: activePanel?.aside && !activePanel.expanded ? /* @__PURE__ */ jsxs27(Fragment15, { children: [
3113
+ children: activePanel?.aside && !activePanel.expanded ? /* @__PURE__ */ jsxs28(Fragment16, { children: [
3059
3114
  activePanel.content,
3060
3115
  activePanel.aside
3061
3116
  ] }) : activePanel?.content
@@ -3081,7 +3136,7 @@ import { ListMusic } from "lucide-react";
3081
3136
  import { useTrackList as useTrackList7 } from "@behindthemusictree/app-kit/genre-tree";
3082
3137
  import { TrackListOriginType as TrackListOriginType3 } from "@behindthemusictree/app-kit/genre-tree";
3083
3138
  import { useTrackNoun as useTrackNoun3 } from "@behindthemusictree/app-kit/track-noun";
3084
- import { jsx as jsx40, jsxs as jsxs28 } from "react/jsx-runtime";
3139
+ import { jsx as jsx41, jsxs as jsxs29 } from "react/jsx-runtime";
3085
3140
  var useBrowserLayoutEffect = typeof window === "undefined" ? useEffect7 : useLayoutEffect2;
3086
3141
  function TrackListSidebar({
3087
3142
  className: className2 = "",
@@ -3183,25 +3238,25 @@ function TrackListSidebar({
3183
3238
  }
3184
3239
  };
3185
3240
  const positionClasses = layout === "inline" ? "relative w-full flex h-full flex-col" : "fixed right-0 w-[50rem] flex flex-col";
3186
- return trackList ? /* @__PURE__ */ jsxs28(
3241
+ return trackList ? /* @__PURE__ */ jsxs29(
3187
3242
  "div",
3188
3243
  {
3189
3244
  "data-theme": "dark",
3190
3245
  className: `track-list-sidebar ${positionClasses} rounded-2xl bg-[var(--color-surface)] pb-1 ${className2}`,
3191
3246
  style: layout === "fixed" ? { top: "63.5px", maxHeight: "calc(100vh - 63.5px - 79px)" } : void 0,
3192
3247
  children: [
3193
- /* @__PURE__ */ jsxs28("div", { className: "header flex h-16 items-center px-4 py-2 text-[var(--color-text)]", children: [
3194
- /* @__PURE__ */ jsxs28("div", { className: "min-w-0 flex flex-1 flex-col justify-center", children: [
3195
- /* @__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 }) })
3248
+ /* @__PURE__ */ jsxs29("div", { className: "header flex h-16 items-center px-4 py-2 text-[var(--color-text)]", children: [
3249
+ /* @__PURE__ */ jsxs29("div", { className: "min-w-0 flex flex-1 flex-col justify-center", children: [
3250
+ /* @__PURE__ */ jsxs29("div", { className: "origin flex text-xl", children: [
3251
+ /* @__PURE__ */ jsx41("div", { className: "from h-auto flex flex-col justify-center items-center mr-2", children: "From" }),
3252
+ /* @__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
3253
  ] }),
3199
- /* @__PURE__ */ jsxs28("div", { className: "info flex items-center text-body text-[var(--color-text-muted)]", children: [
3254
+ /* @__PURE__ */ jsxs29("div", { className: "info flex items-center text-body text-[var(--color-text-muted)]", children: [
3200
3255
  trackList.origin.type === TrackListOriginType3.GENRE_PLAYLIST ? "Genre \u2022 " : trackList.origin.type === TrackListOriginType3.ARTIST ? "Artist \u2022 " : `${trackNoun.singular} playlist \u2022 `,
3201
3256
  `${trackList.total} ${trackList.total > 1 ? trackNoun.plural : trackNoun.singular}`
3202
3257
  ] })
3203
3258
  ] }),
3204
- (isCollapsed && onExpand || !isCollapsed && onMinimize) && /* @__PURE__ */ jsx40(
3259
+ (isCollapsed && onExpand || !isCollapsed && onMinimize) && /* @__PURE__ */ jsx41(
3205
3260
  "button",
3206
3261
  {
3207
3262
  type: "button",
@@ -3210,11 +3265,11 @@ function TrackListSidebar({
3210
3265
  "aria-expanded": !isCollapsed,
3211
3266
  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
3267
  onClick: isCollapsed ? onExpand : onMinimize,
3213
- children: /* @__PURE__ */ jsx40(ListMusic, { "aria-hidden": "true", className: "h-5 w-5" })
3268
+ children: /* @__PURE__ */ jsx41(ListMusic, { "aria-hidden": "true", className: "h-5 w-5" })
3214
3269
  }
3215
3270
  )
3216
3271
  ] }),
3217
- !isCollapsed && /* @__PURE__ */ jsxs28(
3272
+ !isCollapsed && /* @__PURE__ */ jsxs29(
3218
3273
  "ul",
3219
3274
  {
3220
3275
  ref: trackListRef,
@@ -3224,7 +3279,7 @@ function TrackListSidebar({
3224
3279
  onPointerDown: markUserScrollIntent,
3225
3280
  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
3281
  children: [
3227
- trackList.tracks.map((track, index) => /* @__PURE__ */ jsx40("li", { "data-track-uuid": track.uuid, children: /* @__PURE__ */ jsx40(
3282
+ trackList.tracks.map((track, index) => /* @__PURE__ */ jsx41("li", { "data-track-uuid": track.uuid, children: /* @__PURE__ */ jsx41(
3228
3283
  TrackItem,
3229
3284
  {
3230
3285
  track,
@@ -3235,7 +3290,7 @@ function TrackListSidebar({
3235
3290
  renderSongLink
3236
3291
  }
3237
3292
  ) }, track.uuid)),
3238
- hasMore && /* @__PURE__ */ jsx40("li", { ref: loadMoreSentinelRef, "aria-hidden": "true", className: "h-px" })
3293
+ hasMore && /* @__PURE__ */ jsx41("li", { ref: loadMoreSentinelRef, "aria-hidden": "true", className: "h-px" })
3239
3294
  ]
3240
3295
  }
3241
3296
  )
@@ -3260,7 +3315,7 @@ import { PanelEdgeTab } from "@behindthemusictree/ui";
3260
3315
  import { Pause as Pause4, Play as Play4, SkipBack, SkipForward, Volume2, VolumeX } from "lucide-react";
3261
3316
  import { formatTime, useTrackList as useTrackList8 } from "@behindthemusictree/app-kit/genre-tree";
3262
3317
  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";
3318
+ import { jsx as jsx42, jsxs as jsxs30 } from "react/jsx-runtime";
3264
3319
  function PlayerControls() {
3265
3320
  const {
3266
3321
  duration,
@@ -3279,15 +3334,15 @@ function PlayerControls() {
3279
3334
  const safeDuration = Number.isFinite(duration) && duration > 0 ? duration : 0;
3280
3335
  const safeCurrentTime = Number.isFinite(currentTime) ? Math.max(0, Math.min(currentTime, safeDuration)) : 0;
3281
3336
  const isPlaying = playState === PlayStates4.PLAYING;
3282
- return /* @__PURE__ */ jsxs29(
3337
+ return /* @__PURE__ */ jsxs30(
3283
3338
  "section",
3284
3339
  {
3285
3340
  "aria-label": "Player controls",
3286
3341
  className: "player-controls flex shrink-0 flex-col gap-2 border-b border-zinc-800 px-3 py-2 text-zinc-200",
3287
3342
  children: [
3288
- /* @__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(
3343
+ /* @__PURE__ */ jsxs30("div", { className: "flex min-w-0 items-center gap-2 text-xs tabular-nums text-zinc-400", children: [
3344
+ /* @__PURE__ */ jsx42("span", { "aria-label": "Elapsed time", children: formatTime(safeCurrentTime) }),
3345
+ /* @__PURE__ */ jsx42(
3291
3346
  "input",
3292
3347
  {
3293
3348
  "aria-label": "Seek",
@@ -3301,11 +3356,11 @@ function PlayerControls() {
3301
3356
  onChange: (event) => seekTo(Number(event.currentTarget.value))
3302
3357
  }
3303
3358
  ),
3304
- /* @__PURE__ */ jsx41("span", { "aria-label": "Duration", children: formatTime(safeDuration) })
3359
+ /* @__PURE__ */ jsx42("span", { "aria-label": "Duration", children: formatTime(safeDuration) })
3305
3360
  ] }),
3306
- /* @__PURE__ */ jsxs29("div", { className: "grid min-h-9 grid-cols-[1fr_auto_1fr] items-center gap-3", children: [
3307
- /* @__PURE__ */ jsxs29("div", { className: "col-start-2 flex items-center justify-center gap-2", children: [
3308
- /* @__PURE__ */ jsx41(
3361
+ /* @__PURE__ */ jsxs30("div", { className: "grid min-h-9 grid-cols-[1fr_auto_1fr] items-center gap-3", children: [
3362
+ /* @__PURE__ */ jsxs30("div", { className: "col-start-2 flex items-center justify-center gap-2", children: [
3363
+ /* @__PURE__ */ jsx42(
3309
3364
  "button",
3310
3365
  {
3311
3366
  type: "button",
@@ -3314,10 +3369,10 @@ function PlayerControls() {
3314
3369
  disabled: !isReady || !hasPreviousTrack,
3315
3370
  onClick: playPreviousTrack,
3316
3371
  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" })
3372
+ children: /* @__PURE__ */ jsx42(SkipBack, { "aria-hidden": "true", className: "h-4 w-4" })
3318
3373
  }
3319
3374
  ),
3320
- /* @__PURE__ */ jsx41(
3375
+ /* @__PURE__ */ jsx42(
3321
3376
  "button",
3322
3377
  {
3323
3378
  type: "button",
@@ -3326,10 +3381,10 @@ function PlayerControls() {
3326
3381
  disabled: !isReady,
3327
3382
  onClick: handlePlayPauseAction,
3328
3383
  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" })
3384
+ 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
3385
  }
3331
3386
  ),
3332
- /* @__PURE__ */ jsx41(
3387
+ /* @__PURE__ */ jsx42(
3333
3388
  "button",
3334
3389
  {
3335
3390
  type: "button",
@@ -3338,14 +3393,14 @@ function PlayerControls() {
3338
3393
  disabled: !isReady || !hasNextTrack,
3339
3394
  onClick: playNextTrack,
3340
3395
  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" })
3396
+ children: /* @__PURE__ */ jsx42(SkipForward, { "aria-hidden": "true", className: "h-4 w-4" })
3342
3397
  }
3343
3398
  )
3344
3399
  ] }),
3345
- /* @__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(
3400
+ /* @__PURE__ */ jsxs30("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: [
3401
+ 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" }),
3402
+ /* @__PURE__ */ jsx42("span", { className: "sr-only", children: "Volume" }),
3403
+ /* @__PURE__ */ jsx42(
3349
3404
  "input",
3350
3405
  {
3351
3406
  "aria-label": "Volume",
@@ -3367,7 +3422,7 @@ function PlayerControls() {
3367
3422
  }
3368
3423
 
3369
3424
  // src/genre-tree/PlayerPanel.tsx
3370
- import { jsx as jsx42, jsxs as jsxs30 } from "react/jsx-runtime";
3425
+ import { jsx as jsx43, jsxs as jsxs31 } from "react/jsx-runtime";
3371
3426
  var PANEL_ID = "player-panel";
3372
3427
  var DEFAULT_EXTENDED_WIDTH = "calc(100vw - var(--layout-center-left) - var(--layout-gutter))";
3373
3428
  function PlayerPanel({
@@ -3407,7 +3462,7 @@ function PlayerPanel({
3407
3462
  onClick: () => setIsExtendRequested(true),
3408
3463
  expanded: false
3409
3464
  } : null;
3410
- return /* @__PURE__ */ jsxs30(
3465
+ return /* @__PURE__ */ jsxs31(
3411
3466
  "div",
3412
3467
  {
3413
3468
  id: PANEL_ID,
@@ -3420,7 +3475,7 @@ function PlayerPanel({
3420
3475
  ),
3421
3476
  style: isExtended ? { width: extendedWidth } : void 0,
3422
3477
  children: [
3423
- /* @__PURE__ */ jsx42(
3478
+ /* @__PURE__ */ jsx43(
3424
3479
  PanelEdgeTab,
3425
3480
  {
3426
3481
  side: "right",
@@ -3430,7 +3485,7 @@ function PlayerPanel({
3430
3485
  className: "absolute right-full top-1/2 -translate-y-1/2"
3431
3486
  }
3432
3487
  ),
3433
- /* @__PURE__ */ jsxs30(
3488
+ /* @__PURE__ */ jsxs31(
3434
3489
  "div",
3435
3490
  {
3436
3491
  className: twMerge(
@@ -3438,20 +3493,20 @@ function PlayerPanel({
3438
3493
  isExtended ? "flex-row" : "flex-col"
3439
3494
  ),
3440
3495
  children: [
3441
- /* @__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 }) }),
3443
- /* @__PURE__ */ jsxs30("div", { className: "relative w-full shrink-0", children: [
3444
- /* @__PURE__ */ jsx42(PlayerVideoSurface, { className: "aspect-video w-full bg-black" }),
3445
- playerTrackObject?.loadError && /* @__PURE__ */ jsxs30("span", { className: "absolute bottom-2 left-2 right-2 text-xs text-red-400", children: [
3496
+ /* @__PURE__ */ jsxs31("div", { className: "flex w-[var(--layout-right-w)] shrink-0 flex-col", children: [
3497
+ /* @__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 }) }),
3498
+ /* @__PURE__ */ jsxs31("div", { className: "relative w-full shrink-0", children: [
3499
+ /* @__PURE__ */ jsx43(PlayerVideoSurface, { className: "aspect-video w-full bg-black" }),
3500
+ playerTrackObject?.loadError && /* @__PURE__ */ jsxs31("span", { className: "absolute bottom-2 left-2 right-2 text-xs text-red-400", children: [
3446
3501
  playerTrackObject.loadError,
3447
- /* @__PURE__ */ jsx42("button", { type: "button", className: "ml-2 underline", onClick: () => {
3502
+ /* @__PURE__ */ jsx43("button", { type: "button", className: "ml-2 underline", onClick: () => {
3448
3503
  void retryPlayback();
3449
3504
  }, children: "Retry" })
3450
3505
  ] })
3451
3506
  ] }),
3452
- /* @__PURE__ */ jsx42(PlayerControls, {})
3507
+ /* @__PURE__ */ jsx43(PlayerControls, {})
3453
3508
  ] }),
3454
- /* @__PURE__ */ jsx42(
3509
+ /* @__PURE__ */ jsx43(
3455
3510
  TrackListSidebar,
3456
3511
  {
3457
3512
  layout: "inline",
@@ -3478,11 +3533,11 @@ function PlayerPanel({
3478
3533
  import { v4 as uuidv4 } from "uuid";
3479
3534
  import { AiFillStar, AiOutlineStar } from "react-icons/ai";
3480
3535
  import { FORM_RATING_NULL_VALUE } from "@behindthemusictree/app-kit/genre-tree";
3481
- import { jsx as jsx43, jsxs as jsxs31 } from "react/jsx-runtime";
3536
+ import { jsx as jsx44, jsxs as jsxs32 } from "react/jsx-runtime";
3482
3537
  function Rating({ rating, handleChange }) {
3483
3538
  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(
3539
+ return /* @__PURE__ */ jsx44("div", { className: "flex items-center", children: [...Array(6)].map((_, i) => /* @__PURE__ */ jsxs32("div", { className: `relative ${i === 0 ? "mr-4" : "mr-1"}`, children: [
3540
+ /* @__PURE__ */ jsx44(
3486
3541
  "input",
3487
3542
  {
3488
3543
  id: `star-${instanceId}-${i}`,
@@ -3494,16 +3549,16 @@ function Rating({ rating, handleChange }) {
3494
3549
  onChange: handleChange
3495
3550
  }
3496
3551
  ),
3497
- /* @__PURE__ */ jsx43("label", { htmlFor: `star-${instanceId}-${i}`, className: "cursor-pointer", children: i === 0 ? /* @__PURE__ */ jsxs31("div", { className: "flex", children: [
3498
- /* @__PURE__ */ jsx43(
3552
+ /* @__PURE__ */ jsx44("label", { htmlFor: `star-${instanceId}-${i}`, className: "cursor-pointer", children: i === 0 ? /* @__PURE__ */ jsxs32("div", { className: "flex", children: [
3553
+ /* @__PURE__ */ jsx44(
3499
3554
  AiOutlineStar,
3500
3555
  {
3501
3556
  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
3557
  }
3503
3558
  ),
3504
- /* @__PURE__ */ jsx43("span", { className: "flex-grow w-8" }),
3559
+ /* @__PURE__ */ jsx44("span", { className: "flex-grow w-8" }),
3505
3560
  " "
3506
- ] }) : /* @__PURE__ */ jsx43(
3561
+ ] }) : /* @__PURE__ */ jsx44(
3507
3562
  AiFillStar,
3508
3563
  {
3509
3564
  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 +3570,7 @@ function Rating({ rating, handleChange }) {
3515
3570
  // src/genre-tree/EntityDetailPanel.tsx
3516
3571
  import { useState as useState12 } from "react";
3517
3572
  import { DetailPanel as DetailPanel3 } from "@behindthemusictree/ui";
3518
- import { Fragment as Fragment16, jsx as jsx44, jsxs as jsxs32 } from "react/jsx-runtime";
3573
+ import { Fragment as Fragment17, jsx as jsx45, jsxs as jsxs33 } from "react/jsx-runtime";
3519
3574
  function EntityDetailPanel({
3520
3575
  scope,
3521
3576
  getBackendBaseUrl,
@@ -3527,6 +3582,7 @@ function EntityDetailPanel({
3527
3582
  onArtistUuidChange,
3528
3583
  renderArtistLink,
3529
3584
  renderSongLink,
3585
+ renderPlaceLink,
3530
3586
  collapsed,
3531
3587
  edgeTab,
3532
3588
  ...navigation
@@ -3538,8 +3594,9 @@ function EntityDetailPanel({
3538
3594
  getBackendBaseUrl,
3539
3595
  getArtistBackendBaseUrl,
3540
3596
  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 })),
3597
+ renderPlaceLink,
3598
+ renderArtistLink: renderArtistLink ?? ((artist) => artist.isSpecialPurpose ? artist.name : /* @__PURE__ */ jsx45("button", { type: "button", onClick: () => onArtistUuidChange(artist.uuid), className: "hover:underline", children: artist.name })),
3599
+ renderSongLink: renderSongLink ?? ((song) => /* @__PURE__ */ jsx45("button", { type: "button", onClick: () => onSongUuidChange(song.uuid), className: "hover:underline", children: song.title })),
3543
3600
  song: {
3544
3601
  uuid: songUuid,
3545
3602
  expanded: songUuid !== null && expandedSongUuid === songUuid,
@@ -3555,7 +3612,7 @@ function EntityDetailPanel({
3555
3612
  ...navigation
3556
3613
  });
3557
3614
  if (!panel) return null;
3558
- return /* @__PURE__ */ jsx44(
3615
+ return /* @__PURE__ */ jsx45(
3559
3616
  DetailPanel3,
3560
3617
  {
3561
3618
  side: "left",
@@ -3567,7 +3624,7 @@ function EntityDetailPanel({
3567
3624
  onExpandedChange: panel.onExpandedChange,
3568
3625
  aside: panel.extraContent,
3569
3626
  edgeTab,
3570
- children: panel.extraContent && !panel.expanded ? /* @__PURE__ */ jsxs32(Fragment16, { children: [
3627
+ children: panel.extraContent && !panel.expanded ? /* @__PURE__ */ jsxs33(Fragment17, { children: [
3571
3628
  panel.content,
3572
3629
  panel.extraContent
3573
3630
  ] }) : panel.content
@@ -3601,4 +3658,4 @@ export {
3601
3658
  GenreTreeViewSkeleton2 as GenreTreeViewSkeleton,
3602
3659
  GenreTreeOutlineSkeleton
3603
3660
  };
3604
- //# sourceMappingURL=chunk-DPYUE6WD.js.map
3661
+ //# sourceMappingURL=chunk-PVYUC2PN.js.map