feat: two-column top section — tracks + releases side-by-side

Replace full-width top tracks with a split layout: top tracks on the
left (5 default), top releases grid on the right (2×2 = 4 default).
A 'Show more' toggle below both columns expands to 10 tracks and 8
releases. Top releases are sorted newest-first from the existing
discography data.

The full discography section remains below for browsing by type.
This commit is contained in:
2026-03-29 16:21:01 -04:00
parent 7472e31545
commit a312c3d2cf
@@ -92,6 +92,7 @@ export class ExploreArtistDetails extends LitElement {
@state() private loadingSimilar = true; @state() private loadingSimilar = true;
@state() private artistImageURL = ''; @state() private artistImageURL = '';
@state() private similarImageURLs = new Map<string, string>(); @state() private similarImageURLs = new Map<string, string>();
@state() private topSectionExpanded = false;
private libraryMBIDs = new Set<string>(); private libraryMBIDs = new Set<string>();
/* ── Styles ── */ /* ── Styles ── */
@@ -316,6 +317,54 @@ export class ExploreArtistDetails extends LitElement {
white-space: nowrap; white-space: nowrap;
} }
/* ── Top section (tracks + releases side-by-side) ── */
.top-section-columns {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 24px;
}
.top-section-column {
min-width: 0;
}
.top-releases-grid {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 12px;
}
.top-section-toggle {
display: flex;
align-items: center;
justify-content: center;
gap: 6px;
padding: 6px 12px;
margin-top: 12px;
border: none;
border-radius: 6px;
background: var(--yj-bg-overlay, rgba(255, 255, 255, 0.06));
color: var(--yj-text-secondary, #b3b3b3);
font-size: var(--yj-text-sm);
cursor: pointer;
transition: background 0.15s ease, color 0.15s ease;
width: 100%;
}
.top-section-toggle:hover {
background: var(--yj-bg-hover, rgba(255, 255, 255, 0.1));
color: var(--yj-text-primary, #fff);
}
.top-section-toggle wa-icon {
font-size: 12px;
transition: transform 0.2s ease;
}
.top-section-toggle[aria-expanded='true'] wa-icon {
transform: rotate(180deg);
}
/* ── Discography grid ── */ /* ── Discography grid ── */
.disco-group { .disco-group {
display: flex; display: flex;
@@ -828,7 +877,7 @@ export class ExploreArtistDetails extends LitElement {
</div> </div>
</div> </div>
<div class="content"> <div class="content">
${this.renderTopTracks()} ${this.renderDiscography()} ${this.renderTopSection()} ${this.renderDiscography()}
${this.renderSimilarArtists()} ${this.renderSimilarArtists()}
</div> </div>
`; `;
@@ -866,54 +915,113 @@ export class ExploreArtistDetails extends LitElement {
`; `;
} }
/* ── Top Tracks Section ── */ /* ── Top Section (tracks + releases side-by-side) ── */
private renderTopTracks() { private toggleTopSection() {
if (this.loadingTracks) { this.topSectionExpanded = !this.topSectionExpanded;
}
/** Top releases = all release groups sorted newest-first. */
private get topReleases(): MBReleaseGroup[] {
return [...this.releaseGroups].sort((a, b) => {
const da = a.firstReleaseDate || '';
const db = b.firstReleaseDate || '';
return db.localeCompare(da);
});
}
private renderTopSection() {
const hasTracks = !this.loadingTracks && this.topTracks.length > 0;
const hasReleases = !this.loadingReleases && this.releaseGroups.length > 0;
const isLoading = this.loadingTracks || this.loadingReleases;
if (isLoading) {
return html` return html`
<section> <section>
<h3 class="section-header">Top Tracks</h3> <h3 class="section-header">Popular</h3>
<div class="section-loading">Loading\u2026</div> <div class="section-loading">Loading\u2026</div>
</section> </section>
`; `;
} }
if (this.errorTracks) {
return html` if (!hasTracks && !hasReleases) return nothing;
<section>
<h3 class="section-header">Top Tracks</h3> const expanded = this.topSectionExpanded;
<div class="section-error"> const trackLimit = expanded ? 10 : 5;
<wa-icon name="triangle-exclamation"></wa-icon> const releaseLimit = expanded ? 8 : 4;
${this.errorTracks}
</div> const tracks = this.topTracks.slice(0, trackLimit);
</section> const releases = this.topReleases.slice(0, releaseLimit);
`;
} const canExpand =
if (this.topTracks.length === 0) return nothing; this.topTracks.length > 5 || this.releaseGroups.length > 4;
return html` return html`
<section> <section>
<h3 class="section-header">Top Tracks</h3> <div class="top-section-columns">
<div class="track-list"> ${hasTracks
${this.topTracks.map( ? html`
(t, i) => html` <div class="top-section-column">
<div class="track-item"> <h3 class="section-header">Top Tracks</h3>
<span class="track-rank">${i + 1}</span> <div class="track-list">
<div class="track-info"> ${tracks.map(
<div class="track-title"> (t, i) => html`
${t.trackName} <div class="track-item">
</div> <span class="track-rank"
<div class="track-artist"> >${i + 1}</span
${t.artistName} >
</div> <div class="track-info">
</div> <div
<span class="track-listens"> class="track-title"
${formatListenCount(t.totalListenCount)} >
plays ${t.trackName}
</span> </div>
</div> <div
`, class="track-artist"
)} >
${t.artistName}
</div>
</div>
<span class="track-listens">
${formatListenCount(
t.totalListenCount,
)}
plays
</span>
</div>
`,
)}
</div>
</div>
`
: nothing}
${hasReleases
? html`
<div class="top-section-column">
<h3 class="section-header">Top Releases</h3>
<div class="top-releases-grid">
${releases.map((rg) =>
this.renderAlbumCard(rg),
)}
</div>
</div>
`
: nothing}
</div> </div>
${canExpand
? html`
<button
class="top-section-toggle"
@click=${this.toggleTopSection}
aria-expanded="${expanded}"
>
${expanded ? 'Show less' : 'Show more'}
<wa-icon
name="chevron-down"
></wa-icon>
</button>
`
: nothing}
</section> </section>
`; `;
} }