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 ( 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 {/* 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
@@ -663,7 +663,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 pb-2" : "px-4 pt-4 pb-1"}> <div className={channelScoped ? "px-4 sm:px-6" : "px-4 pt-4 pb-1"}>
{toolbar} {toolbar}
{!channelScoped && ( {!channelScoped && (
<ActiveFilterChips <ActiveFilterChips
+1 -3
View File
@@ -67,9 +67,7 @@ export default function PageScroller({
children: ReactNode; children: ReactNode;
scrollKey?: string; scrollKey?: string;
}) { }) {
// Only the bottom edge fades: the top is bounded by the shell's fixed chrome (header / toolbar const fade = useScrollFade();
// 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 fadeRef = fade.ref; const fadeRef = fade.ref;
const elRef = useRef<HTMLElement | null>(null); const elRef = useRef<HTMLElement | null>(null);
// Stable identity: an inline ref callback changes every render, which makes React detach and // 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 // 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 // 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. // 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 export function useScrollFade(fadePx = 20): {
// 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 } = {},
): {
ref: (node: HTMLElement | null) => void; ref: (node: HTMLElement | null) => void;
style: CSSProperties; style: CSSProperties;
} { } {
const { top: fadeTop = true, bottom: fadeBottom = true } = edges;
const [el, setEl] = useState<HTMLElement | null>(null); const [el, setEl] = useState<HTMLElement | null>(null);
const [fade, setFade] = useState({ up: false, down: false }); const [fade, setFade] = useState({ up: false, down: false });
@@ -60,8 +53,8 @@ export function useScrollFade(
}; };
}, [el]); }, [el]);
const topPx = fadeTop && fade.up ? fadePx : 0; const mask = `linear-gradient(to bottom, transparent, #000 ${fade.up ? fadePx : 0}px, #000 calc(100% - ${
const bottomPx = fadeBottom && fade.down ? fadePx : 0; fade.down ? fadePx : 0
const mask = `linear-gradient(to bottom, transparent, #000 ${topPx}px, #000 calc(100% - ${bottomPx}px), transparent)`; }px), transparent)`;
return { ref: setEl, style: { maskImage: mask, WebkitMaskImage: mask } }; return { ref: setEl, style: { maskImage: mask, WebkitMaskImage: mask } };
} }