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. */}
|
{/* The tab bar is fixed chrome; the tab content scrolls under it. */}
|
||||||
<PageToolbar>
|
<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} />
|
<Tabs tabs={tabs} active={active} onChange={setTab} />
|
||||||
</div>
|
</div>
|
||||||
</PageToolbar>
|
</PageToolbar>
|
||||||
|
|||||||
@@ -200,7 +200,7 @@ export default function ChannelDiscovery({
|
|||||||
<>
|
<>
|
||||||
{/* Intro + the table controls are fixed chrome; only the table rows scroll (sticky header). */}
|
{/* Intro + the table controls are fixed chrome; only the table rows scroll (sticky header). */}
|
||||||
<PageToolbar>
|
<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>
|
<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). */}
|
{/* Cards has no column headers, so a dropdown drives the sort + a pager (table uses its own). */}
|
||||||
{layout === "cards" && (
|
{layout === "cards" && (
|
||||||
|
|||||||
@@ -135,7 +135,7 @@ export default function DownloadCenter() {
|
|||||||
<>
|
<>
|
||||||
{/* Page title + subtitle + tabs are fixed chrome; the tab content scrolls under them. */}
|
{/* Page title + subtitle + tabs are fixed chrome; the tab content scrolls under them. */}
|
||||||
<PageToolbar>
|
<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">
|
<div className="flex items-center justify-between gap-3 mb-1">
|
||||||
<h1 className="text-xl font-semibold">{t("downloads.page.title")}</h1>
|
<h1 className="text-xl font-semibold">{t("downloads.page.title")}</h1>
|
||||||
<button
|
<button
|
||||||
|
|||||||
@@ -568,7 +568,7 @@ export default function Feed({
|
|||||||
}));
|
}));
|
||||||
|
|
||||||
const toolbar = (
|
const toolbar = (
|
||||||
<div className="pb-3">
|
<div className="pb-1.5">
|
||||||
<div className="flex items-center gap-1.5 gap-y-2 flex-wrap">
|
<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
|
{/* Watch state is single-select (always exactly one); content types are an independent
|
||||||
multi-select. Both compress to one labeled dropdown pill via FilterSelect. */}
|
multi-select. Both compress to one labeled dropdown pill via FilterSelect. */}
|
||||||
@@ -673,7 +673,7 @@ export default function Feed({
|
|||||||
);
|
);
|
||||||
|
|
||||||
return (
|
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
|
{/* 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
|
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
|
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
|
baseline (channel-scoped, whole-catalog, show-all), so each would read as a "filter you
|
||||||
applied" when it's just the page being itself. */}
|
applied" when it's just the page being itself. */}
|
||||||
<PageToolbar>
|
<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}
|
{toolbar}
|
||||||
{!channelScoped && (
|
{!channelScoped && (
|
||||||
<ActiveFilterChips
|
<ActiveFilterChips
|
||||||
|
|||||||
@@ -123,7 +123,7 @@ export default function NotificationsPanel() {
|
|||||||
<>
|
<>
|
||||||
{/* The inbox header (title + mark-all / clear-all) is fixed chrome; only the list scrolls. */}
|
{/* The inbox header (title + mark-all / clear-all) is fixed chrome; only the list scrolls. */}
|
||||||
<PageToolbar>
|
<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">
|
<div className="glass rounded-2xl p-4 flex items-center gap-3">
|
||||||
<Bell className="w-5 h-5 text-accent shrink-0" />
|
<Bell className="w-5 h-5 text-accent shrink-0" />
|
||||||
<div className="flex-1 min-w-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, "offsetTop", { value: 12, configurable: true });
|
||||||
Object.defineProperty(hdr, "offsetHeight", { value: 96, configurable: true });
|
Object.defineProperty(hdr, "offsetHeight", { value: 96, configurable: true });
|
||||||
roCb?.();
|
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", () => {
|
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
|
// 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
|
// 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,
|
// 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 hdr = () => <div data-testid="hdr">H</div>;
|
||||||
const { container, rerender } = render(<PageShell header={hdr()}>content</PageShell>);
|
const { container, rerender } = render(<PageShell header={hdr()}>content</PageShell>);
|
||||||
const measure = (top: number, h: number) => {
|
const measure = (top: number, h: number) => {
|
||||||
@@ -66,11 +66,11 @@ describe("PageShell --hdr-h (E5 S7)", () => {
|
|||||||
};
|
};
|
||||||
measure(12, 52);
|
measure(12, 52);
|
||||||
const content = () => container.querySelector('[class*="pt-(--hdr-h)"]') as HTMLElement;
|
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>content</PageShell>); // → channel page: header gone
|
||||||
rerender(<PageShell header={hdr()}>content</PageShell>); // → Back: header returns as a NEW node
|
rerender(<PageShell header={hdr()}>content</PageShell>); // → Back: header returns as a NEW node
|
||||||
measure(12, 52); // fires the NEW header's observer
|
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
|
// 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
|
// 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
|
// 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
|
// 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 8px → the content toolbar slides up under the header, stuck until reload. Keying
|
// 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.
|
// 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 [headerEl, setHeaderEl] = useState<HTMLDivElement | null>(null);
|
||||||
const [contentEl, setContentEl] = useState<HTMLDivElement | null>(null);
|
const [contentEl, setContentEl] = useState<HTMLDivElement | null>(null);
|
||||||
@@ -82,9 +82,9 @@ export default function PageShell({
|
|||||||
contentEl?.style.removeProperty("--hdr-h");
|
contentEl?.style.removeProperty("--hdr-h");
|
||||||
return;
|
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 = () =>
|
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();
|
apply();
|
||||||
const ro = new ResizeObserver(apply);
|
const ro = new ResizeObserver(apply);
|
||||||
ro.observe(headerEl);
|
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
|
{/* 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-3 pb-2">
|
<div className="px-4 pt-2 pb-2">
|
||||||
<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 && (
|
||||||
|
|||||||
@@ -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
|
{/* The title count is fixed chrome — it portals into the shell's fixed band so it stays put
|
||||||
while only the poster grid scrolls. */}
|
while only the poster grid scrolls. */}
|
||||||
<PageToolbar>
|
<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}
|
{viewToggle}
|
||||||
<p className="text-xs text-muted">
|
<p className="text-xs text-muted">
|
||||||
{browseQ.isLoading
|
{browseQ.isLoading
|
||||||
|
|||||||
Reference in New Issue
Block a user