11'use client' ;
22
3- import React , { useCallback , useRef } from 'react' ;
3+ import React , { useCallback , useEffect , useRef } from 'react' ;
44import Link from 'next/link' ;
55import { useRouter } from 'next/navigation' ;
66import { cn } from '@/lib/utils' ;
@@ -48,6 +48,9 @@ export const DocTableOfContents: React.FC<DocTableOfContentsProps> = ({
4848 const nav = useNavigation ( ) ;
4949 const router = useRouter ( ) ;
5050 const prefetchedRef = useRef < Set < string > > ( new Set ( ) ) ;
51+ const scrollContainerRef = useRef < HTMLDivElement > ( null ) ;
52+ const hoverScrollRafRef = useRef < number | null > ( null ) ;
53+ const topLevelItems = items . filter ( ( item ) => ( item . level ?? 1 ) === 1 ) ;
5154 const prefetchHref = useCallback (
5255 ( href : string ) => {
5356 if ( ! href . startsWith ( '/docs' ) ) return ;
@@ -58,10 +61,55 @@ export const DocTableOfContents: React.FC<DocTableOfContentsProps> = ({
5861 [ router ]
5962 ) ;
6063
61- if ( items . length === 0 && ! pages ?. length && ! onAddPage ) {
64+ if ( topLevelItems . length === 0 && ! pages ?. length && ! onAddPage ) {
6265 return null ;
6366 }
6467
68+ const scrollSidebarToBottom = ( ) => {
69+ const element = scrollContainerRef . current ;
70+ if ( ! element ) return ;
71+ element . scrollTo ( { top : element . scrollHeight , behavior : 'smooth' } ) ;
72+ } ;
73+
74+ const stopHoverScroll = ( ) => {
75+ if ( hoverScrollRafRef . current !== null ) {
76+ cancelAnimationFrame ( hoverScrollRafRef . current ) ;
77+ hoverScrollRafRef . current = null ;
78+ }
79+ } ;
80+
81+ const startHoverScrollDown = ( ) => {
82+ const element = scrollContainerRef . current ;
83+ if ( ! element ) return ;
84+
85+ stopHoverScroll ( ) ;
86+
87+ const scrollStep = ( ) => {
88+ const currentElement = scrollContainerRef . current ;
89+ if ( ! currentElement ) {
90+ stopHoverScroll ( ) ;
91+ return ;
92+ }
93+
94+ const maxScrollTop = currentElement . scrollHeight - currentElement . clientHeight ;
95+ if ( currentElement . scrollTop >= maxScrollTop ) {
96+ stopHoverScroll ( ) ;
97+ return ;
98+ }
99+
100+ currentElement . scrollTop = Math . min ( currentElement . scrollTop + 2.2 , maxScrollTop ) ;
101+ hoverScrollRafRef . current = requestAnimationFrame ( scrollStep ) ;
102+ } ;
103+
104+ hoverScrollRafRef . current = requestAnimationFrame ( scrollStep ) ;
105+ } ;
106+
107+ useEffect ( ( ) => {
108+ return ( ) => {
109+ stopHoverScroll ( ) ;
110+ } ;
111+ } , [ ] ) ;
112+
65113 return (
66114 < aside
67115 className = { cn (
@@ -72,30 +120,23 @@ export const DocTableOfContents: React.FC<DocTableOfContentsProps> = ({
72120 >
73121 < div className = "flex flex-col h-full" >
74122 { /* Scrollable section - "On This Page" and "Pages" */ }
75- < div className = "p-6 pb-0 flex-1 overflow-y-auto" >
76- { items . length > 0 && (
123+ < div ref = { scrollContainerRef } className = "p-6 pb-0 flex-1 overflow-y-auto no-scrollbar " >
124+ { topLevelItems . length > 0 && (
77125 < >
78126 < h2 className = "text-sm font-semibold text-gray-900 mb-4" >
79127 On This Page
80128 </ h2 >
81129 < nav className = "space-y-2" >
82- { items . map ( ( item ) => {
130+ { topLevelItems . map ( ( item ) => {
83131 const isActive = activeId === item . id ;
84- const level = item . level || 1 ;
85- // Calculate indentation: H2=level 1, H3=level 2, H4=level 3
86- const indentClass = level === 1 ? '' : level === 2 ? 'ml-4' : level === 3 ? 'ml-8' : 'ml-12' ;
87- const fontSize = level === 1 ? 'text-sm' : 'text-sm' ;
88132
89133 return (
90134 < Link
91135 key = { item . id }
92136 href = { `#${ item . id } ` }
93137 className = { cn (
94138 'block transition-colors' ,
95- fontSize ,
96- indentClass ,
97- level === 1 && 'font-medium' ,
98- level > 1 && 'text-gray-600' ,
139+ 'text-sm font-medium' ,
99140 isActive
100141 ? 'text-blue-600'
101142 : 'text-gray-700 hover:text-gray-900'
@@ -113,7 +154,7 @@ export const DocTableOfContents: React.FC<DocTableOfContentsProps> = ({
113154 { pages && pages . length > 0 && (
114155 < >
115156 < div className = { cn (
116- items . length > 0 ? "mt-8 pt-8 border-t border-gray-200" : ""
157+ topLevelItems . length > 0 ? "mt-8 pt-8 border-t border-gray-200" : ""
117158 ) } >
118159 < h2 className = "text-sm font-semibold text-gray-900 mb-4" >
119160 Pages
@@ -152,7 +193,30 @@ export const DocTableOfContents: React.FC<DocTableOfContentsProps> = ({
152193 </ div >
153194
154195 { /* Sticky bottom section - actions only */ }
155- < div className = "mt-auto p-6 pt-8 border-t border-gray-200 flex-shrink-0 bg-gray-50" >
196+ < div className = "relative mt-auto p-6 pt-8 border-t border-gray-200 flex-shrink-0 bg-gray-50" >
197+ < button
198+ type = "button"
199+ onClick = { scrollSidebarToBottom }
200+ onMouseEnter = { startHoverScrollDown }
201+ onMouseLeave = { stopHoverScroll }
202+ className = "absolute left-1/2 top-0 -translate-x-1/2 -translate-y-1/2 rounded-full border border-gray-200 bg-white p-1.5 text-gray-500 shadow-sm transition-all duration-200 hover:text-gray-700"
203+ aria-label = "Scroll to bottom"
204+ title = "Scroll to bottom"
205+ >
206+ < svg
207+ className = "h-3.5 w-3.5"
208+ fill = "none"
209+ stroke = "currentColor"
210+ viewBox = "0 0 24 24"
211+ >
212+ < path
213+ strokeLinecap = "round"
214+ strokeLinejoin = "round"
215+ strokeWidth = { 2 }
216+ d = "M19 9l-7 7-7-7"
217+ />
218+ </ svg >
219+ </ button >
156220 < div className = "space-y-3" >
157221 { onEditPage && canEdit && (
158222 < button
0 commit comments