polish(channel): tighten toolbar-to-grid gap + restore top scroll fade

Two UAT nits on the channel page:
- the chip-row-to-cards gap was ~36px (toolbar pb-3 + wrapper pb-2 + grid
  pt-3 stacking); drop the wrapper/grid pads so the toolbar's own pb-3 is
  the only gap (~15px).
- restore the page scroller's TOP edge fade. It was dropped when the chrome
  was a sticky sub-header (the fade bit into it); now the chrome is a fixed
  band ABOVE the scroller, so the top fade correctly hints at cards
  scrolling up under the toolbar. Reverts the useScrollFade edges option.

Suite 15/15, tsc + knip clean.
This commit is contained in:
2026-07-18 05:01:31 +02:00
parent ec807c8b31
commit 02bd0d433b
3 changed files with 7 additions and 16 deletions
+2 -2
View File
@@ -655,7 +655,7 @@ export default function Feed({
);
return (
<div className={channelScoped ? "px-4 sm:px-6 pb-4 pt-3" : "px-4 pb-4 pt-2"}>
<div className={channelScoped ? "px-4 sm:px-6 pb-4" : "px-4 pb-4 pt-2"}>
{/* 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
@@ -663,7 +663,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 pb-2" : "px-4 pt-4 pb-1"}>
<div className={channelScoped ? "px-4 sm:px-6" : "px-4 pt-4 pb-1"}>
{toolbar}
{!channelScoped && (
<ActiveFilterChips
+1 -3
View File
@@ -67,9 +67,7 @@ export default function PageScroller({
children: ReactNode;
scrollKey?: string;
}) {
// Only the bottom edge fades: the top is bounded by the shell's fixed chrome (header / toolbar
// band) or a module's sticky sub-header, so a top fade would just eat into that chrome.
const fade = useScrollFade(20, { top: false });
const fade = useScrollFade();
const fadeRef = fade.ref;
const elRef = useRef<HTMLElement | null>(null);
// Stable identity: an inline ref callback changes every render, which makes React detach and
+4 -11
View File
@@ -7,17 +7,10 @@ import { useEffect, useState, type CSSProperties } from "react";
// to attach to the scroller and the mask `style` to spread onto it. Uses a node-state callback
// ref (not useRef) so the effect (re)runs whenever the element attaches — e.g. a SidePanel that
// mounts collapsed and only renders its scroll body once expanded still wires up correctly.
// `edges` gates which ends fade. The page scroller passes `{ top: false }`: its top is delimited by
// the fixed/sticky module chrome (header, toolbar band, a sticky sub-header), so a top fade there
// would only bite into that chrome instead of hinting at more content.
export function useScrollFade(
fadePx = 20,
edges: { top?: boolean; bottom?: boolean } = {},
): {
export function useScrollFade(fadePx = 20): {
ref: (node: HTMLElement | null) => void;
style: CSSProperties;
} {
const { top: fadeTop = true, bottom: fadeBottom = true } = edges;
const [el, setEl] = useState<HTMLElement | null>(null);
const [fade, setFade] = useState({ up: false, down: false });
@@ -60,8 +53,8 @@ export function useScrollFade(
};
}, [el]);
const topPx = fadeTop && fade.up ? fadePx : 0;
const bottomPx = fadeBottom && fade.down ? fadePx : 0;
const mask = `linear-gradient(to bottom, transparent, #000 ${topPx}px, #000 calc(100% - ${bottomPx}px), transparent)`;
const mask = `linear-gradient(to bottom, transparent, #000 ${fade.up ? fadePx : 0}px, #000 calc(100% - ${
fade.down ? fadePx : 0
}px), transparent)`;
return { ref: setEl, style: { maskImage: mask, WebkitMaskImage: mask } };
}