the sourcing notifier is a toast: the glass bar hands rstatus to the player-bar slot

The auto-sourcing resolver used to narrate from the right side of the
glass bar — a pulsing line while it searched, and an error that then sat
in the header indefinitely. It is now ResolverToast, in the exact slot,
pill and motion FollowToast and ImportToast already share: bottom-center
above the player bar, dark glass pill, toast-in/out, reduced-motion
respected, pointer-events none.

Three moments: while resolving, an accent dot pulses beside
"{label} — finding the best source…" for as long as the search runs; on
success the pill just leaves — the playbar coming alive IS the
confirmation, the same rule the bar version followed by disappearing; on
failure it swaps to the error in the bar's own red, holds 3.8s so it can
actually be read, then leaves. The label is copied locally so the success
exit never flashes an empty "— finding the best source…" while animating
out (the resolver clears its label in the same breath as active).

GlassBar no longer renders or imports any of it; the rstatus markup and
styles are gone with a pointer comment left in their place.

Verified by driving the resolver state through all four phases after a
clean reload: searching shows, success dismisses, a re-search re-shows,
an error holds then auto-dismisses — with the bar confirmed empty.
This commit is contained in:
KeelrLeek committed 2026-08-31 11:46:27 -05:00
1 parent 0670b8654a
commit 2d8c385a2e
3 files changed
+176 -24

No files matched your search

+2
View File
@@ -25,6 +25,7 @@
import Lyrics from "./views/Lyrics.svelte"; import Lyrics from "./views/Lyrics.svelte";
import FollowToast from "./components/FollowToast.svelte"; import FollowToast from "./components/FollowToast.svelte";
import ImportToast from "./components/ImportToast.svelte"; import ImportToast from "./components/ImportToast.svelte";
import ResolverToast from "./components/ResolverToast.svelte";
import SetlistScanChip from "./components/SetlistScanChip.svelte"; import SetlistScanChip from "./components/SetlistScanChip.svelte";
import { import {
ui, ui,
@@ -160,6 +161,7 @@
<PlayerBar /> <PlayerBar />
<FollowToast /> <FollowToast />
<ImportToast /> <ImportToast />
<ResolverToast />
<SetlistScanChip /> <SetlistScanChip />
<PreviewOverlay /> <PreviewOverlay />
<LyricsSyncEditor /> <LyricsSyncEditor />
+2 -24
View File
@@ -24,7 +24,6 @@
navBack, navBack,
navForward, navForward,
navToCrumb, navToCrumb,
resolver,
} from "../lib/state.svelte.js"; } from "../lib/state.svelte.js";
import { canGoBack, canGoForward, trail } from "../lib/navigation.svelte.js"; import { canGoBack, canGoForward, trail } from "../lib/navigation.svelte.js";
import { markAppScroll, appScrollIdleFor } from "../lib/scrollbar.js"; import { markAppScroll, appScrollIdleFor } from "../lib/scrollbar.js";
@@ -363,11 +362,6 @@
</nav> </nav>
{/if} {/if}
{@render children?.()} {@render children?.()}
{#if resolver.active}
<span class="rstatus"><span class="pulse">●</span> {resolver.label} — finding the best source…</span>
{:else if resolver.error}
<span class="rstatus error">{resolver.error}</span>
{/if}
</div> </div>
<style> <style>
@@ -616,24 +610,8 @@
flex: none; flex: none;
} }
/* Resolver status rides the right side of the bar. */ /* The resolver status that used to ride the right side of the bar is a
.rstatus { toast now — ResolverToast.svelte (Kyle, 2026-08-31). */
margin-left: auto;
max-width: 40%;
font-size: 12px;
color: rgba(255, 255, 255, 0.85);
text-shadow: 0 1px 6px rgba(0, 0, 0, 0.5);
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
flex: none;
}
.rstatus .pulse {
color: var(--accent);
}
.rstatus.error {
color: #ff8a94;
}
@media (max-width: 900px) { @media (max-width: 900px) {
.glassbar { .glassbar {
+172
View File
@@ -0,0 +1,172 @@
<script>
// The auto-sourcing notifier as a toast — moved out of the glass bar
// (Kyle, 2026-08-31): it now rides the same slot, pill and motion as
// FollowToast/ImportToast instead of sitting in the page header.
//
// Three moments, driven by the resolver state machine in state.svelte.js:
// searching resolver.active — pulsing dot + "{label} — finding the
// best source…", up for as long as the resolve runs.
// success active drops with no error: the pill just leaves — the
// playbar coming alive IS the confirmation, same rule the
// bar version followed by disappearing.
// failure active drops with resolver.error set: the pill turns to
// the error, holds long enough to read, then leaves.
//
// The label is copied locally so the success exit doesn't flash an empty
// "— finding the best source…" while the pill animates out (the resolver
// clears its label in the same breath as active).
import { untrack } from "svelte";
import { resolver } from "../lib/state.svelte.js";
const HOLD_ERROR_MS = 3800;
const LEAVE_MS = 280;
let shown = $state(false);
let leaving = $state(false);
let failed = $state(false);
let label = $state("");
let holdTimer = 0;
let leaveTimer = 0;
$effect(() => {
const active = resolver.active;
const error = resolver.error;
const liveLabel = resolver.label;
untrack(() => {
if (active) {
clearTimeout(holdTimer);
clearTimeout(leaveTimer);
label = liveLabel;
failed = false;
leaving = false;
shown = true;
return;
}
if (error) {
clearTimeout(holdTimer);
clearTimeout(leaveTimer);
failed = true;
leaving = false;
shown = true;
holdTimer = setTimeout(() => {
leaving = true;
leaveTimer = setTimeout(() => {
shown = false;
leaving = false;
failed = false;
}, LEAVE_MS);
}, HOLD_ERROR_MS);
return;
}
if (shown && !failed) {
leaving = true;
clearTimeout(leaveTimer);
leaveTimer = setTimeout(() => {
shown = false;
leaving = false;
}, LEAVE_MS);
}
});
});
$effect(() => () => {
clearTimeout(holdTimer);
clearTimeout(leaveTimer);
});
</script>
{#if shown}
<div class="layer" aria-live="polite">
<div class="toast" class:leaving>
{#if failed}
<span class="dot err">●</span>
<span class="text err">{resolver.error}</span>
{:else}
<span class="dot pulse">●</span>
<span class="text">{label} — finding the best source…</span>
{/if}
</div>
</div>
{/if}
<style>
/* Same slot as FollowToast/ImportToast: above the player bar, centred,
never intercepting clicks. One toast speaks at a time by convention. */
.layer {
position: fixed;
left: 0;
right: 0;
bottom: calc(var(--player-height, 62px) + 8px + 26px);
display: flex;
justify-content: center;
z-index: 80;
pointer-events: none;
}
.toast {
display: inline-flex;
align-items: center;
gap: 10px;
max-width: min(70vw, 560px);
padding: 9px 16px;
border-radius: 99px;
background: rgba(22, 23, 28, 0.94);
border: 1px solid rgba(255, 255, 255, 0.12);
backdrop-filter: blur(14px);
-webkit-backdrop-filter: blur(14px);
box-shadow: 0 14px 34px rgba(0, 0, 0, 0.55);
font-size: 12.5px;
font-weight: 700;
color: #fff;
animation: toast-in 340ms cubic-bezier(0.22, 1, 0.36, 1) both;
}
.toast.leaving {
animation: toast-out 260ms ease both;
}
@keyframes toast-in {
from {
opacity: 0;
transform: translateY(16px) scale(0.95);
}
to {
opacity: 1;
transform: none;
}
}
@keyframes toast-out {
to {
opacity: 0;
transform: translateY(12px) scale(0.97);
}
}
.text {
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.dot {
flex: none;
color: var(--accent);
}
.dot.pulse {
animation: rt-pulse 1.1s ease-in-out infinite;
}
@keyframes rt-pulse {
50% {
opacity: 0.35;
}
}
/* The bar version's error red, carried over. */
.dot.err,
.text.err {
color: #ff8a94;
}
@media (prefers-reduced-motion: reduce) {
.toast,
.toast.leaving {
animation-duration: 1ms;
}
.dot.pulse {
animation: none;
}
}
</style>