refactor(shell): introduce PageShell for the normal-page chrome
Collapse App's hand-assembled normal-page chrome (floating Header + top-padded wrapper + PageScroller + banners) into a single PageShell contract with declared fixed vs scrollable regions. No behaviour change; the channel page still renders its own scroller (dissolved next). Groundwork for the S4 fixed-chrome/scroll-ownership seam.
This commit is contained in:
+30
-32
@@ -53,7 +53,7 @@ import NavSidebar from "./components/NavSidebar";
|
||||
import Sidebar from "./components/Sidebar";
|
||||
import PlaylistsRail from "./components/PlaylistsRail";
|
||||
import BackToTop from "./components/BackToTop";
|
||||
import PageScroller from "./components/PageScroller";
|
||||
import PageShell from "./components/PageShell";
|
||||
import GlassTuner from "./components/GlassTuner";
|
||||
import ChatDock from "./components/ChatDock";
|
||||
import Toaster from "./components/Toaster";
|
||||
@@ -715,34 +715,34 @@ export default function App() {
|
||||
/>
|
||||
</Suspense>
|
||||
) : (
|
||||
<>
|
||||
<Header
|
||||
me={meQuery.data!}
|
||||
filters={filters}
|
||||
setFilters={setFilters}
|
||||
plexQ={plexQ}
|
||||
setPlexQ={setPlexQ}
|
||||
channelsSearch={channelsSearch}
|
||||
setChannelsSearch={setChannelsSearch}
|
||||
playlistsSearch={playlistsSearch}
|
||||
setPlaylistsSearch={setPlaylistsSearch}
|
||||
onYtSearch={enterYtSearch}
|
||||
onGoToFullHistory={() => {
|
||||
setChannelFilter("needs_full");
|
||||
setChannelsView("subscribed"); // the status filter applies to the subscriptions tab
|
||||
setChannelsFilterReset((n) => n + 1); // drop any stale column filter hiding the rows
|
||||
setPage("channels");
|
||||
}}
|
||||
/>
|
||||
{/* The header floats (fixed) over the content, so it no longer occupies flow space; this
|
||||
wrapper clears it with a top pad. (All left rails, incl. Playlists, are App-level
|
||||
floating panels beside the nav — none sit inside this content column anymore.) */}
|
||||
<div
|
||||
className="flex-1 min-w-0 min-h-0 flex flex-col pt-[var(--hdr-h)]"
|
||||
>
|
||||
{meQuery.data!.is_demo && <DemoBanner />}
|
||||
<VersionBanner onOpen={() => openReleaseNotes(CURRENT_VERSION)} />
|
||||
<PageScroller>
|
||||
<PageShell
|
||||
header={
|
||||
<Header
|
||||
me={meQuery.data!}
|
||||
filters={filters}
|
||||
setFilters={setFilters}
|
||||
plexQ={plexQ}
|
||||
setPlexQ={setPlexQ}
|
||||
channelsSearch={channelsSearch}
|
||||
setChannelsSearch={setChannelsSearch}
|
||||
playlistsSearch={playlistsSearch}
|
||||
setPlaylistsSearch={setPlaylistsSearch}
|
||||
onYtSearch={enterYtSearch}
|
||||
onGoToFullHistory={() => {
|
||||
setChannelFilter("needs_full");
|
||||
setChannelsView("subscribed"); // the status filter applies to the subscriptions tab
|
||||
setChannelsFilterReset((n) => n + 1); // drop any stale column filter hiding the rows
|
||||
setPage("channels");
|
||||
}}
|
||||
/>
|
||||
}
|
||||
banners={
|
||||
<>
|
||||
{meQuery.data!.is_demo && <DemoBanner />}
|
||||
<VersionBanner onOpen={() => openReleaseNotes(CURRENT_VERSION)} />
|
||||
</>
|
||||
}
|
||||
>
|
||||
<Suspense fallback={pageFallback}>
|
||||
{page === "channels" ? (
|
||||
<Channels
|
||||
@@ -823,9 +823,7 @@ export default function App() {
|
||||
/>
|
||||
)}
|
||||
</Suspense>
|
||||
</PageScroller>
|
||||
</div>
|
||||
</>
|
||||
</PageShell>
|
||||
)}
|
||||
{/* Toasts rise from the bottom-left, by the notification bell in the nav rail.
|
||||
Anchored inside the content column so they clear the sidebar automatically
|
||||
|
||||
@@ -0,0 +1,42 @@
|
||||
import { type ReactNode } from "react";
|
||||
import PageScroller from "./PageScroller";
|
||||
|
||||
// The page shell: the fixed chrome around a single scrollable content region. It formalizes "what
|
||||
// is fixed vs what scrolls" per module, so the chrome App used to hand-assemble around every page
|
||||
// (a floating Header + a top-padded wrapper + PageScroller, plus the channel page's separate
|
||||
// branch that rendered its OWN scroller) collapses to one contract and one scroller.
|
||||
//
|
||||
// Regions, top to bottom:
|
||||
// - `header` — floats (absolute, height `var(--hdr-h)`) over the content column; the wrapper pads
|
||||
// its top by that height so nothing hides under it. Omit it (channel page) → no pad
|
||||
// and no fixed top band, so the module's own banner can scroll from the very top.
|
||||
// - `banners` — in-flow, fixed notices (demo/version) above the scroll region.
|
||||
// - `toolbar` — in-flow, fixed per-module band (filter chips / sort / counts) that must NOT scroll
|
||||
// with the content.
|
||||
// - children — the ONE scrollable region. `PageScroller` owns the scroller and its two
|
||||
// invariants (`scrollbar-gutter: stable`, edge fade); see that file.
|
||||
//
|
||||
// The shell renders a fragment so it drops straight into App's `relative` content column — the
|
||||
// floating header keeps resolving its `absolute` position against that same ancestor.
|
||||
export default function PageShell({
|
||||
header,
|
||||
banners,
|
||||
toolbar,
|
||||
children,
|
||||
}: {
|
||||
header?: ReactNode;
|
||||
banners?: ReactNode;
|
||||
toolbar?: ReactNode;
|
||||
children: ReactNode;
|
||||
}) {
|
||||
return (
|
||||
<>
|
||||
{header}
|
||||
<div className={`flex-1 min-w-0 min-h-0 flex flex-col ${header ? "pt-[var(--hdr-h)]" : ""}`}>
|
||||
{banners}
|
||||
{toolbar}
|
||||
<PageScroller>{children}</PageScroller>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user