feat(layout): centre the header on each page's content width, centre the playlist table
Every page's floating header (search bar + sync chip) and the toolbar band now centre on the SAME max-width the content column uses, instead of spanning the whole column while a max-w page centres its grid. So the search bar lines up with the left edge of the cards/table on every page, and sits centred with the content — not against the full page width. - PageShell takes a `maxWidth`, published as --content-mw; the header adds max-w-[--content-mw] mx-auto inside its left-4 / right-(1rem+scrollbar) span, so it centres on that width (`none` = full width). - moduleRegistry PAGE_CONTENT carries each page's content max width (plex 1600px, channels 96rem, playlists 64rem, downloads 56rem, notifications/users 48rem; feed stays full width) — one source. - Playlists: its content (header, filter row, table) was left-aligned max-w-5xl → now mx-auto, so the table is centred and the header lines up with it. Verified at 1920px: header left edge == content left edge — feed 340, channels 213, playlists 487.
This commit is contained in:
@@ -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() {
|
||||
) : (
|
||||
<PageShell
|
||||
scrollKey={normalScrollKey}
|
||||
maxWidth={maxWidth}
|
||||
// Channels: the registry disables the top fade for its sticky-header table, but the card
|
||||
// layout has no sticky header, so re-enable it there.
|
||||
fadeTop={activePage === "channels" ? channelLayout === "cards" : (fadeTop ?? true)}
|
||||
|
||||
@@ -89,9 +89,11 @@ const Header = forwardRef<
|
||||
// slim and/or no filter sidebar is present — no hard-coded offset.
|
||||
<div
|
||||
ref={ref}
|
||||
// left-4 / right = 1rem + the scrollbar gutter: the header spans exactly the card grid's width
|
||||
// (left card's left edge to right card's right edge), matching the toolbar band below it.
|
||||
className="pointer-events-none absolute top-3 left-4 right-[calc(1rem+var(--sbw))] z-chrome flex flex-wrap items-center gap-x-3 gap-y-2"
|
||||
// left-4 / right = 1rem + the scrollbar gutter defines the full content column; max-w-[--content-mw]
|
||||
// + mx-auto then centres the header on the SAME width the page's content uses, so on a max-width page
|
||||
// (Plex/Channels/Playlists) the header lines up with the centred grid, not the whole column. `none`
|
||||
// (feed) = full width. Matches the toolbar band below it.
|
||||
className="pointer-events-none absolute top-3 left-4 right-[calc(1rem+var(--sbw))] mx-auto max-w-[var(--content-mw,none)] z-chrome flex flex-wrap items-center gap-x-3 gap-y-2"
|
||||
>
|
||||
{search}
|
||||
{/* Library scope (movie/show/both) sits right of the search box on Plex — relocated here from
|
||||
|
||||
@@ -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<HTMLDivElement | null>(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
|
||||
|
||||
@@ -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. */}
|
||||
<PageToolbar>
|
||||
<div className="px-4 pt-2 pb-2">
|
||||
<div className="px-4 pt-2 pb-2 max-w-5xl mx-auto">
|
||||
<div className="flex gap-3 items-start mb-4">
|
||||
<div className="w-24 h-13.5 rounded-lg bg-card overflow-hidden shrink-0">
|
||||
{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. */}
|
||||
<div className="max-w-5xl">
|
||||
<div className="max-w-5xl mx-auto">
|
||||
{/* 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. */}
|
||||
|
||||
@@ -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<Page, PageDef> = {
|
||||
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 },
|
||||
};
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user