artist page: five sections after the hero, and nothing moved to fit them

Latest release, Releases by year, Fans also like, In your library and
About slot into the page between what is already there — hero, Popular,
Discography grid and all — and CatalogArtist gains three lines: an import
and two placements. They live in ArtistExtras so Svelte scopes their
styles away from this file, which matters more than usual here: both
files name a .hero-bg and a .card, for completely different things.

Every section is fed by a call the app already makes. catalog_related is
Deezer's related-artists field, literally commented "Fans also like" and
simply never rendered on this page; the bio and profile calls already
run; latest and the timeline come out of page.albums. Nothing here needs
a provider that did not exist before. Latest release is CatalogSearch's
top-result hero — the app's only surface for presenting one item
prominently — with min-height 0 (there is no stat row to hold space for)
and a 6px artwork radius so it rounds like a record; it tints from its
OWN cover, scoped, so it never wears the playing track's colour. Fans
also like reuses this page's .grid/.release markup, so its columns are
the same 150px tracks as the discography above it, drifting 0px on all
six.

The release grid's min-height reserve had to learn about them. That
reserve is the tab-pinning contract — everything under the pinned tab bar
must span a pane-height — and when the grid was the last thing on the
page the grid had to supply all of it. It is not last any more, so both
reserves now discount the real content below it: without that the grid
stood on 206px of empty min-height under its final card, a hole nobody
could see until something rendered after it. The discount is
parent-bottom minus grid-bottom, which is invariant to the grid's own
min-height because both edges move with it, so the reserve converges
instead of chasing its own write; when nothing follows the grid it is 0
and the figure is exactly the old one. The grid's parent joins the
ResizeObserver, because the sections land asynchronously and grow it
without touching the grid's box or the pane's.

An earlier attempt pulled the sections up by a measured slack instead.
The grid reflowed after the measurement and they landed on top of the
release cards. Reverted: prefer a fix that can only be a few px wrong
over one that can be catastrophically wrong. Verified: expander open and
close leaves scrollTop at 1571, and the tab bar sits at 231 across
switches.
This commit is contained in:
KeelrLeek committed 2026-08-04 14:43:52 -05:00
1 parent 520e2ad4e4
commit 819a2757bc
2 files changed
+894 -5

No files matched your search

+73 -5
View File
@@ -1,5 +1,6 @@
<script>
import { onDestroy, tick, untrack } from "svelte";
import ArtistExtras from "../components/ArtistExtras.svelte";
import { fade, fly } from "svelte/transition";
import { quintOut } from "svelte/easing";
import {
@@ -2933,7 +2934,17 @@
const bar = cachedBar;
if (!pane || !bar) return;
const chrome = node.getBoundingClientRect().top - bar.getBoundingClientRect().top;
let reserve = Math.max(0, Math.round(pane.clientHeight - chrome));
// Real content BELOW the grid fills the pane just as well as the
// grid itself does. This reserve exists so everything under the
// pinned tab bar spans a pane-height; when the grid was the last
// thing on the page, the grid had to supply all of it. It no longer
// is — the added sections (ArtistExtras) follow it — and without
// this discount the grid stood on 206px of empty min-height below
// its last card, a hole that only became visible once something
// rendered after it. When nothing follows the grid, belowGrid is 0
// and the figure is exactly the old one.
const below = belowGrid(node);
let reserve = Math.max(0, Math.round(pane.clientHeight - chrome - below));
// Same argument one level down, for the open expander's own row.
// revealIntoView lifts that row to the banner line, and a row in the
// last screenful has less beneath it than the pane is tall — so the
@@ -2964,10 +2975,18 @@
}
// The cheap half stays live every frame: the pane's height and
// the banner margin move with the window, the row offset does
// not.
// not. Discounted by the content below the grid for the same
// reason as the plain term — the range the reveal needs under
// the open row is supplied by the sections after the grid just
// as well as by empty min-height, and without the discount the
// open panel sat on the same empty band the closed page used
// to. Keep in sync with revealReserve, which the reveal itself
// stands synchronously.
reserve = Math.max(
reserve,
Math.round(rowOff + pane.clientHeight - revealMargin(node)),
Math.round(
rowOff + pane.clientHeight - revealMargin(node) - belowGrid(node),
),
);
}
setReserve(reserve);
@@ -2990,20 +3009,33 @@
// the DOM at all.
let lastW = -1;
let lastPaneH = -1;
// The grid's parent is observed too: the sections after the grid land
// asynchronously (fetches, images), growing the parent without touching
// the grid's or the pane's box — and `below` above is measured against
// exactly that growth. Tracked as its own input so the guard still
// filters pure echoes; the delivery our own min-height write provokes
// does change the parent's height, but measure() then computes an
// identical reserve (below is invariant to the grid's inflation) and
// setReserve no-ops, so it costs one pass and stops.
let lastParentH = -1;
const onResize = (entries) => {
let w = lastW;
let paneH = lastPaneH;
let parentH = lastParentH;
for (const entry of entries) {
const box = entry.borderBoxSize?.[0];
if (entry.target === node) {
w = Math.round(box ? box.inlineSize : entry.contentRect.width);
} else if (entry.target === node.parentElement) {
parentH = Math.round(box ? box.blockSize : entry.contentRect.height);
} else {
paneH = Math.round(box ? box.blockSize : entry.contentRect.height);
}
}
if (w === lastW && paneH === lastPaneH) return;
if (w === lastW && paneH === lastPaneH && parentH === lastParentH) return;
lastW = w;
lastPaneH = paneH;
lastParentH = parentH;
measure();
};
// Anything that changes the DOM rather than the box — an expander
@@ -3013,6 +3045,7 @@
const forced = () => {
lastW = -1;
lastPaneH = -1;
lastParentH = -1;
rowOffCols = -1;
rowOffXp = null;
rowOffRow = null;
@@ -3023,6 +3056,7 @@
gridMeasures.add(forced);
const ro = new ResizeObserver(onResize);
ro.observe(node);
if (node.parentElement) ro.observe(node.parentElement);
const paneEl = cachedPane ?? scrollPaneFor(node);
if (paneEl) ro.observe(paneEl);
return {
@@ -3543,9 +3577,33 @@
- vanishingAbove(grid, row, keep);
}
/** Real content following the grid inside the same scroll flow — the added
* sections (ArtistExtras) and their end spacer. Both reserves exist to
* guarantee scroll RANGE, and range below the grid counts toward it
* wherever it lives; without this discount each reserve inflated the grid
* as if it were still the last thing on the page, which is exactly the
* empty band that showed under the tab bar (closed) and under an open
* panel (revealed). Parent-bottom minus grid-bottom is invariant to the
* grid's own min-height (both edges move together), so a reserve computed
* from it converges instead of chasing its own write. Floored, not
* rounded: over-discounting by half a pixel would leave the reveal's ease
* idling against a target the range misses by that half-pixel. */
function belowGrid(grid) {
const parent = grid?.parentElement;
if (!parent) return 0;
return Math.max(
0,
Math.floor(
parent.getBoundingClientRect().bottom
- grid.getBoundingClientRect().bottom,
),
);
}
function revealReserve(grid, row, pane, keep) {
return Math.round(
revealRowOffset(grid, row, keep) + pane.clientHeight - revealMargin(grid),
revealRowOffset(grid, row, keep) + pane.clientHeight - revealMargin(grid)
- belowGrid(grid),
);
}
@@ -4985,6 +5043,12 @@
</section>
{/if}
<!-- ADDED, not substituted: everything above and below is untouched.
The sections themselves live in ArtistExtras so their styles are
scoped away from this file — it names a .hero-bg and a .card of its
own, and so does this page, for different things. -->
<ArtistExtras {page} part="latest" />
{#if tabs.length}
<div class="disco-head">
<h2 class="disco-title">Discography</h2>
@@ -5333,6 +5397,10 @@
{/each}
</div>
{/each}
<!-- ADDED after the release grid: releases-by-year, fans also like,
what you own, and about. Nothing above was moved to fit them. -->
<ArtistExtras {page} />
</div>
</div>
</div>