Watchlist scan deck v2: portrait-anchored hero, zero layout shift (#831 polish)

Boulder's screenshots: the v1 deck shifted around depending on what data had
arrived (the album row vanished entirely without art, leaving floating
"Processing…/Processing…" text), the images were small, and the feed header
floated in empty space. Redesigned in the artist-detail-page language:

- Big 148px square portrait (rounded, shadowed) anchors the left side, with
  the current album stamped as a 62px overlay badge in its corner — when art
  is missing, both keep their slot and show a glyph placeholder instead of
  collapsing, so the deck NEVER changes shape mid-scan.
- 24px artist name + uppercase accent phase line + a fixed-height
  "now checking" block (accent left rule) for album + track, with stable
  placeholders ("Looking for new releases…" / "—") instead of doubled
  "Processing…" text.
- The additions feed is an inset fixed-height panel (artist-page sidebar
  style): same size whether 0 or 10 tracks, empty state centered.
- JS: hide the artist photo when the CURRENT artist has none (previously the
  prior artist's photo lingered), cleaner placeholder copy.
This commit is contained in:
BoulderBadgeDad 2026-06-09 20:57:27 -07:00
parent 9f12bdfef6
commit 34e0503fad
3 changed files with 139 additions and 56 deletions

View file

@ -6994,17 +6994,17 @@
</div>
<div class="wl-scan-progress"><div id="wl-scan-progress-bar" class="wl-scan-progress-bar" style="width: 0%;"></div></div>
<div class="wl-scan-deck-body">
<div class="wl-scan-stage">
<div class="wl-scan-stage-row">
<img id="watchlist-artist-img" class="wl-scan-artist-img" src="" alt="" onerror="this.style.display='none';" />
<div class="wl-scan-stage-text">
<div id="watchlist-artist-name" class="wl-scan-artist-name">Waiting…</div>
<div id="wl-scan-phase" class="wl-scan-phase">starting…</div>
<div class="wl-scan-hero">
<div class="wl-scan-portrait">
<img id="watchlist-artist-img" class="wl-scan-portrait-img" src="" alt="" onerror="this.style.display='none';" />
<div class="wl-scan-album-thumb">
<img id="watchlist-album-img" src="" alt="" onerror="this.style.display='none';" />
</div>
</div>
<div class="wl-scan-stage-row">
<img id="watchlist-album-img" class="wl-scan-album-img" src="" alt="" onerror="this.style.display='none';" />
<div class="wl-scan-stage-text">
<div class="wl-scan-hero-text">
<div id="watchlist-artist-name" class="wl-scan-artist-name">Waiting…</div>
<div id="wl-scan-phase" class="wl-scan-phase">starting…</div>
<div class="wl-scan-now">
<div id="watchlist-album-name" class="wl-scan-album-name"></div>
<div id="watchlist-track-name" class="wl-scan-track-name"></div>
</div>

View file

@ -3276,15 +3276,21 @@ function handleWatchlistScanData(data) {
if (liveActivity && data.status === 'scanning') {
liveActivity.style.display = liveActivity.classList.contains('wl-scan-deck') ? 'block' : 'flex';
// Update artist image and name
// Update artist image and name (hide the img when THIS artist has no
// photo, so the previous artist's portrait doesn't linger and the
// glyph placeholder shows instead)
const artistImg = document.getElementById('watchlist-artist-img');
const artistName = document.getElementById('watchlist-artist-name');
if (artistImg && data.current_artist_image_url) {
artistImg.src = data.current_artist_image_url;
artistImg.style.display = 'block';
if (artistImg) {
if (data.current_artist_image_url) {
artistImg.src = data.current_artist_image_url;
artistImg.style.display = 'block';
} else {
artistImg.style.display = 'none';
}
}
if (artistName) {
artistName.textContent = data.current_artist_name || 'Processing...';
artistName.textContent = data.current_artist_name || 'Starting…';
}
// Update album image and name
@ -3297,13 +3303,14 @@ function handleWatchlistScanData(data) {
albumImg.style.display = 'none';
}
if (albumName) {
albumName.textContent = data.current_album || (data.current_phase === 'fetching_discography' ? 'Fetching releases...' : 'Processing...');
albumName.textContent = data.current_album
|| (data.current_phase === 'fetching_discography' ? 'Fetching releases…' : 'Looking for new releases…');
}
// Update current track
// Update current track ('' keeps the slot without echoing the album line)
const trackName = document.getElementById('watchlist-track-name');
if (trackName) {
trackName.textContent = data.current_track_name || (data.current_phase === 'fetching_discography' ? 'Fetching releases...' : 'Processing...');
trackName.textContent = data.current_track_name || '—';
}
// #831 round 2: scan-deck extras — artist progress bar, live counters,

View file

@ -20077,57 +20077,100 @@ body.helper-mode-active #dashboard-activity-feed:hover {
.wl-scan-deck-body {
display: grid;
grid-template-columns: minmax(260px, 1fr) minmax(280px, 1.2fr);
gap: 20px;
grid-template-columns: minmax(320px, 1.1fr) minmax(300px, 1fr);
gap: 24px;
align-items: stretch;
}
@media (max-width: 760px) {
.wl-scan-deck-body { grid-template-columns: 1fr; }
}
.wl-scan-stage {
display: flex;
flex-direction: column;
gap: 14px;
min-width: 0;
}
.wl-scan-stage-row {
/* Hero big square portrait (artist-page language) with the current album
stamped as an overlay badge, so the layout NEVER shifts when album art is
missing: the badge keeps its slot and shows a glyph placeholder instead. */
.wl-scan-hero {
display: flex;
align-items: center;
gap: 12px;
gap: 20px;
min-width: 0;
}
.wl-scan-artist-img {
width: 64px;
height: 64px;
border-radius: 50%;
border: 2px solid rgb(var(--accent-rgb));
box-shadow: 0 0 18px rgba(var(--accent-rgb), 0.3);
object-fit: cover;
background: #1a1a1a;
.wl-scan-portrait {
position: relative;
width: 148px;
height: 148px;
border-radius: 16px;
background: #181818;
box-shadow: 0 8px 28px rgba(0, 0, 0, 0.45), 0 0 0 1px rgba(var(--accent-rgb), 0.25);
flex-shrink: 0;
}
.wl-scan-album-img {
width: 64px;
height: 64px;
border-radius: 8px;
border: 1px solid rgba(255, 255, 255, 0.15);
object-fit: cover;
background: #1a1a1a;
flex-shrink: 0;
transition: opacity 0.3s ease;
/* glyph placeholder behind the (possibly hidden) artist image */
.wl-scan-portrait::before {
content: '♪';
position: absolute;
inset: 0;
display: flex;
align-items: center;
justify-content: center;
font-size: 42px;
color: rgba(255, 255, 255, 0.12);
}
.wl-scan-stage-text {
.wl-scan-portrait-img {
position: absolute;
inset: 0;
width: 100%;
height: 100%;
border-radius: 16px;
object-fit: cover;
}
.wl-scan-album-thumb {
position: absolute;
right: -12px;
bottom: -12px;
width: 62px;
height: 62px;
border-radius: 10px;
background: #181818;
border: 3px solid #121212;
box-shadow: 0 4px 14px rgba(0, 0, 0, 0.55);
overflow: hidden;
}
.wl-scan-album-thumb::before {
content: '♪';
position: absolute;
inset: 0;
display: flex;
align-items: center;
justify-content: center;
font-size: 18px;
color: rgba(255, 255, 255, 0.15);
}
.wl-scan-album-thumb img {
position: absolute;
inset: 0;
width: 100%;
height: 100%;
object-fit: cover;
}
.wl-scan-hero-text {
min-width: 0;
display: flex;
flex-direction: column;
justify-content: center;
gap: 4px;
}
.wl-scan-artist-name {
font-size: 16px;
font-weight: 700;
font-size: 24px;
font-weight: 800;
letter-spacing: -0.3px;
color: #fff;
overflow: hidden;
text-overflow: ellipsis;
@ -20136,14 +20179,27 @@ body.helper-mode-active #dashboard-activity-feed:hover {
.wl-scan-phase {
font-size: 12px;
color: rgba(var(--accent-light-rgb), 0.85);
margin-top: 2px;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.6px;
color: rgba(var(--accent-light-rgb), 0.9);
}
/* Fixed-height "now checking" block placeholders keep the slot when the
scan hasn't reached an album/track yet, so nothing jumps. */
.wl-scan-now {
margin-top: 8px;
padding: 8px 12px;
border-left: 2px solid rgba(var(--accent-rgb), 0.5);
background: rgba(255, 255, 255, 0.03);
border-radius: 0 8px 8px 0;
min-height: 44px;
}
.wl-scan-album-name {
font-size: 13px;
font-size: 14px;
font-weight: 600;
color: rgba(255, 255, 255, 0.85);
color: rgba(255, 255, 255, 0.9);
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
@ -20158,27 +20214,47 @@ body.helper-mode-active #dashboard-activity-feed:hover {
white-space: nowrap;
}
/* Feed inset panel (artist-page sidebar language) with a FIXED height so
the deck is the same size whether 0 or 10 tracks have been added. */
.wl-scan-feed {
min-width: 0;
display: flex;
flex-direction: column;
height: 172px;
padding: 12px 14px;
background: rgba(0, 0, 0, 0.3);
border: 1px solid rgba(255, 255, 255, 0.07);
border-radius: 12px;
}
.wl-scan-feed-label {
font-size: 11px;
font-weight: 600;
font-weight: 700;
text-transform: uppercase;
letter-spacing: 0.5px;
letter-spacing: 0.6px;
color: #6fd99a;
margin-bottom: 6px;
margin-bottom: 8px;
flex-shrink: 0;
}
.wl-scan-feed-list {
max-height: 176px;
flex: 1;
min-height: 0;
overflow-y: auto;
display: flex;
flex-direction: column;
gap: 5px;
}
.wl-scan-feed-list .watchlist-live-addition-empty {
flex: 1;
display: flex;
align-items: center;
justify-content: center;
color: rgba(255, 255, 255, 0.3);
font-size: 12px;
}
.wl-scan-feed-list .watchlist-live-addition-item {
padding: 5px 8px;
border-radius: 6px;