Files
SoulStream/src/App.svelte
T
KeelrLeek 56036216f8 Phase 2C: fix reel centering via window-relative card positioning
The transitioned track translateY collapsed to 0 in Chromium (percent-
translated zero-height track), leaving the active cover one stride low
per step. The track is now a fixed anchor at the reel centre; each card
positions itself at (index - activeIndex) * stride and animates its own
small `top`, so no coordinate ever exceeds +/-RENDER_RADIUS * stride.

Verified with the transition ON: centre offset 0 at every index in the
12- and 500-item fixtures, 5 cards in the DOM at scale, wheel gates,
dialog semantics, Tab containment, focus restore, and polite announce
all pass. Dev window.__preview*/__ui handles removed from App.svelte.
2026-07-22 19:38:10 -05:00

132 lines
4.8 KiB
Svelte

<script>
import Sidebar from "./components/Sidebar.svelte";
import TitleBar from "./components/TitleBar.svelte";
import PlayerBar from "./components/PlayerBar.svelte";
import NowPlaying from "./components/NowPlaying.svelte";
import ArtworkViewer from "./components/ArtworkViewer.svelte";
import LoginModal from "./components/LoginModal.svelte";
import PreviewOverlay from "./components/PreviewOverlay.svelte";
import { previewUi } from "./lib/preview/previewSession.svelte.js";
import Home from "./views/Home.svelte";
import Search from "./views/Search.svelte";
import Library from "./views/Library.svelte";
import Downloads from "./views/Downloads.svelte";
import Settings from "./views/Settings.svelte";
import Playlist from "./views/Playlist.svelte";
import CatalogSearch from "./views/CatalogSearch.svelte";
import CatalogArtist from "./views/CatalogArtist.svelte";
import CatalogAlbum from "./views/CatalogAlbum.svelte";
import {
ui,
initEvents,
autoConnect,
refreshLibrary,
refreshPlaylists,
} from "./lib/state.svelte.js";
import { floatScroll } from "./lib/scrollbar.js";
const views = {
home: Home,
search: Search,
library: Library,
downloads: Downloads,
settings: Settings,
playlist: Playlist,
catalog: CatalogSearch,
catalogArtist: CatalogArtist,
catalogAlbum: CatalogAlbum,
};
const CurrentView = $derived(views[ui.view] ?? Home);
initEvents();
autoConnect();
refreshLibrary();
refreshPlaylists();
</script>
<div
class="app"
class:np-open={ui.nowPlaying}
class:sidebar-collapsed={ui.sidebarCollapsed}
class:preview-open={previewUi.open}
>
<TitleBar />
<!-- Previews take over the whole window (decision 2026-07-22): while the
modal is open the page and the player bar are inert/not rendered —
there is deliberately NO UI path to command main audio behind the
focus coordinator's back. The titlebar stays live for window chrome. -->
<Sidebar />
<main class="content" use:floatScroll inert={previewUi.open || undefined}>
<CurrentView />
</main>
<NowPlaying />
{#if !previewUi.open}
<PlayerBar />
{/if}
<PreviewOverlay />
<ArtworkViewer />
<LoginModal />
</div>
<style>
/* Frameless window: the titlebar row is part of the app background —
no separate chrome color, the panels float beneath it. */
/* While the preview modal is open: keyboard is contained by the dialog's
focus trap and aria-modal, the content pane is `inert`, and the flanking
panels stop taking pointer input here. (The player bar is simply not
rendered — see the markup note above.) */
.app.preview-open :global(.sidebar),
.app.preview-open :global(.nowplaying) {
pointer-events: none;
}
.app {
display: grid;
grid-template-columns: var(--sidebar-width) 1fr var(--nowplaying-width);
grid-template-rows: 30px 1fr var(--player-height);
grid-template-areas:
"titlebar titlebar titlebar"
"sidebar content nowplaying"
"player player player";
height: 100%;
gap: 8px;
padding: 0 8px 8px;
background: var(--bg-sidebar);
overflow: hidden;
transition: grid-template-columns 0.28s cubic-bezier(0.4, 0, 0.2, 1);
}
/* Collapsed left nav: narrow the sidebar column to an icon rail. The
grid-template-columns transition animates the reflow. */
.app.sidebar-collapsed {
--sidebar-width: 64px;
}
/* Now Playing panel slides away: the column collapses to zero (the panel
itself keeps its width and slides off-screen — see NowPlaying.svelte). */
.app:not(.np-open) {
--nowplaying-width: 0px;
}
.content {
grid-area: content;
background: linear-gradient(var(--bg-highlight) 0, var(--bg-base) 240px);
/* The highlight wash belongs to the top of the PAGE, not the viewport:
anchored to the pane (default `scroll`), the gradient stays put
while content scrolls, so the hero's scrim — which dissolves into
pure bg-base — ends on a lighter backdrop mid-gradient and paints a
hard seam at the hero's bottom edge. `local` scrolls the gradient
away with the content, keeping the seam colors identical at every
scroll position. */
background-attachment: local;
border-radius: var(--radius);
overflow-y: auto;
overflow-anchor: none;
/* Main scroll pane: no elastic rubber-band at its edges. Inner regions
deliberately do NOT set this, so their wheel chains here. */
overscroll-behavior: none;
padding: 24px 28px;
}
</style>