artist page: float the load status instead of shoving the page down

"Refining release details…" rendered as a paragraph in the content flow, above
the action row. It cost about 32px of layout, so the play button and everything
under it visibly jumped up the instant the page finished resolving -- a shift
announcing nothing more than that a background stage had ended. It also read as
debug output: 12px secondary text and a bullet, in a page that otherwise speaks
in pills and chips.

It is now an absolutely positioned pill at the pane's top-right with a spinning
accent ring. Measured: the play button and the Popular heading sit at identical
offsets whether it is showing or not, so appearing and resolving move nothing.

Two placement constraints, both found the hard way:

- It must NOT live in .fade-flow with the rest of the body. That carries a
  transform, so the pill would anchor to it rather than the page, and it sits
  inside the clipped .fade-frame, which would mask it.
- z-index has to clear 40, not merely .scrollbody's 3. The sticky .results-head
  shares this top band, and at any lower value it paints straight over the pill
  -- which looks exactly like the pill failing to render, with no other symptom.

.artist-page was already position:relative, so this introduces no new containing
block into a scroll pipeline that is sensitive to exactly that.

The pill is pointer-events:none since it is never a target, carries
role="status" aria-live="polite" so the stage change is announced rather than
silently repainted, and holds the spinner still under prefers-reduced-motion
rather than dropping the only sign that work is happening.
This commit is contained in:
KeelrLeek committed 2026-07-25 20:10:44 -05:00
1 parent 0955dc02ce
commit d67df6f55e
1 file changed
+73 -8
+73 -8
View File
@@ -3415,6 +3415,22 @@
{/if} {/if}
<p class="hint">{catalog.error || "Artist not found."}</p> <p class="hint">{catalog.error || "Artist not found."}</p>
{:else} {:else}
<!-- Loading status, OUT of the document flow on purpose.
In flow it cost ~32px above the action row, so the play button
visibly jumped up the moment the page finished resolving. Absolute
inside .artist-page (already position:relative, so this adds no new
containing block) means it can appear and vanish without moving a
single pixel of content. It must NOT live in .fade-flow with the
rest of the body: that carries a transform and sits inside the
clipped .fade-frame, which would both re-anchor and mask it. -->
{#if page.art_preview}
<div class="stage-pill" role="status" aria-live="polite">
<span class="ring" aria-hidden="true"></span>
{page.albums.length || popularTracks.length
? "Refining release details"
: "Loading songs and releases"}
</div>
{/if}
<!-- ROUND 12, "three layers" (the user's own model, mocked and <!-- ROUND 12, "three layers" (the user's own model, mocked and
approved as research/artist-header-scroll/v12-three-layers.html approved as research/artist-header-scroll/v12-three-layers.html
build 5). Content on top (z 3) → the page background SHEET with build 5). Content on top (z 3) → the page background SHEET with
@@ -3784,12 +3800,6 @@
<div class="fade-flow"> <div class="fade-flow">
{#if page.partial_error} {#if page.partial_error}
<p class="resolving error">{page.partial_error}</p> <p class="resolving error">{page.partial_error}</p>
{:else if page.art_preview}
<p class="resolving stage-status"><span class="pulse">●</span>
{page.albums.length || popularTracks.length
? "Refining release details…"
: "Loading songs and releases…"}
</p>
{/if} {/if}
{#if page.art_preview && !popularTracks.length && !page.albums.length} {#if page.art_preview && !popularTracks.length && !page.albums.length}
@@ -6249,8 +6259,63 @@
contain-intrinsic-size: auto 360px; contain-intrinsic-size: auto 360px;
} }
.stage-status { /* Transient load status, pinned to the pane's top-right and out of flow.
min-height: 20px; It shares the top band with the sticky .results-head (z-index 40), which
is why this has to clear 40 rather than merely clearing .scrollbody's 3 —
at any lower value the bar paints straight over it and the pill is
invisible with no other symptom. */
.stage-pill {
position: absolute;
top: 16px;
right: 20px;
z-index: 41;
display: inline-flex;
align-items: center;
gap: 9px;
height: 30px;
padding: 0 14px;
border-radius: 15px;
background: rgba(14, 14, 16, 0.72);
border: 1px solid rgba(255, 255, 255, 0.1);
box-shadow: 0 6px 22px rgba(0, 0, 0, 0.4);
backdrop-filter: blur(14px);
color: var(--text-secondary);
font-size: 12px;
font-weight: 500;
white-space: nowrap;
pointer-events: none;
animation: stage-pill-in 180ms ease-out;
}
.stage-pill .ring {
width: 11px;
height: 11px;
border-radius: 50%;
border: 2px solid var(--accent);
border-right-color: transparent;
animation: stage-pill-spin 0.9s linear infinite;
}
@keyframes stage-pill-spin {
to {
transform: rotate(360deg);
}
}
@keyframes stage-pill-in {
from {
opacity: 0;
transform: translateY(-4px);
}
}
/* The spinner is decoration; honour a reduced-motion preference by holding
it still rather than removing the only signal that work is happening. */
@media (prefers-reduced-motion: reduce) {
.stage-pill,
.stage-pill .ring {
animation: none;
}
} }
.catalog-skeleton { .catalog-skeleton {