diff --git a/apps/www/components/ui/heading.tsx b/apps/www/components/ui/heading.tsx index 6d819e7fb..033c6cede 100644 --- a/apps/www/components/ui/heading.tsx +++ b/apps/www/components/ui/heading.tsx @@ -2,6 +2,7 @@ import type { ComponentProps } from "react"; import { useEffect, useState } from "react"; +import { useIsMobile } from "../../hooks/use-is-mobile"; // Global state to track the active heading ID let activeHeadingId: string | null = null; @@ -24,18 +25,27 @@ export function Heading({ as?: "h1" | "h2" | "h3" | "h4" | "h5" | "h6"; }) { const [isActive, setIsActive] = useState(false); + const isMobile = useIsMobile(); useEffect(() => { const handleActiveChange = (activeId: string | null) => { - setIsActive(activeId === id); + // Only set active state on mobile devices + if (isMobile) { + setIsActive(activeId === id); + } }; headingListeners.add(handleActiveChange); - // Set initial state - setIsActive(activeHeadingId === id); + // Set initial state only on mobile + if (isMobile) { + setIsActive(activeHeadingId === id); + } const handleClickOutside = (event: MouseEvent) => { + // Only handle click outside on mobile devices + if (!isMobile) return; + // Check if the click is outside any heading or anchor const target = event.target as Element; if ( @@ -52,11 +62,15 @@ export function Heading({ headingListeners.delete(handleActiveChange); document.removeEventListener("click", handleClickOutside); }; - }, [id]); + }, [id, isMobile]); const handleAnchorClick = (e: React.MouseEvent) => { e.preventDefault(); - setActiveHeading(id ?? null); + + // Only set active state on mobile devices + if (isMobile) { + setActiveHeading(id ?? null); + } // Always scroll to the element, even if hash is already set const element = document.getElementById(id || ""); @@ -74,8 +88,10 @@ export function Heading({ return; } - // Show the anchor but don't scroll - only the pound symbol should scroll - setActiveHeading(id ?? null); + // Only show the anchor on mobile - on desktop it will only show on hover + if (isMobile) { + setActiveHeading(id ?? null); + } }; if (!id) @@ -95,15 +111,17 @@ export function Heading({ - # + Link to section: {id} + {children} diff --git a/apps/www/hooks/use-is-mobile.ts b/apps/www/hooks/use-is-mobile.ts new file mode 100644 index 000000000..13d9cd9f2 --- /dev/null +++ b/apps/www/hooks/use-is-mobile.ts @@ -0,0 +1,27 @@ +import * as React from "react"; + +const MOBILE_BREAKPOINT = 768; + +/** + * Hook to detect if the current viewport is mobile-sized. + * + * Based on the implementation from Vercel AI Chatbot: + * @see https://github.com/vercel/ai-chatbot/blob/main/hooks/use-mobile.tsx + */ +export function useIsMobile() { + const [isMobile, setIsMobile] = React.useState( + undefined, + ); + + React.useEffect(() => { + const mql = window.matchMedia(`(max-width: ${MOBILE_BREAKPOINT - 1}px)`); + const onChange = () => { + setIsMobile(window.innerWidth < MOBILE_BREAKPOINT); + }; + mql.addEventListener("change", onChange); + setIsMobile(window.innerWidth < MOBILE_BREAKPOINT); + return () => mql.removeEventListener("change", onChange); + }, []); + + return !!isMobile; +}