From dc6e62e8228a52595bf842e5cf420f9f6a0d12de Mon Sep 17 00:00:00 2001 From: npeter83 Date: Sat, 18 Jul 2026 03:15:41 +0200 Subject: [PATCH] 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. --- frontend/src/App.tsx | 62 +++++++++++++-------------- frontend/src/components/PageShell.tsx | 42 ++++++++++++++++++ 2 files changed, 72 insertions(+), 32 deletions(-) create mode 100644 frontend/src/components/PageShell.tsx diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index 4c649dc..d6f1405 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -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() { /> ) : ( - <> -
{ - 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.) */} -
- {meQuery.data!.is_demo && } - openReleaseNotes(CURRENT_VERSION)} /> - + { + 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 && } + openReleaseNotes(CURRENT_VERSION)} /> + + } + > {page === "channels" ? ( )} - -
- + )} {/* 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 diff --git a/frontend/src/components/PageShell.tsx b/frontend/src/components/PageShell.tsx new file mode 100644 index 0000000..f496617 --- /dev/null +++ b/frontend/src/components/PageShell.tsx @@ -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} +
+ {banners} + {toolbar} + {children} +
+ + ); +}