From 8017c0d87740e7bc9f0acab2c9754762efb3132a Mon Sep 17 00:00:00 2001 From: KeelrLeek <156356974+KeelrLeek@users.noreply.github.com> Date: Tue, 28 Jul 2026 17:31:01 -0500 Subject: [PATCH] navigation: one glass bar for the app's life, not one per page The bar was rendered by each view, so `` swapping on every navigation destroyed it and built a new one. Measured: the `.glassbar` element is not the same node on either side of an artist -> album move, and the old one is disconnected. That broke the crumb entrance animation, which is what this was noticed from. The `seen` set exists so only a genuinely ARRIVING crumb slides in, and it is component state -- so it was empty on every mount and every crumb looked new. Instrumenting Element.animate across one navigation: AC/DC -> Highway to Hell should animate: the album actually animated: all three crumbs Back to AC/DC should animate: nothing actually animated: both crumbs So the whole trail re-flew in from the right on every page change, and Back animated crumbs that had never left -- the exact opposite of the direction-of-travel cue the animation was written for. The bar is app chrome, not part of a page, so App.svelte now mounts it once as the scroll pane's first child and pages no longer carry it. Views that do not show it yet hide it with `display: none` rather than an {#if}: the element leaves the layout and measures zero -- scrollbar.js reads this bar's height to start the thumb below it, and correctly gets 0 -- while the component stays alive and keeps its crumb memory across the round trip. Geometry is unchanged, which the negative-margin bleed made easy to get wrong: the bar's net flow height is zero either way, so moving it from first-child-of-the-page to previous-sibling-of-the-page leaves the page's top exactly where it was. Verified against the live pane -- bar top/left/ width flush with the pane and 46 high, page top at pane + 24, z-index 40 and elementFromPoint at the bar's centre still returning the bar rather than the hero underneath. After: arriving at an album animates only the album's own crumb, Back animates nothing, and a trip out to Library and back animates nothing. The backdrop-filter layer is no longer recomposited on every page change, and a bar element can now transition between pages at all, which was impossible when each side was a different node. Two consequences worth stating. The resolver pulse takes the GLOBAL accent everywhere now; on the album page it used to inherit that page's extracted palette, because the bar sat inside `.album-page`. The artist page always gave it the global accent, so the two now agree rather than disagreeing. And putting the bar on the remaining pages is now widening BAR_VIEWS plus deciding how it sits against each page's existing header -- the mounting question is settled. 85 JS tests pass. --- src/App.svelte | 13 +++++++++++++ src/components/GlassBar.svelte | 26 ++++++++++++++++++++++++-- src/views/CatalogAlbum.svelte | 3 --- src/views/CatalogArtist.svelte | 10 +++++----- 4 files changed, 42 insertions(+), 10 deletions(-) diff --git a/src/App.svelte b/src/App.svelte index f44466a..61afb2c 100644 --- a/src/App.svelte +++ b/src/App.svelte @@ -7,6 +7,7 @@ import LoginModal from "./components/LoginModal.svelte"; import PreviewOverlay from "./components/PreviewOverlay.svelte"; import DebugOverlay from "./components/DebugOverlay.svelte"; + import GlassBar from "./components/GlassBar.svelte"; import { previewUi } from "./lib/preview/previewSession.svelte.js"; import Home from "./views/Home.svelte"; import Search from "./views/Search.svelte"; @@ -39,6 +40,13 @@ }; const CurrentView = $derived(views[ui.view] ?? Home); + /* The glass bar is app chrome, not part of a page, so it is mounted ONCE + here and lives above whichever view is current — see the note at the top + of GlassBar.svelte for what a per-view instance cost. Views that do not + show it yet hide it; the component is never unmounted, so its crumb + memory survives every navigation, including out to Home and back. */ + const BAR_VIEWS = new Set(["catalogArtist", "catalogAlbum"]); + initEvents(); autoConnect(); refreshLibrary(); @@ -62,6 +70,11 @@ The titlebar stays live for window chrome. -->
+ +
diff --git a/src/components/GlassBar.svelte b/src/components/GlassBar.svelte index f63b760..3b7b4b4 100644 --- a/src/components/GlassBar.svelte +++ b/src/components/GlassBar.svelte @@ -2,6 +2,15 @@ // The header every catalog page navigates by — a frosted strip pinned to // the top of the scroll pane, floating over whatever artwork is behind it. // + // MOUNTED ONCE, by App.svelte, as the first child of the scroll pane — + // never by a page. A per-view `` was a different component + // instance on either side of every navigation, which cost three things: + // the entrance animation below could not tell an arriving crumb from one + // that was already there (see `seen`), the backdrop-filter layer was + // recomposited on every page change, and nothing in the bar could ever + // transition between pages. Pages it should not appear on hide it rather + // than unmount it, so its memory survives the round trip. + // // It began as the artist page's `.results-head`, in parity with the Now // Playing panel's `.np-head`. Geometry, tint, blur radius and the hairline // edge are all load-bearing; see the notes on the rules below. @@ -20,7 +29,8 @@ import { canGoBack, canGoForward, trail } from "../lib/navigation.svelte.js"; import Icon from "./Icon.svelte"; - let { children } = $props(); + /** `hidden` hides the bar without unmounting it — see the note above. */ + let { children, hidden = false } = $props(); const crumbs = $derived(nav.stack.length ? trail() : []); const back = $derived(canGoBack()); @@ -38,6 +48,10 @@ const DUR = 300; const EASE = "cubic-bezier(0.22, 1, 0.36, 1)"; + // Survives navigation only because this component does: a per-page bar + // remounted with an EMPTY set, so every crumb looked new and the whole + // trail re-flew in on every page change — including on Back, where nothing + // had arrived at all. let seen = new Set(); function slideIn(node, key) { // Only genuinely new crumbs animate. Without this every crumb re-runs @@ -66,7 +80,7 @@ -
+
- - +