From e58410d9f854669e4e6763f7a66d1badfd93e4ab Mon Sep 17 00:00:00 2001 From: KeelrLeek <156356974+KeelrLeek@users.noreply.github.com> Date: Fri, 24 Jul 2026 17:51:41 -0500 Subject: [PATCH] artist page: always open at the top MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit .content is the app's one scroll pane and it outlives the view swap, so its scrollTop was inherited by whatever loaded next: leave one artist 900px down their discography, open another, and you arrived 900px down the new one — mid-grid, hero already squeezed into the bar, with no hint the page had a top. Reset it on catalog.artistOpenSeq, a counter bumped once per openCatalogArtist call, rather than on the artist's id. The id cannot distinguish a re-open of the artist already on screen — clicking their name from Now Playing while parked deep in their own page, where nothing moved and the click read as dead — from no navigation at all. Nor can this key on the page object: the cold path replaces it stage by stage under the same id, which would haul a reader back to the top mid-read. The counter bumps once per open and never per stage. The bump sits after openCatalogArtist's invalid-id bail-out, so a request that never opens anything cannot scroll the page the reader is still on. Verified live: three artist-to-artist hops parked at 1222 each land at 0 and stay 0 through every cold stage; re-opening the artist already on screen from 1070 lands at 0; mounting from another view with the pane genuinely parked at 1500 mounts at 0; and a reader parked at 600 on the artist they are already on holds at 600 through six seconds of stage churn. --- src/lib/state.svelte.js | 10 ++++++++++ src/views/CatalogArtist.svelte | 19 +++++++++++++++++++ 2 files changed, 29 insertions(+) diff --git a/src/lib/state.svelte.js b/src/lib/state.svelte.js index 747d14d..7823b5e 100644 --- a/src/lib/state.svelte.js +++ b/src/lib/state.svelte.js @@ -751,6 +751,13 @@ export const catalog = $state({ artistLoading: false, artistStageError: "", artistPage: null, // { artist, albums } + // Bumped once per openCatalogArtist call, INCLUDING a re-open of the artist + // already on screen. The view opens itself at the top off this rather than + // off the artist's id: the id cannot tell a re-open from no navigation at + // all, and it cannot tell the cold path's stage-by-stage replacement of + // artistPage (same id, new object, several times a second) from a real one + // — keying on that would drag a reader back to the top mid-read. + artistOpenSeq: 0, albumLoading: false, albumPage: null, // { album, tracks } pendingTrackId: null, // a track to scroll to/flash on the album page @@ -1533,6 +1540,9 @@ export async function openCatalogArtist(artistId, seedArtist = null) { const request = ++artistPageRequest; const artistKey = artistCacheKey(artistId); if (!artistKey) return; + // After the bail-out above, so a request we never act on cannot scroll the + // page the reader is still on. + catalog.artistOpenSeq += 1; activateCoverUpgradeScope(artistId); ui.view = "catalogArtist"; const active = () => diff --git a/src/views/CatalogArtist.svelte b/src/views/CatalogArtist.svelte index d15fac9..a2d64f8 100644 --- a/src/views/CatalogArtist.svelte +++ b/src/views/CatalogArtist.svelte @@ -113,6 +113,25 @@ return () => clearTimeout(t); }); + // Every artist page opens at the top. `.content` is the app's ONE scroll + // pane and it outlives the view swap, so its scrollTop carried over: leave + // one artist 900px down its discography, open another, and you landed 900px + // down the new one — mid-grid, hero already squeezed into the bar, with no + // hint the page had a top. + // Keyed on catalog.artistOpenSeq, NOT the artist's id. The id cannot tell a + // re-open of the artist already on screen (clicking their name from Now + // Playing while parked deep in their own page — nothing moved) from no + // navigation at all. It also cannot be keyed on `page`: the cold path + // replaces that object stage by stage under the same id, which would haul a + // reader back to the top mid-read. The seq bumps once per open and never + // per stage. Ask for the pane BY NAME — the same rule scrollPaneFor() + // follows, and for the same reason. + $effect(() => { + void catalog.artistOpenSeq; + const pane = document.querySelector(".content"); + if (pane) pane.scrollTop = 0; + }); + // THE HERO PIPELINE (Kyle, 2026-07-24 — the approved configuration, // now unconditional): // - Crossfade twins: the main-thread blur filter rides on the hero