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 me = meQuery.data!;
|
||||||
const pageGate = moduleDef(page).gate;
|
const pageGate = moduleDef(page).gate;
|
||||||
const activePage: Page = !pageGate || pageGate(me) ? page : "feed";
|
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];
|
const railDef = channelView ? undefined : PAGE_RAIL[activePage];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -379,6 +379,7 @@ export default function App() {
|
|||||||
) : (
|
) : (
|
||||||
<PageShell
|
<PageShell
|
||||||
scrollKey={normalScrollKey}
|
scrollKey={normalScrollKey}
|
||||||
|
maxWidth={maxWidth}
|
||||||
// Channels: the registry disables the top fade for its sticky-header table, but the card
|
// 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.
|
// layout has no sticky header, so re-enable it there.
|
||||||
fadeTop={activePage === "channels" ? channelLayout === "cards" : (fadeTop ?? true)}
|
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.
|
// slim and/or no filter sidebar is present — no hard-coded offset.
|
||||||
<div
|
<div
|
||||||
ref={ref}
|
ref={ref}
|
||||||
// left-4 / right = 1rem + the scrollbar gutter: the header spans exactly the card grid's width
|
// left-4 / right = 1rem + the scrollbar gutter defines the full content column; max-w-[--content-mw]
|
||||||
// (left card's left edge to right card's right edge), matching the toolbar band below it.
|
// + mx-auto then centres the header on the SAME width the page's content uses, so on a max-width page
|
||||||
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"
|
// (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}
|
{search}
|
||||||
{/* Library scope (movie/show/both) sits right of the search box on Plex — relocated here from
|
{/* 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,
|
children,
|
||||||
scrollKey,
|
scrollKey,
|
||||||
fadeTop = true,
|
fadeTop = true,
|
||||||
|
maxWidth,
|
||||||
}: {
|
}: {
|
||||||
header?: ReactNode;
|
header?: ReactNode;
|
||||||
banners?: ReactNode;
|
banners?: ReactNode;
|
||||||
@@ -60,8 +61,20 @@ export default function PageShell({
|
|||||||
scrollKey?: string;
|
scrollKey?: string;
|
||||||
// Off when this page's scroll region starts with a sticky header — see PageScroller.
|
// Off when this page's scroll region starts with a sticky header — see PageScroller.
|
||||||
fadeTop?: boolean;
|
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);
|
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
|
// 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
|
// 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
|
// 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
|
{/* Header (title, actions, view controls) is fixed chrome — it portals into the shell's
|
||||||
fixed band, so only the video list scrolls. */}
|
fixed band, so only the video list scrolls. */}
|
||||||
<PageToolbar>
|
<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="flex gap-3 items-start mb-4">
|
||||||
<div className="w-24 h-13.5 rounded-lg bg-card overflow-hidden shrink-0">
|
<div className="w-24 h-13.5 rounded-lg bg-card overflow-hidden shrink-0">
|
||||||
{items[0]?.thumbnail_url && (
|
{items[0]?.thumbnail_url && (
|
||||||
@@ -757,7 +757,7 @@ export default function Playlists() {
|
|||||||
accessibility={{ screenReaderInstructions, announcements }}
|
accessibility={{ screenReaderInstructions, announcements }}
|
||||||
>
|
>
|
||||||
{/* Not clipped, so the header's filter popovers can overflow the table box. */}
|
{/* 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
|
{/* 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
|
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. */}
|
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
|
// 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.
|
// would sit on it and flip on with the first scroll, so they turn it off. Defaults to on.
|
||||||
fadeTop?: boolean;
|
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).
|
// page id → the component App renders in the content column (+ its shell chrome config).
|
||||||
export const PAGE_CONTENT: Record<Page, PageDef> = {
|
export const PAGE_CONTENT: Record<Page, PageDef> = {
|
||||||
feed: { Component: FeedPage },
|
feed: { Component: FeedPage },
|
||||||
channels: { Component: Channels, fadeTop: false },
|
channels: { Component: Channels, fadeTop: false, maxWidth: "96rem" },
|
||||||
playlists: { Component: Playlists },
|
playlists: { Component: Playlists, maxWidth: "64rem" },
|
||||||
plex: { Component: PlexBrowse },
|
plex: { Component: PlexBrowse, maxWidth: "1600px" },
|
||||||
notifications: { Component: NotificationsPanel },
|
notifications: { Component: NotificationsPanel, maxWidth: "48rem" },
|
||||||
messages: { Component: MessagesPage },
|
messages: { Component: MessagesPage },
|
||||||
downloads: { Component: DownloadCenter },
|
downloads: { Component: DownloadCenter, maxWidth: "56rem" },
|
||||||
stats: { Component: Stats },
|
stats: { Component: Stats },
|
||||||
scheduler: { Component: Scheduler },
|
scheduler: { Component: Scheduler },
|
||||||
config: { Component: ConfigPanel },
|
config: { Component: ConfigPanel },
|
||||||
users: { Component: AdminUsers },
|
users: { Component: AdminUsers, maxWidth: "48rem" },
|
||||||
audit: { Component: AuditLog, fadeTop: false },
|
audit: { Component: AuditLog, fadeTop: false },
|
||||||
settings: { Component: SettingsPanel },
|
settings: { Component: SettingsPanel },
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -104,6 +104,11 @@
|
|||||||
Defaults to 0 for overlay-scrollbar platforms where the gutter is empty. */
|
Defaults to 0 for overlay-scrollbar platforms where the gutter is empty. */
|
||||||
--sbw: 0px;
|
--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
|
/* ===== Glass tunables — every value that materially drives the look lives here so it can be
|
||||||
tuned in ONE place. ===== */
|
tuned in ONE place. ===== */
|
||||||
/* GLOBAL baseline = "Adaptive": solid-enough dark glass that reads well on content-less chrome
|
/* GLOBAL baseline = "Adaptive": solid-enough dark glass that reads well on content-less chrome
|
||||||
|
|||||||
Reference in New Issue
Block a user