navigation: one glass bar for the app's life, not one per page
The bar was rendered by each view, so `<CurrentView />` 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.
This commit is contained in:
1 parent
98d11a2228
commit
8017c0d877
4 files changed
+42
-10
No files matched your search
@@ -7,6 +7,7 @@
|
|||||||
import LoginModal from "./components/LoginModal.svelte";
|
import LoginModal from "./components/LoginModal.svelte";
|
||||||
import PreviewOverlay from "./components/PreviewOverlay.svelte";
|
import PreviewOverlay from "./components/PreviewOverlay.svelte";
|
||||||
import DebugOverlay from "./components/DebugOverlay.svelte";
|
import DebugOverlay from "./components/DebugOverlay.svelte";
|
||||||
|
import GlassBar from "./components/GlassBar.svelte";
|
||||||
import { previewUi } from "./lib/preview/previewSession.svelte.js";
|
import { previewUi } from "./lib/preview/previewSession.svelte.js";
|
||||||
import Home from "./views/Home.svelte";
|
import Home from "./views/Home.svelte";
|
||||||
import Search from "./views/Search.svelte";
|
import Search from "./views/Search.svelte";
|
||||||
@@ -39,6 +40,13 @@
|
|||||||
};
|
};
|
||||||
const CurrentView = $derived(views[ui.view] ?? Home);
|
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();
|
initEvents();
|
||||||
autoConnect();
|
autoConnect();
|
||||||
refreshLibrary();
|
refreshLibrary();
|
||||||
@@ -62,6 +70,11 @@
|
|||||||
The titlebar stays live for window chrome. -->
|
The titlebar stays live for window chrome. -->
|
||||||
<Sidebar />
|
<Sidebar />
|
||||||
<main class="content" use:floatScroll inert={previewUi.open || undefined}>
|
<main class="content" use:floatScroll inert={previewUi.open || undefined}>
|
||||||
|
<!-- First child of the scroll pane: the bar's sticky pin and its
|
||||||
|
negative bleed margins are measured against THIS element's
|
||||||
|
padding, and its net flow height is zero, so the page below still
|
||||||
|
starts exactly where it did when each view carried its own. -->
|
||||||
|
<GlassBar hidden={!BAR_VIEWS.has(ui.view)} />
|
||||||
<CurrentView />
|
<CurrentView />
|
||||||
</main>
|
</main>
|
||||||
<NowPlaying />
|
<NowPlaying />
|
||||||
|
|||||||
@@ -2,6 +2,15 @@
|
|||||||
// The header every catalog page navigates by — a frosted strip pinned to
|
// 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.
|
// 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 `<GlassBar />` 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
|
// 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
|
// Playing panel's `.np-head`. Geometry, tint, blur radius and the hairline
|
||||||
// edge are all load-bearing; see the notes on the rules below.
|
// 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 { canGoBack, canGoForward, trail } from "../lib/navigation.svelte.js";
|
||||||
import Icon from "./Icon.svelte";
|
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 crumbs = $derived(nav.stack.length ? trail() : []);
|
||||||
const back = $derived(canGoBack());
|
const back = $derived(canGoBack());
|
||||||
@@ -38,6 +48,10 @@
|
|||||||
const DUR = 300;
|
const DUR = 300;
|
||||||
const EASE = "cubic-bezier(0.22, 1, 0.36, 1)";
|
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();
|
let seen = new Set();
|
||||||
function slideIn(node, key) {
|
function slideIn(node, key) {
|
||||||
// Only genuinely new crumbs animate. Without this every crumb re-runs
|
// Only genuinely new crumbs animate. Without this every crumb re-runs
|
||||||
@@ -66,7 +80,7 @@
|
|||||||
|
|
||||||
<!-- data-scrollhead keeps the overlay scrollbar's thumb below this bar rather
|
<!-- data-scrollhead keeps the overlay scrollbar's thumb below this bar rather
|
||||||
than running under it (see src/lib/scrollbar.js). -->
|
than running under it (see src/lib/scrollbar.js). -->
|
||||||
<div class="glassbar" data-scrollhead>
|
<div class="glassbar" class:hidden data-scrollhead>
|
||||||
<div class="hist">
|
<div class="hist">
|
||||||
<button class="arrow" disabled={!back} title="Back" aria-label="Back" onclick={navBack}>
|
<button class="arrow" disabled={!back} title="Back" aria-label="Back" onclick={navBack}>
|
||||||
<Icon name="chevron-left" size={17} />
|
<Icon name="chevron-left" size={17} />
|
||||||
@@ -146,6 +160,14 @@
|
|||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* `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 gets 0 here — correct for a page with no bar), while the
|
||||||
|
COMPONENT stays alive and keeps its crumb memory. */
|
||||||
|
.glassbar.hidden {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
.hist {
|
.hist {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
|||||||
@@ -29,7 +29,6 @@
|
|||||||
import { listenerHeat, listenerPeak, listenerShare } from "../lib/listenerBars.js";
|
import { listenerHeat, listenerPeak, listenerShare } from "../lib/listenerBars.js";
|
||||||
import { consumeScroll, restoreScroll } from "../lib/navigation.svelte.js";
|
import { consumeScroll, restoreScroll } from "../lib/navigation.svelte.js";
|
||||||
import Icon from "../components/Icon.svelte";
|
import Icon from "../components/Icon.svelte";
|
||||||
import GlassBar from "../components/GlassBar.svelte";
|
|
||||||
|
|
||||||
const page = $derived(catalog.albumPage); // { album, tracks, album_plays, … }
|
const page = $derived(catalog.albumPage); // { album, tracks, album_plays, … }
|
||||||
const album = $derived(page?.album ?? null);
|
const album = $derived(page?.album ?? null);
|
||||||
@@ -509,8 +508,6 @@
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="album-page" style={accentStyle} bind:this={pageEl}>
|
<div class="album-page" style={accentStyle} bind:this={pageEl}>
|
||||||
<GlassBar />
|
|
||||||
|
|
||||||
{#if catalog.albumLoading}
|
{#if catalog.albumLoading}
|
||||||
<p class="hint">Loading album…</p>
|
<p class="hint">Loading album…</p>
|
||||||
{:else if !page}
|
{:else if !page}
|
||||||
|
|||||||
@@ -44,7 +44,6 @@
|
|||||||
import { openPreview } from "../lib/preview/previewSession.svelte.js";
|
import { openPreview } from "../lib/preview/previewSession.svelte.js";
|
||||||
import { traceToSvg, svgDataUrl } from "../lib/trace.js";
|
import { traceToSvg, svgDataUrl } from "../lib/trace.js";
|
||||||
import Icon from "../components/Icon.svelte";
|
import Icon from "../components/Icon.svelte";
|
||||||
import GlassBar from "../components/GlassBar.svelte";
|
|
||||||
import { consumeScroll, restoreScroll } from "../lib/navigation.svelte.js";
|
import { consumeScroll, restoreScroll } from "../lib/navigation.svelte.js";
|
||||||
import HeroGpuCanvas from "../components/HeroGpuCanvas.svelte";
|
import HeroGpuCanvas from "../components/HeroGpuCanvas.svelte";
|
||||||
|
|
||||||
@@ -4459,10 +4458,11 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- The shared frosted header (src/components/GlassBar.svelte); it
|
<!-- The shared frosted header (src/components/GlassBar.svelte) floats
|
||||||
floats over the seam tail's mirror at the top. Its -24px margin is
|
over the seam tail's mirror at the top. It is mounted by App.svelte
|
||||||
what the pin geometry below is measured against. -->
|
as the scroll pane's first child, not here — but its -24px bleed is
|
||||||
<GlassBar />
|
still what the pin geometry below is measured against, and its net
|
||||||
|
flow height is zero either way, so this spacer's top is unmoved. -->
|
||||||
|
|
||||||
<!-- Pure spacer now: it still reserves the room the artwork needs and
|
<!-- Pure spacer now: it still reserves the room the artwork needs and
|
||||||
still sets where the content begins (both from its aspect-ratio,
|
still sets where the content begins (both from its aspect-ratio,
|
||||||
|
|||||||
Reference in new issue
Block a user