forked from Keelrleek/SoulStream
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:
1 parent
0955dc02ce
commit
d67df6f55e
1 file changed
+73
-8
@@ -3415,6 +3415,22 @@
|
||||
{/if}
|
||||
<p class="hint">{catalog.error || "Artist not found."}</p>
|
||||
{: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
|
||||
approved as research/artist-header-scroll/v12-three-layers.html
|
||||
build 5). Content on top (z 3) → the page background SHEET with
|
||||
@@ -3784,12 +3800,6 @@
|
||||
<div class="fade-flow">
|
||||
{#if page.partial_error}
|
||||
<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 page.art_preview && !popularTracks.length && !page.albums.length}
|
||||
@@ -6249,8 +6259,63 @@
|
||||
contain-intrinsic-size: auto 360px;
|
||||
}
|
||||
|
||||
.stage-status {
|
||||
min-height: 20px;
|
||||
/* Transient load status, pinned to the pane's top-right and out of flow.
|
||||
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 {
|
||||
|
||||
Reference in new issue
Block a user