fix(tracks): RC3 layout regressions + multi-artist links (#976)

* fix(tracks): RC3 layout regressions + multi-artist links

- Restore vertical rhythm on the Tracks hub: the header/hero/rails/browse
  sections sat two wrapper divs below .tracks-page so its flex gap never
  reached them and they collapsed together. New .tracks-hub-stack re-applies
  the gap, fixing the tagline-touching-hero and Random-Pick-overlapping-hero
  spacing (the rail's nav buttons no longer ride into the box above).
- Stop the sticky browse-table header going transparent on hover: its base
  background became opaque but the :hover rule still forced transparent, so
  rows bled through. Header now keeps its card background on hover.
- Widen the Duration column (56px -> 72px; 56 -> 64 on mobile) so the
  "DURATION" header no longer clips/overruns.
- Split multi-artist tracks into individually clickable artist links in both
  the "Track of the moment" hero and the browse list rows (OpenSubsonic
  artists[] with single-artist fallback), matching the album tracklist.

* docs(changelog): Tracks RC3 spacing, Duration, header hover, multi-artist (#976)
This commit is contained in:
Frank Stellmacher
2026-06-04 02:28:13 +02:00
committed by GitHub
parent 908c349cfd
commit 88df194808
8 changed files with 64 additions and 19 deletions
+9
View File
@@ -855,6 +855,15 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
* Hiding **Mainstage** from the sidebar no longer leaves the app opening on a blank page — it now starts on the first visible library entry instead.
* When every Mainstage section is turned off, the page shows a short message with a shortcut into **Settings → Personalisation** rather than appearing empty.
### Tracks — spacing, Duration column, header hover, multi-artist links
**By [@Psychotoxical](https://github.com/Psychotoxical), reported by zunoz on Discord, PR [#976](https://github.com/Psychotoxical/psysonic/pull/976)**
* The Tracks hub sections (tagline, **Track of the moment**, **Random Pick** rail, **Browse all tracks**) no longer bunch together — even vertical spacing is restored, so the rail's navigation buttons stop riding up into the card above.
* The **Browse all tracks** table no longer clips the **Duration** column header.
* The track-list column header keeps its background on hover instead of turning transparent and letting rows show through.
* **Track of the moment** and the browse rows split multi-artist tracks into individually clickable artist links, matching the album track list.
## [1.46.0] - 2026-05-18
> **🙏 Special thanks to [@zz5zz](https://github.com/zz5zz)** for his tireless quirk-spotting and bug reports on the [Psysonic Discord](https://discord.gg/AMnDRErm4u) — several of the polish fixes in this release landed directly off the back of his messages.
+18 -7
View File
@@ -39,6 +39,13 @@ function SongRow({ song, showBpm }: Props) {
enqueue([songToTrack(song)]);
};
// Split multi-artist tracks into individually clickable links (OpenSubsonic
// `artists[]`), falling back to the single flat artist when absent — mirrors
// the album tracklist so a "A · B" credit isn't one link to a single artist.
const artistRefs = song.artists && song.artists.length > 0
? song.artists
: [{ id: song.artistId, name: song.artist }];
const bpmTooltip =
song.localBpmSource === 'analysis'
? t('search.bpmSourceAnalysis')
@@ -93,13 +100,17 @@ function SongRow({ song, showBpm }: Props) {
</button>
</div>
<div className="song-list-row-cell song-list-row-title truncate" title={song.title}>{song.title}</div>
<div className="song-list-row-cell truncate">
<span
className={song.artistId ? 'track-artist-link' : ''}
style={{ cursor: song.artistId ? 'pointer' : 'default' }}
onClick={(e) => { if (song.artistId) { e.stopPropagation(); navigateToArtist(song.artistId); } }}
title={song.artist}
>{song.artist}</span>
<div className="song-list-row-cell truncate" title={song.artist}>
{artistRefs.map((a, i) => (
<React.Fragment key={a.id ?? a.name ?? i}>
{i > 0 && <span className="track-artist-sep">&nbsp;·&nbsp;</span>}
<span
className={a.id ? 'track-artist-link' : ''}
style={{ cursor: a.id ? 'pointer' : 'default' }}
onClick={(e) => { if (a.id) { e.stopPropagation(); navigateToArtist(a.id!); } }}
>{a.name ?? song.artist}</span>
</React.Fragment>
))}
</div>
<div className="song-list-row-cell truncate">
{song.albumId ? (
+18 -5
View File
@@ -105,6 +105,14 @@ export default function TracksPageChrome({
[random, hero],
);
// Split a multi-artist feature into individually clickable links
// (OpenSubsonic `artists[]`), falling back to the single flat artist.
const heroArtistRefs = hero
? (hero.artists && hero.artists.length > 0
? hero.artists
: [{ id: hero.artistId, name: hero.artist }])
: [];
return (
<>
{!perfFlags.disableMainstageStickyHeader && (
@@ -140,11 +148,16 @@ export default function TracksPageChrome({
</span>
<h2 className="tracks-hero-title" title={hero.title}>{hero.title}</h2>
<p className="tracks-hero-meta">
<span
className={hero.artistId ? 'track-artist-link' : ''}
style={{ cursor: hero.artistId ? 'pointer' : 'default' }}
onClick={() => hero.artistId && navigateToArtist(hero.artistId)}
>{hero.artist}</span>
{heroArtistRefs.map((a, i) => (
<React.Fragment key={a.id ?? a.name ?? i}>
{i > 0 && <span className="track-artist-sep">&nbsp;·&nbsp;</span>}
<span
className={a.id ? 'track-artist-link' : ''}
style={{ cursor: a.id ? 'pointer' : 'default' }}
onClick={() => a.id && navigateToArtist(a.id)}
>{a.name ?? hero.artist}</span>
</React.Fragment>
))}
{hero.album && (
<>
<span className="tracks-hero-meta-dot">·</span>
+1 -1
View File
@@ -852,7 +852,7 @@ export default function SearchBrowsePage() {
data-advanced-search-root
>
<div style={{ visibility: isLeaveRestorePending ? 'hidden' : 'visible' }}>
<div>
<div className={showTracksChrome ? 'tracks-hub-stack' : undefined}>
{showTracksChrome ? (
<>
<TracksPageChrome
+10
View File
@@ -9,6 +9,16 @@
padding-bottom: var(--space-8, 2rem);
}
/* The hub sections (header, hero, rails, browse list) live two wrapper divs
deep inside .tracks-page, so the page-level flex gap never reached them and
the sections collapsed together. This stack restores even vertical rhythm
between them. */
.tracks-hub-stack {
display: flex;
flex-direction: column;
gap: var(--space-6, 1.5rem);
}
.tracks-header {
display: flex;
align-items: flex-end;
+2 -2
View File
@@ -18,11 +18,11 @@
}
.song-list-row {
grid-template-columns: 64px minmax(0, 1.5fr) minmax(0, 1fr) 56px;
grid-template-columns: 64px minmax(0, 1.5fr) minmax(0, 1fr) 64px;
}
.song-list-row--with-bpm {
grid-template-columns: 64px minmax(0, 1.5fr) minmax(0, 1fr) 48px 56px;
grid-template-columns: 64px minmax(0, 1.5fr) minmax(0, 1fr) 48px 64px;
}
.song-list-row-cell:nth-child(4), /* album */
@@ -2,7 +2,7 @@
.song-list-row {
display: grid;
grid-template-columns: 64px minmax(0, 1.5fr) minmax(0, 1fr) minmax(0, 1fr) 110px 56px;
grid-template-columns: 64px minmax(0, 1.5fr) minmax(0, 1fr) minmax(0, 1fr) 110px 72px;
gap: var(--space-3);
align-items: center;
height: 52px;
@@ -43,8 +43,11 @@
border-bottom: 1px solid var(--border-subtle);
}
/* Header is sticky and opaque — keep its card background on hover so the
generic .song-list-row:hover rule doesn't turn it transparent (which let
rows scrolling underneath bleed through). */
.song-list-row--header:hover {
background: transparent;
background: var(--bg-card);
}
.song-list-row-cell {
@@ -75,7 +78,7 @@
}
.song-list-row--with-bpm {
grid-template-columns: 64px minmax(0, 1.5fr) minmax(0, 1fr) minmax(0, 1fr) 110px 48px 56px;
grid-template-columns: 64px minmax(0, 1.5fr) minmax(0, 1fr) minmax(0, 1fr) 110px 48px 72px;
}
.song-list-row-bpm {
-1
View File
@@ -4,7 +4,6 @@
display: flex;
flex-direction: column;
gap: var(--space-3);
margin-top: var(--space-2);
}
.virtual-song-list-title {