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 @@
-