discover: unify album sections onto the .ya-card grid + Show-all clamp

Step 2 of the makeover — the album/release sections now use the same .ya-card as Your Artists
(square cover so art is not cropped, name overlaid, artist beneath) in responsive wrapping grids,
replacing the old spotify-library-card / discover-card / carousels:
- Recent Releases, Your Albums (owned/missing badge kept), Undiscovered Albums, New In Your Genres,
  From Your Labels (one shared _cacheDiscoverCard), Seasonal Albums.
- _insertCacheSection gained a wrapGrid flag; Genre Explorer opts out (wrapGrid=false) so its pill
  layout is not squashed into the card grid (fixes the regression where it broke under the hero).
- new _clampGrid caps a grid at ~12 cards with a Show all (N) / Show less toggle, so a 30-item
  section is not a wall; wired into the cache sections + Recent Releases + Seasonal Albums.

64 script-integrity tests green. Next: the two artist sections (Based On Your Listening / Recommended
For You) need their watchlist + live-enrichment hooks moved before they can switch to .ya-card.
This commit is contained in:
BoulderBadgeDad 2026-06-29 14:21:52 -07:00
parent 9c832ab3c3
commit 4cab44889c
3 changed files with 125 additions and 44 deletions

View file

@ -3283,7 +3283,7 @@
<option value="recent">Date Added</option>
</select>
</div>
<div class="spotify-library-grid" id="your-albums-grid">
<div class="discover-grid" id="your-albums-grid">
<div class="discover-loading">
<div class="loading-spinner"></div>
<p>Loading your albums...</p>
@ -3298,7 +3298,7 @@
<h2 class="discover-section-title">Recent Releases</h2>
<p class="discover-section-subtitle">New music from artists you follow</p>
</div>
<div class="discover-carousel" id="recent-releases-carousel">
<div class="discover-grid" id="recent-releases-carousel">
<!-- Content will be populated dynamically -->
<div class="discover-loading">
<div class="loading-spinner"></div>
@ -3313,7 +3313,7 @@
<h2 class="discover-section-title" id="seasonal-albums-title">Seasonal</h2>
<p class="discover-section-subtitle" id="seasonal-albums-subtitle">Seasonal music</p>
</div>
<div class="discover-carousel" id="seasonal-albums-carousel">
<div class="discover-grid" id="seasonal-albums-carousel">
<!-- Content will be populated dynamically -->
</div>
</div>

View file

@ -1008,15 +1008,18 @@ let _recentReleasesCtrl = null;
function _renderRecentReleaseCard(album, index) {
const coverUrl = album.album_cover_url || '/static/placeholder-album.png';
// Unified Discover card — same .ya-card the Your Artists section uses (full-bleed cover,
// gradient, name overlaid), square so the album art isn't cropped. #discover redesign.
return `
<div class="discover-card" onclick="openDownloadModalForRecentAlbum(${index})" style="cursor: pointer;">
<div class="discover-card-image">
<img src="${coverUrl}" alt="${album.album_name}" loading="lazy">
<div class="ya-card discover-album-card" onclick="openDownloadModalForRecentAlbum(${index})">
<div class="ya-card-img">
<img src="${coverUrl}" alt="" loading="lazy" onerror="this.style.display='none';this.nextElementSibling.style.display='flex'">
<div class="ya-card-placeholder" style="display:none">&#9835;</div>
</div>
<div class="discover-card-info">
<h4 class="discover-card-title">${album.album_name}</h4>
<p class="discover-card-subtitle">${album.artist_name}</p>
<p class="discover-card-meta">${album.release_date}</p>
<div class="ya-card-gradient"></div>
<div class="ya-card-info">
<div class="ya-card-name">${_esc(album.album_name)}</div>
<div class="ya-card-sub">${_esc(album.artist_name)}</div>
</div>
</div>
`;
@ -1043,7 +1046,8 @@ async function loadDiscoverRecentReleases() {
showErrorToast: true,
});
}
return _recentReleasesCtrl.load();
await _recentReleasesCtrl.load();
_clampGrid(document.getElementById('recent-releases-carousel'));
}
// ===============================
@ -1182,22 +1186,19 @@ function _renderYourAlbumsGrid(albums) {
let html = '';
albums.forEach((album, index) => {
const coverUrl = album.image_url || '/static/placeholder-album.png';
const year = album.release_date ? album.release_date.substring(0, 4) : '';
const badgeClass = album.in_library ? 'owned' : 'missing';
const badgeIcon = album.in_library ? '\u2713' : '\u2193';
const trackInfo = album.total_tracks ? `${album.total_tracks} tracks` : '';
const meta = [year, trackInfo].filter(Boolean).join(' \u00B7 ');
const sources = (album.source_services || []).join(', ');
html += `
<div class="spotify-library-card" onclick="openYourAlbumDownload(${index})" title="${escapeHtml(album.album_name)} \u2014 ${escapeHtml(album.artist_name)}">
<div class="spotify-library-card-img">
<img src="${coverUrl}" alt="${escapeHtml(album.album_name)}" loading="lazy">
<div class="spotify-library-card-badge ${badgeClass}">${badgeIcon}</div>
<div class="ya-card discover-album-card" onclick="openYourAlbumDownload(${index})" title="${escapeHtml(album.album_name)} \u2014 ${escapeHtml(album.artist_name)}">
<div class="ya-card-img">
<img src="${coverUrl}" alt="" loading="lazy" onerror="this.style.display='none';this.nextElementSibling.style.display='flex'">
<div class="ya-card-placeholder" style="display:none">&#9835;</div>
</div>
<div class="spotify-library-card-info">
<p class="spotify-library-card-title">${escapeHtml(album.album_name)}</p>
<p class="spotify-library-card-artist">${escapeHtml(album.artist_name)}</p>
<p class="spotify-library-card-meta">${escapeHtml(meta)}</p>
<div class="ya-card-gradient"></div>
<div class="ya-card-badges"><div class="discover-album-badge ${badgeClass}">${badgeIcon}</div></div>
<div class="ya-card-info">
<div class="ya-card-name">${_esc(album.album_name)}</div>
<div class="ya-card-sub">${_esc(album.artist_name)}</div>
</div>
</div>`;
});
@ -4008,14 +4009,15 @@ async function loadSeasonalContent() {
function _renderSeasonalAlbumCard(album, index) {
const coverUrl = album.album_cover_url || '/static/placeholder-album.png';
return `
<div class="discover-card" onclick="openDownloadModalForSeasonalAlbum(${index})" style="cursor: pointer;">
<div class="discover-card-image">
<img src="${coverUrl}" alt="${album.album_name}" loading="lazy">
<div class="ya-card discover-album-card" onclick="openDownloadModalForSeasonalAlbum(${index})">
<div class="ya-card-img">
<img src="${coverUrl}" alt="" loading="lazy" onerror="this.style.display='none';this.nextElementSibling.style.display='flex'">
<div class="ya-card-placeholder" style="display:none">&#9835;</div>
</div>
<div class="discover-card-info">
<h4 class="discover-card-title">${album.album_name}</h4>
<p class="discover-card-subtitle">${album.artist_name}</p>
${album.release_date ? `<p class="discover-card-meta">${album.release_date}</p>` : ''}
<div class="ya-card-gradient"></div>
<div class="ya-card-info">
<div class="ya-card-name">${_esc(album.album_name)}</div>
<div class="ya-card-sub">${_esc(album.artist_name)}</div>
</div>
</div>
`;
@ -4050,7 +4052,8 @@ async function loadSeasonalAlbums(seasonData) {
verboseErrors: true,
showErrorToast: true,
});
return ctrl.load();
await ctrl.load();
_clampGrid(document.getElementById('seasonal-albums-carousel'));
}
let _seasonalPlaylistCtrl = null;
@ -8183,18 +8186,20 @@ function _cacheDiscoverCard(item, type, sectionKey, index) {
const coverUrl = item.image_url || '/static/placeholder-album.png';
const title = item.name || '';
const subtitle = item.artist_name || '';
const meta = item.release_date ? item.release_date.substring(0, 10) : (item.label || '');
const onclick = `openCacheDiscoverAlbum('${sectionKey}',${index})`;
const libBadge = item.in_library ? '<div class="discover-card-lib-badge">In Library</div>' : '';
return `<div class="discover-card" onclick="${onclick}" style="cursor:pointer">
<div class="discover-card-image">
<img src="${_esc(coverUrl)}" alt="${_esc(title)}" loading="lazy" onerror="this.src='/static/placeholder-album.png'">
${libBadge}
// Unified Discover album card (#discover redesign) — same .ya-card as Your Artists, square.
const libBadge = item.in_library
? '<div class="ya-card-badges"><div class="discover-album-badge owned">&#10003;</div></div>' : '';
return `<div class="ya-card discover-album-card" onclick="${onclick}">
<div class="ya-card-img">
<img src="${_esc(coverUrl)}" alt="" loading="lazy" onerror="this.style.display='none';this.nextElementSibling.style.display='flex'">
<div class="ya-card-placeholder" style="display:none">&#9835;</div>
</div>
<div class="discover-card-info">
<h4 class="discover-card-title">${_esc(title)}</h4>
<p class="discover-card-subtitle">${_esc(subtitle)}</p>
${meta ? `<p class="discover-card-meta">${_esc(meta)}</p>` : ''}
<div class="ya-card-gradient"></div>
${libBadge}
<div class="ya-card-info">
<div class="ya-card-name">${_esc(title)}</div>
<div class="ya-card-sub">${_esc(subtitle)}</div>
</div>
</div>`;
}
@ -8339,7 +8344,32 @@ async function openCacheDiscoverAlbum(sectionKey, index) {
}
}
function _insertCacheSection(id, title, subtitle, html, position) {
// Cap a wrapping discover-grid to `limit` cards (≈ a couple rows) with a Show all / Show less
// toggle, so a 30-item section doesn't become a wall (#discover redesign). Idempotent.
function _clampGrid(gridEl, limit = 12) {
if (!gridEl) return;
const cards = Array.from(gridEl.children);
// tidy any previous toggle so re-renders don't stack buttons
if (gridEl.nextElementSibling && gridEl.nextElementSibling.classList.contains('discover-show-all')) {
gridEl.nextElementSibling.remove();
}
if (cards.length <= limit) {
cards.forEach(c => { c.style.display = ''; });
return;
}
let expanded = false;
const btn = document.createElement('button');
btn.className = 'discover-show-all';
const apply = () => {
cards.forEach((c, i) => { c.style.display = (expanded || i < limit) ? '' : 'none'; });
btn.textContent = expanded ? 'Show less' : `Show all ${cards.length}`;
};
btn.onclick = () => { expanded = !expanded; apply(); };
gridEl.after(btn);
apply();
}
function _insertCacheSection(id, title, subtitle, html, position, wrapGrid = true) {
const container = document.getElementById('discover-bylt-sections') || document.querySelector('.discover-container');
if (!container) return;
let section = document.getElementById(id);
@ -8366,8 +8396,9 @@ function _insertCacheSection(id, title, subtitle, html, position) {
<h3 class="discover-section-title">${title}</h3>
</div>
</div>
<div class="discover-carousel">${html}</div>
${wrapGrid ? `<div class="discover-grid">${html}</div>` : html}
`;
if (wrapGrid) _clampGrid(section.querySelector('.discover-grid'));
}
async function loadCacheUndiscoveredAlbums() {
@ -8437,7 +8468,7 @@ async function loadCacheGenreExplorer() {
</div>
`).join('')}</div>`;
_insertCacheSection('cache-genre-explorer',
'Genre Explorer', 'Tap a genre to explore', html, 'top');
'Genre Explorer', 'Tap a genre to explore', html, 'top', false);
} catch (e) { console.debug('Cache genre explorer:', e); }
}

View file

@ -35095,6 +35095,56 @@ div.artist-hero-badge {
.mix-modal-close:hover { color: #fff; }
.mix-modal-body { overflow-y: auto; padding: 16px 24px 24px; }
/* Unified Discover card grid (#discover redesign)
One responsive wrapping grid (fills the section width, wraps to rows no
horizontal scroll) holding the same .ya-card the Your Artists section uses, so
every album/artist section reads as one design. */
.discover-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(160px, 1fr));
gap: 16px;
}
.discover-grid .ya-card { width: 100%; }
/* Albums are square, so don't crop the cover with the artist card's 0.85 portrait ratio. */
.discover-grid .discover-album-card.ya-card { aspect-ratio: 1; }
/* Secondary line under the title (album → artist). */
.ya-card-sub {
font-size: 11px; font-weight: 500;
color: rgba(255,255,255,0.72);
white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
text-shadow: 0 1px 3px rgba(0,0,0,0.6);
margin-top: 2px;
}
/* Owned / missing indicator on album cards (corner badge). */
.discover-album-badge {
width: 20px; height: 20px; border-radius: 50%;
display: flex; align-items: center; justify-content: center;
font-size: 11px; font-weight: 700;
box-shadow: 0 2px 6px rgba(0,0,0,0.45);
}
.discover-album-badge.owned { background: #1DB954; color: #000; }
.discover-album-badge.missing {
background: rgba(0,0,0,0.55); color: rgba(255,255,255,0.85);
border: 1px solid rgba(255,255,255,0.2);
}
/* Show all / Show less toggle under a clamped discover-grid. */
.discover-show-all {
margin: 14px auto 0;
display: block;
background: rgba(255,255,255,0.06);
border: 1px solid rgba(255,255,255,0.12);
color: rgba(255,255,255,0.85);
font-size: 13px; font-weight: 600;
padding: 8px 20px; border-radius: 20px;
cursor: pointer;
transition: background 0.2s, border-color 0.2s, color 0.2s;
}
.discover-show-all:hover {
background: rgba(var(--accent-rgb),0.15);
border-color: rgba(var(--accent-rgb),0.4);
color: #fff;
}
/* ── Artist Info Modal ── */
.ya-info-modal {
background: #141420; border-radius: 18px;