android: no bar arrives over the page — the tab pages lose their top banner

The glass bar was anonymous until you scrolled, then faded in carrying the
title the page already had in 27px type two centimetres below it. On Home that
meant "Good morning" over "Good morning", half-transparent, while the content
it covered kept moving underneath. Kyle's word: remove them.

So a tab page has no bar at all. `barOn`/`titled` and the scroll trigger they
read are gone; `showBar` is `!root`, and the element is not rendered on Home,
Search, Library, Community or You. Nothing fades in over what you are reading.

The bar was not only decoration, which is why this is more than a deletion: its
⋯ is the page overflow, and on a tab page that overflow is the app's own menu —
the queue, the sleep timer, Settings. That moves to where the page's title
already is, in `.phead`, beside Home's identicon and at the end of Library's
existing row of actions. Search and You grow the row they did not have.
Community already carried its own ⋯ and keeps it.

A pushed page still has a bar, because it carries the back chevron and a title
that is not repeated anywhere else on the screen. It is static now: rendered
only when there is somewhere to go back to, on from the first frame and titled
from the first frame. The opacity-0 default, the 180ms cross-fade, and the
`.gb-t` rise the title used to arrive on all come out of the stylesheet —
nothing is left that can catch the eye mid-scroll.

The walk reached that overflow through `.gbar .iconbtn` on a tab page, which is
now a selector that matches nothing there: `home-more` on both step lists and
the two sleep-timer steps go to `.phead .iconbtn.muted` instead. The other
`.gbar` steps are untouched on purpose — the playlist export sheet is a PUSHED
page and still has its bar, and `library-ambience` is the album page's back
chevron, not an overflow.

Checked on the device rather than argued: on Home `.gbar` is absent, the ⋯ is
present, and tapping it opens the half sheet on "SoulStream · Queue · Sleep
timer off · Settings"; on a pushed record the bar is there with its two
buttons, its title, and a computed opacity of exactly 1.

One thing this leaves: the bar was also what backed the OS status bar once a
page scrolled under it. The clock and the battery now sit on the content. That
is fine on the ground and on dark artwork, and it will not be over a white
sleeve — a short scrim with no title and no buttons would fix it without
bringing a banner back, and is deliberately not in this commit.
This commit is contained in:
KeelrLeek committed 2026-09-14 11:09:34 -05:00
1 parent 07dad65ca5
commit cdb861fcbd
7 files changed
+26 -28

No files matched your search

+13 -10
View File
@@ -117,9 +117,10 @@
} }
}); });
const groundPath = $derived(meta.amb ?? player.album?.art_path ?? null); const groundPath = $derived(meta.amb ?? player.album?.art_path ?? null);
const trigger = $derived(meta.hero ? 210 : 52); /* A tab page has no bar at all: nothing fades in over what you are
const barOn = $derived(!root || scrolled > trigger); reading. A pushed page has one because it carries the back chevron,
const titled = $derived(scrolled > trigger); and it is static — there and titled from the first frame. */
const showBar = $derived(!root);
const playing = $derived(player.status === "playing" || player.status === "buffering"); const playing = $derived(player.status === "playing" || player.status === "buffering");
const played = $derived(player.duration ? Math.min(1, player.position / player.duration) : 0); const played = $derived(player.duration ? Math.min(1, player.position / player.duration) : 0);
@@ -382,13 +383,15 @@
{/key} {/key}
</div> </div>
<!-- the glass bar: GlassBar.svelte's recipe; anonymous until the <!-- the bar a pushed page carries: back, the title, and the page's ⋯.
page's own large title has scrolled under it --> A tab page has none — see showBar. -->
<div class="gbar" class:on={barOn} class:titled> {#if showBar}
<button class="iconbtn" style:visibility={root ? "hidden" : "visible"} onclick={back} aria-label="Back"><Icon name="chevron-left" size={24} /></button> <div class="gbar">
<span class="gb-t">{meta.title}{#if meta.kicker && meta.kicker !== meta.title}<small>{meta.kicker}</small>{/if}</span> <button class="iconbtn" onclick={back} aria-label="Back"><Icon name="chevron-left" size={24} /></button>
<button class="iconbtn muted" onclick={() => openHalf("more")} aria-label="More"><Icon name="more" size={21} /></button> <span class="gb-t">{meta.title}{#if meta.kicker && meta.kicker !== meta.title}<small>{meta.kicker}</small>{/if}</span>
</div> <button class="iconbtn muted" onclick={() => openHalf("more")} aria-label="More"><Icon name="more" size={21} /></button>
</div>
{/if}
</div> </div>
{#if player.track} {#if player.track}
+6 -3
View File
@@ -150,9 +150,12 @@
<div class="kick">{dateKick()}</div> <div class="kick">{dateKick()}</div>
<h1>{greeting()}</h1> <h1>{greeting()}</h1>
</div> </div>
<button class="iconbtn" onclick={() => go("you")} aria-label="You"> <div class="acts">
<Identicon seed={lib.username || "SoulStream"} size={34} /> <button class="iconbtn muted" onclick={() => openHalf("more")} aria-label="More"><Icon name="more" size={21} /></button>
</button> <button class="iconbtn" onclick={() => go("you")} aria-label="You">
<Identicon seed={lib.username || "SoulStream"} size={34} />
</button>
</div>
</div> </div>
</div> </div>
+1
View File
@@ -102,6 +102,7 @@
<button class="iconbtn" onclick={() => go("search")} aria-label="Search"><Icon name="search" size={21} /></button> <button class="iconbtn" onclick={() => go("search")} aria-label="Search"><Icon name="search" size={21} /></button>
<button class="iconbtn" onclick={() => patch({ tab: "playlists" })} aria-label="Playlists"><Icon name="playlist" size={21} /></button> <button class="iconbtn" onclick={() => patch({ tab: "playlists" })} aria-label="Playlists"><Icon name="playlist" size={21} /></button>
<button class="iconbtn" onclick={() => openHalf("librarysort")} aria-label="Sort and filter"><Icon name="layers" size={21} /></button> <button class="iconbtn" onclick={() => openHalf("librarysort")} aria-label="Sort and filter"><Icon name="layers" size={21} /></button>
<button class="iconbtn muted" onclick={() => openHalf("more")} aria-label="More"><Icon name="more" size={21} /></button>
</div> </div>
</div> </div>
</div> </div>
+1 -1
View File
@@ -287,7 +287,7 @@
} }
</script> </script>
<div class="phead"><div class="row"><h1>Search</h1></div></div> <div class="phead"><div class="row"><h1>Search</h1><div class="acts"><button class="iconbtn muted" onclick={() => openHalf("more")} aria-label="More"><Icon name="more" size={21} /></button></div></div></div>
<div class="sfield"> <div class="sfield">
<Icon name="search" size={18} /> <Icon name="search" size={18} />
+1 -1
View File
@@ -189,7 +189,7 @@
]; ];
</script> </script>
<div class="phead"><div class="row"><h1>You</h1></div></div> <div class="phead"><div class="row"><h1>You</h1><div class="acts"><button class="iconbtn muted" onclick={() => openHalf("more")} aria-label="More"><Icon name="more" size={21} /></button></div></div></div>
<div class="sheet-sec" style="padding-top: 4px">Appearance</div> <div class="sheet-sec" style="padding-top: 4px">Appearance</div>
<div class="group"> <div class="group">
-9
View File
@@ -117,11 +117,7 @@
the solid floor first. */ the solid floor first. */
background: rgba(18, 18, 22, 0.92); background: rgba(18, 18, 22, 0.92);
box-shadow: 0 1px 0 rgba(255, 255, 255, 0.05); box-shadow: 0 1px 0 rgba(255, 255, 255, 0.05);
opacity: 0;
transition: opacity 180ms ease;
pointer-events: none;
} }
.gbar.on { opacity: 1; pointer-events: auto; }
html[data-glass="blur"] .gbar { background: rgba(20, 20, 24, 0.28); backdrop-filter: blur(12px) saturate(1.7); -webkit-backdrop-filter: blur(12px) saturate(1.7); } html[data-glass="blur"] .gbar { background: rgba(20, 20, 24, 0.28); backdrop-filter: blur(12px) saturate(1.7); -webkit-backdrop-filter: blur(12px) saturate(1.7); }
html[data-material="solid"] .gbar { background: rgba(24, 24, 24, 0.94); backdrop-filter: none; -webkit-backdrop-filter: none; } html[data-material="solid"] .gbar { background: rgba(24, 24, 24, 0.94); backdrop-filter: none; -webkit-backdrop-filter: none; }
.gbar .gb-t { .gbar .gb-t {
@@ -134,12 +130,7 @@ html[data-material="solid"] .gbar { background: rgba(24, 24, 24, 0.94); backdrop
min-width: 0; min-width: 0;
flex: 1; flex: 1;
text-shadow: 0 1px 4px rgba(0, 0, 0, 0.45); text-shadow: 0 1px 4px rgba(0, 0, 0, 0.45);
/* the bar arrives before its title does */
opacity: 0;
transform: translateY(5px);
transition: opacity 200ms ease, transform 240ms cubic-bezier(0.22, 1, 0.36, 1);
} }
.gbar.titled .gb-t { opacity: 1; transform: none; }
.gbar .gb-t small { .gbar .gb-t small {
display: block; display: block;
font-size: 10.5px; font-size: 10.5px;
+4 -4
View File
@@ -966,7 +966,7 @@ const MOCK_STEPS = [
{ do: `__settle()`, wait: 600 }, { do: `__settle()`, wait: 600 },
{ do: `document.querySelectorAll('.nav button')[0].click()`, name: "home-again", wait: 700 }, { do: `document.querySelectorAll('.nav button')[0].click()`, name: "home-again", wait: 700 },
/* the page overflow → the sleep timer */ /* the page overflow → the sleep timer */
{ do: `document.querySelectorAll('.gbar .iconbtn')[1].click()`, name: "home-more", wait: 700 }, { do: `document.querySelector('.phead .iconbtn.muted').click()`, name: "home-more", wait: 700 },
{ do: `__click('.halfsheet .row', 'Sleep timer')`, name: "sleep-sheet", wait: 700 }, { do: `__click('.halfsheet .row', 'Sleep timer')`, name: "sleep-sheet", wait: 700 },
{ do: `__click('.halfsheet .chip', '30 min')`, name: "sleep-set", wait: 700 }, { do: `__click('.halfsheet .chip', '30 min')`, name: "sleep-set", wait: 700 },
/* ── the phone turned over. A resize, so the app keeps playing and keeps /* ── the phone turned over. A resize, so the app keeps playing and keeps
@@ -2105,12 +2105,12 @@ const PHONE_STEPS = [
died in that gap would leave a pause waiting on his playback — so died in that gap would leave a pause waiting on his playback — so
it is the SHORTEST chip in the sheet, not a comfortable one. */ it is the SHORTEST chip in the sheet, not a comfortable one. */
{ do: `document.querySelectorAll('.nav button')[0].click()`, name: "home-feed", wait: 1200 }, { do: `document.querySelectorAll('.nav button')[0].click()`, name: "home-feed", wait: 1200 },
{ do: `document.querySelectorAll('.gbar .iconbtn')[1].click()`, name: "home-more", wait: 900 }, { do: `document.querySelector('.phead .iconbtn.muted').click()`, name: "home-more", wait: 900 },
{ do: `__click('.halfsheet .row', 'Sleep timer')`, name: "sleep-sheet", wait: 800 }, { do: `__click('.halfsheet .row', 'Sleep timer')`, name: "sleep-sheet", wait: 800 },
{ do: `__click('.halfsheet .chip', '15 min')`, name: "sleep-set", wait: 800 }, { do: `__click('.halfsheet .chip', '15 min')`, name: "sleep-set", wait: 800 },
{ do: `document.querySelectorAll('.gbar .iconbtn')[1].click()`, name: "sleep-armed", wait: 900 }, { do: `document.querySelector('.phead .iconbtn.muted').click()`, name: "sleep-armed", wait: 900 },
{ do: `(function () { function pick(sel, re) { return [].slice.call(document.querySelectorAll(sel)).find(function (x) { return re.test((x.textContent || '').trim()); }); } var r = pick('.halfsheet .row', /Sleep timer/); if (r) r.click(); return new Promise(function (res) { setTimeout(function () { var c = pick('.halfsheet .chip', /^Off$/); if (c) c.click(); res(!!c); }, 400); }); })()`, name: "sleep-off", wait: 900 }, { do: `(function () { function pick(sel, re) { return [].slice.call(document.querySelectorAll(sel)).find(function (x) { return re.test((x.textContent || '').trim()); }); } var r = pick('.halfsheet .row', /Sleep timer/); if (r) r.click(); return new Promise(function (res) { setTimeout(function () { var c = pick('.halfsheet .chip', /^Off$/); if (c) c.click(); res(!!c); }, 400); }); })()`, name: "sleep-off", wait: 900 },
{ do: `document.querySelectorAll('.gbar .iconbtn')[1].click()`, name: "sleep-cleared", wait: 900 }, { do: `document.querySelector('.phead .iconbtn.muted').click()`, name: "sleep-cleared", wait: 900 },
{ do: `(function () { var s = document.querySelector('.scrim'); if (s) s.click(); return true; })()`, wait: 600 }, { do: `(function () { var s = document.querySelector('.scrim'); if (s) s.click(); return true; })()`, wait: 600 },
/* You: the stream cache's retention chip, read first and put back /* You: the stream cache's retention chip, read first and put back
last. The value is stashed IN THE PAGE as well as in the harness, last. The value is stashed IN THE PAGE as well as in the harness,