Skip to content

Commit 1f15cba

Browse files
lyzno1crazywoola
andauthored
Enhance API documentation TOC with modern design and improved UX (langgenius#23490)
Co-authored-by: crazywoola <427733928@qq.com>
1 parent 3344aaa commit 1f15cba

28 files changed

Lines changed: 462 additions & 109 deletions

web/app/(commonLayout)/datasets/Doc.tsx

Lines changed: 94 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
import { useEffect, useMemo, useState } from 'react'
44
import { useContext } from 'use-context-selector'
55
import { useTranslation } from 'react-i18next'
6-
import { RiListUnordered } from '@remixicon/react'
6+
import { RiCloseLine, RiListUnordered } from '@remixicon/react'
77
import TemplateEn from './template/template.en.mdx'
88
import TemplateZh from './template/template.zh.mdx'
99
import TemplateJa from './template/template.ja.mdx'
@@ -22,6 +22,7 @@ const Doc = ({ apiBaseUrl }: DocProps) => {
2222
const { t } = useTranslation()
2323
const [toc, setToc] = useState<Array<{ href: string; text: string }>>([])
2424
const [isTocExpanded, setIsTocExpanded] = useState(false)
25+
const [activeSection, setActiveSection] = useState<string>('')
2526
const { theme } = useTheme()
2627

2728
// Set initial TOC expanded state based on screen width
@@ -47,12 +48,47 @@ const Doc = ({ apiBaseUrl }: DocProps) => {
4748
return null
4849
}).filter((item): item is { href: string; text: string } => item !== null)
4950
setToc(tocItems)
51+
// Set initial active section
52+
if (tocItems.length > 0)
53+
setActiveSection(tocItems[0].href.replace('#', ''))
5054
}
5155
}
5256

5357
setTimeout(extractTOC, 0)
5458
}, [locale])
5559

60+
// Track scroll position for active section highlighting
61+
useEffect(() => {
62+
const handleScroll = () => {
63+
const scrollContainer = document.querySelector('.scroll-container')
64+
if (!scrollContainer || toc.length === 0)
65+
return
66+
67+
// Find active section based on scroll position
68+
let currentSection = ''
69+
toc.forEach((item) => {
70+
const targetId = item.href.replace('#', '')
71+
const element = document.getElementById(targetId)
72+
if (element) {
73+
const rect = element.getBoundingClientRect()
74+
// Consider section active if its top is above the middle of viewport
75+
if (rect.top <= window.innerHeight / 2)
76+
currentSection = targetId
77+
}
78+
})
79+
80+
if (currentSection && currentSection !== activeSection)
81+
setActiveSection(currentSection)
82+
}
83+
84+
const scrollContainer = document.querySelector('.scroll-container')
85+
if (scrollContainer) {
86+
scrollContainer.addEventListener('scroll', handleScroll)
87+
handleScroll() // Initial check
88+
return () => scrollContainer.removeEventListener('scroll', handleScroll)
89+
}
90+
}, [toc, activeSection])
91+
5692
// Handle TOC item click
5793
const handleTocClick = (e: React.MouseEvent<HTMLAnchorElement>, item: { href: string; text: string }) => {
5894
e.preventDefault()
@@ -84,40 +120,76 @@ const Doc = ({ apiBaseUrl }: DocProps) => {
84120

85121
return (
86122
<div className="flex">
87-
<div className={`fixed right-20 top-32 z-10 transition-all ${isTocExpanded ? 'w-64' : 'w-10'}`}>
123+
<div className={`fixed right-20 top-32 z-10 transition-all duration-150 ease-out ${isTocExpanded ? 'w-[280px]' : 'w-11'}`}>
88124
{isTocExpanded
89125
? (
90-
<nav className="toc max-h-[calc(100vh-150px)] w-full overflow-y-auto rounded-lg border border-components-panel-border bg-components-panel-bg p-4 shadow-md">
91-
<div className="mb-4 flex items-center justify-between">
92-
<h3 className="text-lg font-semibold text-text-primary">{t('appApi.develop.toc')}</h3>
126+
<nav className="toc flex max-h-[calc(100vh-150px)] w-full flex-col overflow-hidden rounded-xl border-[0.5px] border-components-panel-border bg-background-default-hover shadow-xl">
127+
<div className="relative z-10 flex items-center justify-between border-b border-components-panel-border-subtle bg-background-default-hover px-4 py-2.5">
128+
<span className="text-xs font-medium uppercase tracking-wide text-text-tertiary">
129+
{t('appApi.develop.toc')}
130+
</span>
93131
<button
94132
onClick={() => setIsTocExpanded(false)}
95-
className="text-text-tertiary hover:text-text-secondary"
133+
className="group flex h-6 w-6 items-center justify-center rounded-md transition-colors hover:bg-state-base-hover"
134+
aria-label="Close"
96135
>
97-
136+
<RiCloseLine className="h-3 w-3 text-text-quaternary transition-colors group-hover:text-text-secondary" />
98137
</button>
99138
</div>
100-
<ul className="space-y-2">
101-
{toc.map((item, index) => (
102-
<li key={index}>
103-
<a
104-
href={item.href}
105-
className="text-text-secondary transition-colors duration-200 hover:text-text-primary hover:underline"
106-
onClick={e => handleTocClick(e, item)}
107-
>
108-
{item.text}
109-
</a>
110-
</li>
111-
))}
112-
</ul>
139+
140+
<div className="from-components-panel-border-subtle/20 pointer-events-none absolute left-0 right-0 top-[41px] z-10 h-2 bg-gradient-to-b to-transparent"></div>
141+
<div className="pointer-events-none absolute left-0 right-0 top-[43px] z-10 h-3 bg-gradient-to-b from-background-default-hover to-transparent"></div>
142+
143+
<div className="relative flex-1 overflow-y-auto px-3 py-3 pt-1">
144+
{toc.length === 0 ? (
145+
<div className="px-2 py-8 text-center text-xs text-text-quaternary">
146+
{t('appApi.develop.noContent')}
147+
</div>
148+
) : (
149+
<ul className="space-y-0.5">
150+
{toc.map((item, index) => {
151+
const isActive = activeSection === item.href.replace('#', '')
152+
return (
153+
<li key={index}>
154+
<a
155+
href={item.href}
156+
onClick={e => handleTocClick(e, item)}
157+
className={cn(
158+
'group relative flex items-center rounded-md px-3 py-2 text-[13px] transition-all duration-200',
159+
isActive
160+
? 'bg-state-base-hover font-medium text-text-primary'
161+
: 'text-text-tertiary hover:bg-state-base-hover hover:text-text-secondary',
162+
)}
163+
>
164+
<span
165+
className={cn(
166+
'mr-2 h-1.5 w-1.5 rounded-full transition-all duration-200',
167+
isActive
168+
? 'scale-100 bg-text-accent'
169+
: 'scale-75 bg-components-panel-border',
170+
)}
171+
/>
172+
<span className="flex-1 truncate">
173+
{item.text}
174+
</span>
175+
</a>
176+
</li>
177+
)
178+
})}
179+
</ul>
180+
)}
181+
</div>
182+
183+
<div className="pointer-events-none absolute bottom-0 left-0 right-0 z-10 h-4 rounded-b-xl bg-gradient-to-t from-background-default-hover to-transparent"></div>
113184
</nav>
114185
)
115186
: (
116187
<button
117188
onClick={() => setIsTocExpanded(true)}
118-
className="flex h-10 w-10 items-center justify-center rounded-full border border-components-panel-border bg-components-button-secondary-bg shadow-md transition-colors duration-200 hover:bg-components-button-secondary-bg-hover"
189+
className="group flex h-11 w-11 items-center justify-center rounded-full border-[0.5px] border-components-panel-border bg-components-panel-bg shadow-lg transition-all duration-150 hover:bg-background-default-hover hover:shadow-xl"
190+
aria-label="Open table of contents"
119191
>
120-
<RiListUnordered className="h-6 w-6 text-components-button-secondary-text" />
192+
<RiListUnordered className="h-5 w-5 text-text-tertiary transition-colors group-hover:text-text-secondary" />
121193
</button>
122194
)}
123195
</div>

0 commit comments

Comments
 (0)