import { useEffect, useRef, useState, type ReactNode } from 'react'; /** * A row of page controls held under the nav bar while the page scrolls. * * Whatever sits above a page — its view tabs, its search, its action buttons — * used to leave with the scroll. Reaching the bottom of a long list put every * control over that list off the top of the window, and the only way back to * them was to scroll back through everything just read. The further in somebody * was, the further from the controls. * * The page still scrolls normally. Nothing here gets a scrollbar of its own — * the row simply stays where it is. Positioning and the canvas it paints live * in `.page-sticky`; this component exists for the one part CSS cannot answer, * which is whether the row is currently pinned. * * `PageHeader` wraps itself in this, so every page built from it gets the * behaviour without asking. Dispatch builds its own switcher rather than going * through `PageHeader`, so it wraps itself — which is the reason this is a * component and not four more lines inside the header. */ export function StickyRow({ children }: { children: ReactNode }) { /** * Pinned or not, so the hairline can be drawn only when it is. * * At rest the nav bar's own hairline sits just above this one, and two * parallel lines across the page with nothing between them read as a mistake. * The line is what separates the row from content passing UNDER it, so it * belongs to the pinned state rather than to the row. * * Watched with an observer rather than a scroll handler: a scroll listener * runs on every frame of every scroll to answer a question that changes * twice. The negative top margin puts the observer's boundary a pixel above * where the row comes to rest, so "no longer fully inside that box" means * exactly "stuck". */ const rowRef = useRef(null); const [isStuck, setIsStuck] = useState(false); useEffect(() => { const element = rowRef.current; /* Guarded for any renderer without it — the row still sticks, it just never grows its line, which is a missing hairline rather than a crash. */ if (!element || typeof IntersectionObserver === 'undefined') return; const observer = new IntersectionObserver( ([entry]) => setIsStuck(Boolean(entry && !entry.isIntersecting)), { rootMargin: '-57px 0px 0px 0px', threshold: 1 }, ); observer.observe(element); return () => observer.disconnect(); }, []); return (
{children}
); }