Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
34 changes: 27 additions & 7 deletions apps/web/src/features/hero/components/Hero/Hero.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ const Hero: FC<ExtendedHeroProps> = memo(
logger.debug(`[Home] Section "${id}" is now visible`);
});
const containerRef = useRef<HTMLDivElement>(null);
const topSentinelRef = useRef<HTMLDivElement>(null);
const [mousePosition, setMousePosition] = useState({ x: 0, y: 0 });
const [isMouseNearBorder, setIsMouseNearBorder] = useState(false);

Expand Down Expand Up @@ -92,20 +93,33 @@ const Hero: FC<ExtendedHeroProps> = 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;
Expand Down Expand Up @@ -144,6 +158,12 @@ const Hero: FC<ExtendedHeroProps> = memo(
backgroundPosition: `center ${scrollPosition * 0.05}px`,
}}
>
{/* 1px sentinel at the very top — drives the scroll-hint visibility */}
<div
ref={topSentinelRef}
aria-hidden="true"
className={styles.scrollSentinel}
/>
<div
className={
isDarkMode ? styles.heroOverlayDark : styles.heroOverlayLight
Expand Down
8 changes: 8 additions & 0 deletions apps/web/src/features/hero/components/Hero/hero.module.css
Original file line number Diff line number Diff line change
Expand Up @@ -203,6 +203,14 @@
transform: translateY(-2px);
}

/* 1px marker at the top of the hero; the scroll-to-explore hint is shown only
while this is in the viewport (i.e. the page is at the very top). */
.scrollSentinel {
width: 100%;
height: 1px;
pointer-events: none;
Comment on lines +208 to +211

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Keep the scroll sentinel out of flex flow

This sentinel is inserted as a normal child of .heroSection, which is a centered flex container, so on the homepage it participates in the row layout alongside .heroContainer: the full-width sentinel can shrink/shift the hero content and, because align-items: center applies, the 1px target is vertically centered rather than at the top. In that state the IntersectionObserver keeps the scroll hint visible until the centered sentinel leaves the viewport instead of hiding on any scroll; position the sentinel absolutely at top: 0 (or otherwise remove it from flex flow).

Useful? React with 👍 / 👎.

}

.scrollIndicator {
position: fixed; /* Changed from absolute to fixed for viewport positioning */
/* Hug the very bottom edge and stay compact so it overlaps the interactive
Expand Down
8 changes: 8 additions & 0 deletions apps/web/src/features/portfolio/ProjectDetail.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,10 @@ import {
ChevronRight,
Home,
Bot,
Waypoints,
Receipt,
MessagesSquare,
Share2,
X,
} from "lucide-react";
import { SEO } from "@/components/SEO";
Expand All @@ -48,6 +52,10 @@ const projectIcons: Record<string, React.ReactNode> = {
autopr: <Bot size={48} />,
"phoenixvc-website": <Globe size={48} />,
"design-system": <Code size={48} />,
sluice: <Waypoints size={48} />,
docket: <Receipt size={48} />,
convolens: <MessagesSquare size={48} />,
omnipost: <Share2 size={48} />,
};

// Focus area icons (non-serializable, kept local)
Expand Down
Loading