@@ -12,19 +12,43 @@ import { contents } from "./sidebar-content";
1212
1313export default function CustomSidebar ( ) {
1414 const [ currentOpen , setCurrentOpen ] = useState < number > ( 0 ) ;
15+ const [ nestedOpen , setNestedOpen ] = useState < Set < string > > ( new Set ( ) ) ;
1516 const pathname = usePathname ( ) ;
1617 const { setOpenSearch } = useSearchContext ( ) ;
1718
1819 const getDefaultValue = useCallback ( ( ) => {
1920 const defaultValue = contents . findIndex ( ( item ) =>
20- item . list . some ( ( listItem ) => listItem . href === pathname )
21+ item . list . some ( ( listItem ) => {
22+ if ( listItem . href === pathname ) {
23+ return true ;
24+ }
25+ if ( listItem . children ) {
26+ return listItem . children . some ( ( child ) => child . href === pathname ) ;
27+ }
28+ return false ;
29+ } )
2130 ) ;
2231 return defaultValue === - 1 ? 0 : defaultValue ;
2332 } , [ pathname ] ) ;
2433
2534 useEffect ( ( ) => {
2635 setCurrentOpen ( getDefaultValue ( ) ) ;
27- } , [ getDefaultValue ] ) ;
36+ // Auto-open nested items that contain the current path
37+ const openNested = new Set < string > ( ) ;
38+ for ( const section of contents ) {
39+ for ( const item of section . list ) {
40+ if ( item . children ) {
41+ const hasActiveChild = item . children . some (
42+ ( child ) => child . href === pathname
43+ ) ;
44+ if ( hasActiveChild ) {
45+ openNested . add ( item . title ) ;
46+ }
47+ }
48+ }
49+ }
50+ setNestedOpen ( openNested ) ;
51+ } , [ getDefaultValue , pathname ] ) ;
2852
2953 const handleSearch = ( ) => {
3054 setOpenSearch ( true ) ;
@@ -65,7 +89,7 @@ export default function CustomSidebar() {
6589 weight = "fill"
6690 />
6791 < span className = "flex-1 text-sm" > { item . title } </ span >
68- { item . isNew && < NewBadge /> }
92+ { item . isNew ? < NewBadge /> : null }
6993 < motion . div
7094 animate = { { rotate : currentOpen === index ? 180 : 0 } }
7195 className = "shrink-0"
@@ -77,7 +101,7 @@ export default function CustomSidebar() {
77101 </ motion . div >
78102 </ button >
79103 < AnimatePresence initial = { false } >
80- { currentOpen === index && (
104+ { currentOpen === index ? (
81105 < motion . div
82106 animate = { { opacity : 1 , height : "auto" } }
83107 className = "relative overflow-hidden"
@@ -95,6 +119,85 @@ export default function CustomSidebar() {
95119 </ p >
96120 < div className = "h-px flex-grow bg-linear-to-r from-stone-800/90 to-stone-800/60" />
97121 </ div >
122+ ) : listItem . children ? (
123+ < div >
124+ < button
125+ className = "flex w-full items-center gap-3 px-6 py-2 text-left text-muted-foreground text-sm hover:bg-muted/50 hover:text-foreground"
126+ onClick = { ( ) => {
127+ const newOpen = new Set ( nestedOpen ) ;
128+ if ( newOpen . has ( listItem . title ) ) {
129+ newOpen . delete ( listItem . title ) ;
130+ } else {
131+ newOpen . add ( listItem . title ) ;
132+ }
133+ setNestedOpen ( newOpen ) ;
134+ } }
135+ type = "button"
136+ >
137+ { listItem . icon ? (
138+ < listItem . icon
139+ className = "size-5 shrink-0"
140+ weight = "duotone"
141+ />
142+ ) : null }
143+ < span className = "flex-1" >
144+ { listItem . title }
145+ </ span >
146+ { listItem . isNew ? < NewBadge /> : null }
147+ < motion . div
148+ animate = { {
149+ rotate : nestedOpen . has ( listItem . title )
150+ ? 90
151+ : 0 ,
152+ } }
153+ className = "shrink-0"
154+ >
155+ < CaretDownIcon
156+ className = "size-3 text-muted-foreground"
157+ weight = "duotone"
158+ />
159+ </ motion . div >
160+ </ button >
161+ < AnimatePresence initial = { false } >
162+ { nestedOpen . has ( listItem . title ) && (
163+ < motion . div
164+ animate = { {
165+ opacity : 1 ,
166+ height : "auto" ,
167+ } }
168+ className = "relative overflow-hidden"
169+ exit = { { opacity : 0 , height : 0 } }
170+ initial = { { opacity : 0 , height : 0 } }
171+ >
172+ < div className = "ml-4 border-border border-l pl-2" >
173+ { listItem . children . map ( ( child ) => (
174+ < AsideLink
175+ activeClassName = "!bg-muted !text-foreground font-medium"
176+ className = "flex items-center gap-3 px-6 py-2 text-muted-foreground text-sm hover:bg-muted/50 hover:text-foreground"
177+ href = { child . href || "#" }
178+ key = { child . title }
179+ startWith = "/docs"
180+ title = { child . title }
181+ >
182+ { child . icon ? (
183+ < child . icon
184+ className = "size-4 shrink-0"
185+ weight = "duotone"
186+ />
187+ ) : null }
188+ < span className = "flex-1" >
189+ { child . title }
190+ </ span >
191+ { child . isNew ? (
192+ < NewBadge />
193+ ) : null }
194+ </ AsideLink >
195+ ) ) }
196+ </ div >
197+ </ motion . div >
198+ ) }
199+ </ AnimatePresence >
200+ </ div >
98201 ) : (
99202 < AsideLink
100203 activeClassName = "!bg-muted !text-foreground font-medium"
@@ -103,22 +206,24 @@ export default function CustomSidebar() {
103206 startWith = "/docs"
104207 title = { listItem . title }
105208 >
106- < listItem . icon
107- className = "size-5 shrink-0"
108- weight = "duotone"
109- />
209+ { listItem . icon ? (
210+ < listItem . icon
211+ className = "size-5 shrink-0"
212+ weight = "duotone"
213+ />
214+ ) : null }
110215 < span className = "flex-1" >
111216 { listItem . title }
112217 </ span >
113- { listItem . isNew && < NewBadge /> }
218+ { listItem . isNew ? < NewBadge /> : null }
114219 </ AsideLink >
115220 ) }
116221 </ Suspense >
117222 </ div >
118223 ) ) }
119224 </ motion . div >
120225 </ motion . div >
121- ) }
226+ ) : null }
122227 </ AnimatePresence >
123228 </ div >
124229 ) ) }
@@ -136,7 +241,7 @@ function NewBadge({ isSelected }: { isSelected?: boolean }) {
136241 < Badge
137242 className = { cn (
138243 "!no-underline !decoration-transparent pointer-events-none border-dashed" ,
139- isSelected && "!border-solid"
244+ isSelected ? "!border-solid" : " "
140245 ) }
141246 variant = { isSelected ? "default" : "outline" }
142247 >
0 commit comments