diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index 8f625ea6..9ec6a00d 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -313,7 +313,7 @@ export default function App() { const me = meQuery.data!; const pageGate = moduleDef(page).gate; const activePage: Page = !pageGate || pageGate(me) ? page : "feed"; - const { Component: PageContent, fadeTop } = PAGE_CONTENT[activePage]; + const { Component: PageContent, fadeTop, maxWidth } = PAGE_CONTENT[activePage]; const railDef = channelView ? undefined : PAGE_RAIL[activePage]; return ( @@ -379,6 +379,7 @@ export default function App() { ) : ( {search} {/* Library scope (movie/show/both) sits right of the search box on Plex — relocated here from diff --git a/frontend/src/components/PageShell.tsx b/frontend/src/components/PageShell.tsx index ad7548ff..61236b79 100644 --- a/frontend/src/components/PageShell.tsx +++ b/frontend/src/components/PageShell.tsx @@ -52,6 +52,7 @@ export default function PageShell({ children, scrollKey, fadeTop = true, + maxWidth, }: { header?: ReactNode; banners?: ReactNode; @@ -60,8 +61,20 @@ export default function PageShell({ scrollKey?: string; // Off when this page's scroll region starts with a sticky header — see PageScroller. fadeTop?: boolean; + // The content column's max width (e.g. "1600px"); the header + toolbar centre on the same width via + // --content-mw. Omit for a full-width page (feed). See index.css --content-mw. + maxWidth?: string; }) { const [toolbarSlot, setToolbarSlot] = useState(null); + // Publish this page's content max-width so the floating header (and toolbar band) can centre on the + // same width the content column uses. Global (one page shell renders at a time). + useLayoutEffect(() => { + const root = document.documentElement; + root.style.setProperty("--content-mw", maxWidth ?? "none"); + return () => { + root.style.removeProperty("--content-mw"); + }; + }, [maxWidth]); // The floating header pads the content below it by `--hdr-h`. That must equal the header's REAL // height, which grows as the flex-wrap header wraps to 2–3 rows on a narrow window — a fixed pad // would let the content toolbar slide up under it (E5 S7). Measure the header and write its height diff --git a/frontend/src/components/Playlists.tsx b/frontend/src/components/Playlists.tsx index fc7a9a63..466d8c8a 100644 --- a/frontend/src/components/Playlists.tsx +++ b/frontend/src/components/Playlists.tsx @@ -571,7 +571,7 @@ export default function Playlists() { {/* Header (title, actions, view controls) is fixed chrome — it portals into the shell's fixed band, so only the video list scrolls. */} -
+
{items[0]?.thumbnail_url && ( @@ -757,7 +757,7 @@ export default function Playlists() { accessibility={{ screenReaderInstructions, announcements }} > {/* Not clipped, so the header's filter popovers can overflow the table box. */} -
+
{/* table-fixed, not auto: every column but the title has a declared width, so the title absorbs the rest and its `truncate` works. Auto layout sizes columns from min-content, and a nowrap title cell's min-content is the entire title. */} diff --git a/frontend/src/components/moduleRegistry.tsx b/frontend/src/components/moduleRegistry.tsx index 80eecfa1..a14a440d 100644 --- a/frontend/src/components/moduleRegistry.tsx +++ b/frontend/src/components/moduleRegistry.tsx @@ -75,21 +75,25 @@ export type PageDef = { // Channels + Audit render a table with a sticky header at the scroller top; the shell's top fade // would sit on it and flip on with the first scroll, so they turn it off. Defaults to on. fadeTop?: boolean; + // The content column's max width. The header + toolbar centre on this same width (via --content-mw) + // so the floating chrome lines up with the centred content instead of spanning the whole column. + // MUST match the max-w the page's own content wrapper uses. Omit for a full-width page (feed). + maxWidth?: string; }; // page id → the component App renders in the content column (+ its shell chrome config). export const PAGE_CONTENT: Record = { feed: { Component: FeedPage }, - channels: { Component: Channels, fadeTop: false }, - playlists: { Component: Playlists }, - plex: { Component: PlexBrowse }, - notifications: { Component: NotificationsPanel }, + channels: { Component: Channels, fadeTop: false, maxWidth: "96rem" }, + playlists: { Component: Playlists, maxWidth: "64rem" }, + plex: { Component: PlexBrowse, maxWidth: "1600px" }, + notifications: { Component: NotificationsPanel, maxWidth: "48rem" }, messages: { Component: MessagesPage }, - downloads: { Component: DownloadCenter }, + downloads: { Component: DownloadCenter, maxWidth: "56rem" }, stats: { Component: Stats }, scheduler: { Component: Scheduler }, config: { Component: ConfigPanel }, - users: { Component: AdminUsers }, + users: { Component: AdminUsers, maxWidth: "48rem" }, audit: { Component: AuditLog, fadeTop: false }, settings: { Component: SettingsPanel }, }; diff --git a/frontend/src/index.css b/frontend/src/index.css index c90dfd05..78ec681a 100644 --- a/frontend/src/index.css +++ b/frontend/src/index.css @@ -104,6 +104,11 @@ Defaults to 0 for overlay-scrollbar platforms where the gutter is empty. */ --sbw: 0px; + /* Max width of a page's content column (cards / table). The floating header + toolbar band read it + so they centre on the SAME max-width the content uses, instead of spanning the whole column while + a max-w page centres its grid. Each page sets it via PageShell's `maxWidth`; `none` = full width. */ + --content-mw: none; + /* ===== Glass tunables — every value that materially drives the look lives here so it can be tuned in ONE place. ===== */ /* GLOBAL baseline = "Adaptive": solid-enough dark glass that reads well on content-less chrome