33import { useEffect , useMemo , useState } from 'react'
44import { useContext } from 'use-context-selector'
55import { useTranslation } from 'react-i18next'
6- import { RiListUnordered } from '@remixicon/react'
6+ import { RiCloseLine , RiListUnordered } from '@remixicon/react'
77import TemplateEn from './template/template.en.mdx'
88import TemplateZh from './template/template.zh.mdx'
99import 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