Skip to content

Commit b4d0a30

Browse files
resolved the inconsistent behaviour in the table of content side bar
1 parent e3eac29 commit b4d0a30

3 files changed

Lines changed: 81 additions & 17 deletions

File tree

components/docs/DocTableOfContents.tsx

Lines changed: 79 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
'use client';
22

3-
import React, { useCallback, useRef } from 'react';
3+
import React, { useCallback, useEffect, useRef } from 'react';
44
import Link from 'next/link';
55
import { useRouter } from 'next/navigation';
66
import { 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

components/docs/DocsPageClient.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -238,7 +238,7 @@ export function DocsPageClient({
238238

239239
{/* Render sections */}
240240
{(page?.sections ?? []).map((section: DocumentSection) => (
241-
<section key={section.id} id={section.id} className="mt-8">
241+
<section key={section.id} id={section.id} className="mt-8 scroll-mt-24">
242242
<h2 className="text-2xl font-bold text-gray-900 mb-4">{section.title}</h2>
243243
{section.type === 'text' && Array.isArray(section.content) && (
244244
<div>

components/docs/DocsPageContent.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -947,7 +947,7 @@ const DocsPageContentComponent = ({
947947
}
948948

949949
return (
950-
<section key={section.id} id={section.id} className="mt-8">
950+
<section key={section.id} id={section.id} className="mt-8 scroll-mt-24">
951951
{/* Only render title if it exists */}
952952
{section.title && (
953953
<h2 className="text-xl font-bold text-gray-900 mb-3">{section.title}</h2>

0 commit comments

Comments
 (0)