style(layout): tighten the oversized vertical gaps around the page toolbars
The floating-header → toolbar → filter-chips → content stack carried ~2× the whitespace it needed. Halve it, app-wide where the chrome is shared: - PageShell: the --hdr-h gap below the floating header 8px → 4px (every page's header). - Feed: toolbar wrapper pt-4 → pt-2, the toolbar's own pb-3 → pb-1.5, and the grid's top pt-2 → pt-1. Measured on the feed: header→toolbar 24→12px, toolbar→chips 12→6px, chips→cards 12→8px. - Plex / Playlists / ChannelDiscovery / DownloadCenter / Notifications / AdminUsers: toolbar wrapper pt-3 → pt-2, so every page's top band matches the tighter feed. PageShell.dom.test updated for the 4px gap.
This commit is contained in:
@@ -34,7 +34,7 @@ export default function AdminUsers() {
|
||||
<>
|
||||
{/* The tab bar is fixed chrome; the tab content scrolls under it. */}
|
||||
<PageToolbar>
|
||||
<div className="px-4 pt-3 max-w-3xl w-full mx-auto">
|
||||
<div className="px-4 pt-2 max-w-3xl w-full mx-auto">
|
||||
<Tabs tabs={tabs} active={active} onChange={setTab} />
|
||||
</div>
|
||||
</PageToolbar>
|
||||
|
||||
@@ -200,7 +200,7 @@ export default function ChannelDiscovery({
|
||||
<>
|
||||
{/* Intro + the table controls are fixed chrome; only the table rows scroll (sticky header). */}
|
||||
<PageToolbar>
|
||||
<div className="px-4 pt-3 max-w-384 mx-auto flex items-start justify-between gap-4">
|
||||
<div className="px-4 pt-2 max-w-384 mx-auto flex items-start justify-between gap-4">
|
||||
<p className="text-xs text-muted leading-relaxed">{t("channels.discovery.intro")}</p>
|
||||
{/* Cards has no column headers, so a dropdown drives the sort + a pager (table uses its own). */}
|
||||
{layout === "cards" && (
|
||||
|
||||
@@ -135,7 +135,7 @@ export default function DownloadCenter() {
|
||||
<>
|
||||
{/* Page title + subtitle + tabs are fixed chrome; the tab content scrolls under them. */}
|
||||
<PageToolbar>
|
||||
<div className="px-4 pt-3 max-w-4xl w-full mx-auto">
|
||||
<div className="px-4 pt-2 max-w-4xl w-full mx-auto">
|
||||
<div className="flex items-center justify-between gap-3 mb-1">
|
||||
<h1 className="text-xl font-semibold">{t("downloads.page.title")}</h1>
|
||||
<button
|
||||
|
||||
@@ -568,7 +568,7 @@ export default function Feed({
|
||||
}));
|
||||
|
||||
const toolbar = (
|
||||
<div className="pb-3">
|
||||
<div className="pb-1.5">
|
||||
<div className="flex items-center gap-1.5 gap-y-2 flex-wrap">
|
||||
{/* Watch state is single-select (always exactly one); content types are an independent
|
||||
multi-select. Both compress to one labeled dropdown pill via FilterSelect. */}
|
||||
@@ -673,7 +673,7 @@ export default function Feed({
|
||||
);
|
||||
|
||||
return (
|
||||
<div className={channelScoped ? "px-4 sm:px-6 pb-4" : "px-4 pb-4 pt-2"}>
|
||||
<div className={channelScoped ? "px-4 sm:px-6 pb-4" : "px-4 pb-4 pt-1"}>
|
||||
{/* The toolbar (+ active-filter chips) is FIXED chrome: it portals into a PageToolbarSlot so
|
||||
it stays put while the cards scroll (bug 4). On the feed page that slot is the shell's top
|
||||
band; on a channel page ChannelPage overrides the slot with its own sticky sub-header, so
|
||||
@@ -681,7 +681,7 @@ export default function Feed({
|
||||
baseline (channel-scoped, whole-catalog, show-all), so each would read as a "filter you
|
||||
applied" when it's just the page being itself. */}
|
||||
<PageToolbar>
|
||||
<div className={channelScoped ? "px-4 sm:px-6" : "px-4 pt-4 pb-1"}>
|
||||
<div className={channelScoped ? "px-4 sm:px-6" : "px-4 pt-2 pb-1"}>
|
||||
{toolbar}
|
||||
{!channelScoped && (
|
||||
<ActiveFilterChips
|
||||
|
||||
@@ -123,7 +123,7 @@ export default function NotificationsPanel() {
|
||||
<>
|
||||
{/* The inbox header (title + mark-all / clear-all) is fixed chrome; only the list scrolls. */}
|
||||
<PageToolbar>
|
||||
<div className="px-4 pt-3 max-w-3xl w-full mx-auto">
|
||||
<div className="px-4 pt-2 max-w-3xl w-full mx-auto">
|
||||
<div className="glass rounded-2xl p-4 flex items-center gap-3">
|
||||
<Bell className="w-5 h-5 text-accent shrink-0" />
|
||||
<div className="flex-1 min-w-0">
|
||||
|
||||
@@ -43,7 +43,7 @@ describe("PageShell --hdr-h (E5 S7)", () => {
|
||||
Object.defineProperty(hdr, "offsetTop", { value: 12, configurable: true });
|
||||
Object.defineProperty(hdr, "offsetHeight", { value: 96, configurable: true });
|
||||
roCb?.();
|
||||
expect(content.style.getPropertyValue("--hdr-h")).toBe("116px"); // 12 + 96 + 8
|
||||
expect(content.style.getPropertyValue("--hdr-h")).toBe("112px"); // 12 + 96 + 4
|
||||
});
|
||||
|
||||
it("adds no `--hdr-h` pad when there is no header", () => {
|
||||
@@ -55,7 +55,7 @@ describe("PageShell --hdr-h (E5 S7)", () => {
|
||||
// PageShell is REUSED across feed↔channel navigation (same component position in App.tsx) — it
|
||||
// never remounts, so the header DOM node is swapped under a stable PageShell. The measurement
|
||||
// must re-run on that swap; a one-shot effect kept observing the FIRST node and, when it detached,
|
||||
// rewrote --hdr-h to a stale 8px that stuck on Back. Regression for the channel-Back header overlap.
|
||||
// rewrote --hdr-h to a stale 4px that stuck on Back. Regression for the channel-Back header overlap.
|
||||
const hdr = () => <div data-testid="hdr">H</div>;
|
||||
const { container, rerender } = render(<PageShell header={hdr()}>content</PageShell>);
|
||||
const measure = (top: number, h: number) => {
|
||||
@@ -66,11 +66,11 @@ describe("PageShell --hdr-h (E5 S7)", () => {
|
||||
};
|
||||
measure(12, 52);
|
||||
const content = () => container.querySelector('[class*="pt-(--hdr-h)"]') as HTMLElement;
|
||||
expect(content().style.getPropertyValue("--hdr-h")).toBe("72px"); // 12 + 52 + 8
|
||||
expect(content().style.getPropertyValue("--hdr-h")).toBe("68px"); // 12 + 52 + 4
|
||||
|
||||
rerender(<PageShell>content</PageShell>); // → channel page: header gone
|
||||
rerender(<PageShell header={hdr()}>content</PageShell>); // → Back: header returns as a NEW node
|
||||
measure(12, 52); // fires the NEW header's observer
|
||||
expect(content().style.getPropertyValue("--hdr-h")).toBe("72px"); // re-measured, not the stale 8px
|
||||
expect(content().style.getPropertyValue("--hdr-h")).toBe("68px"); // re-measured, not the stale 4px
|
||||
});
|
||||
});
|
||||
|
||||
@@ -71,8 +71,8 @@ export default function PageShell({
|
||||
// PageShell is reused — never remounted — across feed↔channel navigation (same component position in
|
||||
// App.tsx), so the header DOM node is swapped out from under a stable PageShell. A one-shot effect keeps
|
||||
// observing the FIRST header node; when that node detaches on channel-open, its ResizeObserver fires with
|
||||
// a 0 box and rewrites --hdr-h to `0+0+8`=8px on the (still-mounted) content div. On Back the pad class
|
||||
// returns with that stale 8px → the content toolbar slides up under the header, stuck until reload. Keying
|
||||
// a 0 box and rewrites --hdr-h to `0+0+4`=4px on the (still-mounted) content div. On Back the pad class
|
||||
// returns with that stale 4px → the content toolbar slides up under the header, stuck until reload. Keying
|
||||
// the measurement to the live node re-runs it every time the header mounts/unmounts, so it stays correct.
|
||||
const [headerEl, setHeaderEl] = useState<HTMLDivElement | null>(null);
|
||||
const [contentEl, setContentEl] = useState<HTMLDivElement | null>(null);
|
||||
@@ -82,9 +82,9 @@ export default function PageShell({
|
||||
contentEl?.style.removeProperty("--hdr-h");
|
||||
return;
|
||||
}
|
||||
// `+ 8` = the header's own gap-y-2 row gap (0.5rem), reused as the gap below it.
|
||||
// `+ 4` = a tight gap below the floating header before the content toolbar starts.
|
||||
const apply = () =>
|
||||
contentEl.style.setProperty("--hdr-h", `${headerEl.offsetTop + headerEl.offsetHeight + 8}px`);
|
||||
contentEl.style.setProperty("--hdr-h", `${headerEl.offsetTop + headerEl.offsetHeight + 4}px`);
|
||||
apply();
|
||||
const ro = new ResizeObserver(apply);
|
||||
ro.observe(headerEl);
|
||||
|
||||
@@ -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-3 pb-2">
|
||||
<div className="px-4 pt-2 pb-2">
|
||||
<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 && (
|
||||
|
||||
@@ -388,7 +388,7 @@ export default function PlexBrowse() {
|
||||
{/* The title count is fixed chrome — it portals into the shell's fixed band so it stays put
|
||||
while only the poster grid scrolls. */}
|
||||
<PageToolbar>
|
||||
<div className="px-4 pt-3 pb-1 max-w-[1600px] mx-auto flex items-center gap-2">
|
||||
<div className="px-4 pt-2 pb-1 max-w-[1600px] mx-auto flex items-center gap-2">
|
||||
{viewToggle}
|
||||
<p className="text-xs text-muted">
|
||||
{browseQ.isLoading
|
||||
|
||||
Reference in New Issue
Block a user