From ba303175cc24d2722d53e02f6a649cb4e2b98983 Mon Sep 17 00:00:00 2001 From: Taesu Date: Sun, 28 Dec 2025 08:39:33 +0900 Subject: [PATCH] docs: add gradient mask image for TOCScrollArea --- docs/components/docs/layout/toc.tsx | 53 +++++++++++++++++++++++++++++ 1 file changed, 53 insertions(+) diff --git a/docs/components/docs/layout/toc.tsx b/docs/components/docs/layout/toc.tsx index a4008f6fa3..c748246b0c 100644 --- a/docs/components/docs/layout/toc.tsx +++ b/docs/components/docs/layout/toc.tsx @@ -81,6 +81,54 @@ export function TOCScrollArea({ ...props }: ComponentProps & { isMenu?: boolean }) { const viewRef = useRef(null); + const [scrollState, setScrollState] = useState<{ + isAtTop: boolean; + isAtBottom: boolean; + }>({ isAtTop: true, isAtBottom: true }); + + useEffect(() => { + const viewport = viewRef.current; + if (!viewport) return; + + const checkScroll = () => { + const { scrollTop, scrollHeight, clientHeight } = viewport; + const newState = { + isAtTop: scrollTop <= 1, + isAtBottom: scrollTop + clientHeight >= scrollHeight - 1, + }; + + setScrollState((prev) => { + if ( + prev.isAtTop === newState.isAtTop && + prev.isAtBottom === newState.isAtBottom + ) { + return prev; + } + return newState; + }); + }; + + checkScroll(); + viewport.addEventListener("scroll", checkScroll, { passive: true }); + + const observer = new ResizeObserver(checkScroll); + observer.observe(viewport); + + return () => { + viewport.removeEventListener("scroll", checkScroll); + observer.disconnect(); + }; + }, []); + + const maskImage = useMemo(() => { + const { isAtTop, isAtBottom } = scrollState; + if (isAtTop && isAtBottom) return "none"; + if (isAtTop) + return "linear-gradient(to bottom, black calc(100% - 40px), transparent)"; + if (isAtBottom) + return "linear-gradient(to top, black calc(100% - 40px), transparent)"; + return "linear-gradient(to bottom, transparent, black 40px, black calc(100% - 40px), transparent)"; + }, [scrollState]); return ( {props.children}