From 2f3098cac4eb8b85e8b8bf12c5b82acc586147ce Mon Sep 17 00:00:00 2001 From: Jurie Smit Date: Mon, 22 Jun 2026 23:12:02 +0200 Subject: [PATCH] fix(web): hide scroll hint via IntersectionObserver + icons for new projects MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The "Scroll to Explore" hint was driven by window.scrollY, which on this layout left it visible far down the page (only disappearing near the last section). Drive its visibility from an IntersectionObserver on a 1px sentinel at the top of the hero instead — it now hides the moment you scroll and returns at the top, regardless of how the page scrolls. Also give the newly-added portfolio projects detail-page icons (they were falling back to the generic Layers icon): Sluice→Waypoints, Docket→Receipt, ConvoLens→MessagesSquare, OmniPost→Share2. --- .../features/hero/components/Hero/Hero.tsx | 34 +++++++++++++++---- .../hero/components/Hero/hero.module.css | 8 +++++ .../src/features/portfolio/ProjectDetail.tsx | 8 +++++ 3 files changed, 43 insertions(+), 7 deletions(-) diff --git a/apps/web/src/features/hero/components/Hero/Hero.tsx b/apps/web/src/features/hero/components/Hero/Hero.tsx index 23e6a089..5223fa75 100644 --- a/apps/web/src/features/hero/components/Hero/Hero.tsx +++ b/apps/web/src/features/hero/components/Hero/Hero.tsx @@ -44,6 +44,7 @@ const Hero: FC = memo( logger.debug(`[Home] Section "${id}" is now visible`); }); const containerRef = useRef(null); + const topSentinelRef = useRef(null); const [mousePosition, setMousePosition] = useState({ x: 0, y: 0 }); const [isMouseNearBorder, setIsMouseNearBorder] = useState(false); @@ -92,20 +93,33 @@ const Hero: FC = memo( const handleScroll = (): void => { const currentScrollPosition = window.scrollY; setScrollPosition(currentScrollPosition); - - // The "Scroll to Explore" hint is only for the very top of the page — - // hide it as soon as the user scrolls at all (small threshold avoids - // sub-pixel flicker). Return-to-stars still waits until meaningfully - // scrolled away. - setShowScrollIndicator(currentScrollPosition <= 4); + // Return-to-stars appears once the user has scrolled meaningfully away. setShowReturnToStars(currentScrollPosition > 50); }; - window.addEventListener("scroll", handleScroll); + window.addEventListener("scroll", handleScroll, { passive: true }); handleScroll(); // Initial check return (): void => window.removeEventListener("scroll", handleScroll); }, []); + // The "Scroll to Explore" hint belongs only at the very top of the page. + // Drive its visibility from an IntersectionObserver on a 1px sentinel at the + // top of the hero rather than window.scrollY — on this layout window.scrollY + // wasn't a reliable signal, which left the hint visible far down the page. + // The sentinel leaves the viewport the moment the user scrolls at all, and + // re-enters when they scroll back to the top. + useEffect(() => { + const sentinel = topSentinelRef.current; + if (!sentinel) return; + + const observer = new IntersectionObserver( + ([entry]): void => setShowScrollIndicator(entry.isIntersecting), + { threshold: 0 }, + ); + observer.observe(sentinel); + return (): void => observer.disconnect(); + }, []); + const getThemeStyles = (): { textColor: string; gradientColors: string; @@ -144,6 +158,12 @@ const Hero: FC = memo( backgroundPosition: `center ${scrollPosition * 0.05}px`, }} > + {/* 1px sentinel at the very top — drives the scroll-hint visibility */} +