1
1
"use client" ;
2
2
3
- import { motion , AnimatePresence } from "framer-motion" ;
4
3
import { Menu , X } from "lucide-react" ;
5
- import Link from "next/link" ;
6
4
import { usePathname } from "next/navigation" ;
7
- import React , { useMemo , useCallback , useEffect } from "react" ;
5
+ import React , { useEffect } from "react" ;
8
6
9
- import Folder from "@/components/icons/folder" ;
10
7
import { cn } from "@/lib/utils" ;
11
8
12
- import { docsStructure , DocItem } from "../sidebar-structure" ;
13
-
14
- import {
15
- sidebarStaggerContainer ,
16
- sidebarFadeInLeft ,
17
- sidebarFadeInUp ,
18
- sidebarItemHover ,
19
- } from "./animations" ;
20
- import { useMobileSidebar } from "./useMobileSidebar" ;
21
-
22
- /**
23
- * Props for the DocSidebar component
24
- */
25
- interface DocSidebarProps {
26
- /** Additional CSS classes to apply */
27
- className ?: string ;
28
- /** Callback function when a sidebar item is clicked */
29
- onItemClick ?: ( path : string ) => void ;
30
- /** Whether the sidebar has already been animated */
31
- hasAnimated ?: boolean ;
32
- }
33
-
34
- /**
35
- * Props for the DocItemComponent
36
- */
37
- interface DocItemProps {
38
- /** The documentation item to render */
39
- item : DocItem ;
40
- /** The nesting level of the item */
41
- level : number ;
42
- /** Whether the item is currently active */
43
- isActive : boolean ;
44
- /** Callback function when the item is clicked */
45
- onItemClick ?: ( path : string ) => void ;
46
- /** Index of the item in its parent's children array */
47
- index : number ;
48
- }
49
-
50
- /**
51
- * Component for rendering a single documentation item in the sidebar
52
- */
53
- const DocItemComponent : React . FC < DocItemProps > = React . memo (
54
- ( { item, level, isActive, onItemClick, index } ) => {
55
- const pathname = usePathname ( ) ;
56
- const hasChildren = item . children && item . children . length > 0 ;
57
-
58
- const isPathActive = useCallback (
59
- ( path : string ) => pathname === path || pathname . startsWith ( `${ path } /` ) ,
60
- [ pathname ]
61
- ) ;
62
-
63
- const handleClick = useCallback ( ( ) => {
64
- onItemClick ?.( item . path ) ;
65
- } , [ item . path , onItemClick ] ) ;
66
-
67
- if ( hasChildren ) {
68
- return (
69
- < motion . div
70
- className = { cn ( "mb-3" , level === 0 && "first:mt-0" ) }
71
- variants = { sidebarFadeInUp }
72
- custom = { index }
73
- layout
9
+ import { docsStructure } from "./sidebar-structure" ;
10
+ import SidebarItem from "./SidebarItem" ;
11
+ import { DocSidebarProps } from "./types" ;
12
+ import { useMobileSidebar } from './useMobileSidebar' ;
13
+
14
+ const DocSidebar : React . FC < DocSidebarProps > = ( { className = "" , onItemClick } ) => {
15
+ const pathname = usePathname ( ) ;
16
+ const { isOpen, isMobile, toggleSidebar, sidebarRef, toggleButtonRef, setIsOpen } = useMobileSidebar ( ) ;
17
+
18
+ useEffect ( ( ) => {
19
+ const handleEscapeKey = ( event : KeyboardEvent ) => {
20
+ if ( event . key === "Escape" && isOpen && isMobile ) {
21
+ toggleSidebar ( ) ;
22
+ }
23
+ } ;
24
+
25
+ document . addEventListener ( "keydown" , handleEscapeKey ) ;
26
+ return ( ) => document . removeEventListener ( "keydown" , handleEscapeKey ) ;
27
+ } , [ isOpen , isMobile , toggleSidebar ] ) ;
28
+
29
+ return (
30
+ < >
31
+ { isMobile && (
32
+ < button
33
+ ref = { toggleButtonRef }
34
+ onClick = { toggleSidebar }
35
+ className = "mb-4 flex w-full items-center justify-center gap-2 rounded-md bg-gray-100 px-3 py-2 text-sm font-medium text-gray-900 dark:bg-gray-800 dark:text-white lg:hidden"
36
+ aria-expanded = { isOpen }
37
+ aria-controls = "doc-sidebar"
74
38
>
75
- < motion . div
76
- className = { cn (
77
- "mb-1 text-sm font-semibold" ,
78
- level > 0 && "pl-4" ,
79
- "tracking-wide text-gray-900 dark:text-white"
80
- ) }
81
- role = "heading"
82
- aria-level = { level + 1 }
83
- whileHover = { sidebarItemHover }
84
- layout
85
- >
86
- < Folder
87
- className = "-mt-0.5 mr-2 inline-block h-4 w-4 align-middle text-gray-500 dark:text-gray-400"
88
- aria-hidden = "true"
89
- />
90
- { item . title }
91
- </ motion . div >
92
- < motion . ul className = "list-none space-y-1" variants = { sidebarStaggerContainer } layout >
93
- { item . children ?. map ( ( child , childIndex ) => (
94
- < li key = { child . path } >
95
- < DocItemComponent
96
- item = { child }
97
- level = { level + 1 }
98
- isActive = { isPathActive ( child . path ) }
99
- onItemClick = { onItemClick }
100
- index = { childIndex }
101
- />
102
- </ li >
103
- ) ) }
104
- </ motion . ul >
105
- </ motion . div >
106
- ) ;
107
- }
39
+ { isOpen ? (
40
+ < >
41
+ < X className = "h-4 w-4" />
42
+ < span > Close navigation</ span >
43
+ </ >
44
+ ) : (
45
+ < >
46
+ < Menu className = "h-4 w-4" />
47
+ < span > Open navigation</ span >
48
+ </ >
49
+ ) }
50
+ </ button >
51
+ ) }
108
52
109
- return (
110
- < motion . div variants = { sidebarFadeInUp } custom = { index } layout >
111
- < Link
112
- href = { item . path }
113
- onClick = { handleClick }
53
+ { ( isOpen || ! isMobile ) && (
54
+ < nav
55
+ ref = { sidebarRef }
56
+ id = "doc-sidebar"
114
57
className = { cn (
115
- "relative block rounded-lg py-1 pl-4 pr-8 text-sm font-medium transition-colors " ,
116
- isActive
117
- ? "bg-gray-100 text-gray-900 dark: bg-gray-800 dark:text-white "
118
- : "text-gray-700 hover:bg-gray-100 dark:text-gray-400 dark:hover:bg-gray-800"
58
+ "w-full " ,
59
+ isMobile
60
+ ? "fixed inset-y-0 left-0 z-50 w-64 overflow-y-auto bg-white p-4 shadow-lg dark:bg-gray-900 "
61
+ : className
119
62
) }
120
- aria-current = { isActive ? "page" : undefined }
121
- >
122
- < motion . span className = "flex w-full items-start" whileHover = { sidebarItemHover } layout >
123
- < span className = "block flex-1" > { item . title } </ span >
124
- </ motion . span >
125
- </ Link >
126
- </ motion . div >
127
- ) ;
128
- }
129
- ) ;
130
-
131
- DocItemComponent . displayName = "DocItemComponent" ;
132
-
133
- /**
134
- * Documentation sidebar component
135
- *
136
- * Renders the navigation sidebar for the documentation pages
137
- * with support for both desktop and mobile views
138
- */
139
- const DocSidebar : React . FC < DocSidebarProps > = React . memo (
140
- ( { className = "" , onItemClick, hasAnimated = false } ) => {
141
- const pathname = usePathname ( ) ;
142
- const { isOpen, isMobile, toggleSidebar, sidebarRef, toggleButtonRef, setIsOpen } =
143
- useMobileSidebar ( ) ;
144
-
145
- // Add event listener for Escape key to close sidebar (for accessibility)
146
- useEffect ( ( ) => {
147
- const handleEscapeKey = ( event : KeyboardEvent ) => {
148
- if ( event . key === "Escape" && isOpen && isMobile ) {
149
- toggleSidebar ( ) ;
150
- }
151
- } ;
152
-
153
- // Add event listener
154
- document . addEventListener ( "keydown" , handleEscapeKey ) ;
155
-
156
- // Clean up event listener on unmount
157
- return ( ) => {
158
- document . removeEventListener ( "keydown" , handleEscapeKey ) ;
159
- } ;
160
- } , [ isOpen , isMobile , toggleSidebar ] ) ;
161
-
162
- const sidebarContent = useMemo (
163
- ( ) => (
164
- < motion . ul
165
- className = "list-none space-y-1"
166
- variants = { sidebarStaggerContainer }
167
- initial = { ! hasAnimated ? "hidden" : false }
168
- animate = { ! hasAnimated ? "visible" : false }
169
- layout
63
+ role = "navigation"
64
+ aria-label = "Documentation navigation"
170
65
>
171
- { docsStructure . map ( ( item , index ) => (
172
- < li key = { item . path } >
173
- < DocItemComponent
66
+ < div className = "space-y-1" >
67
+ { docsStructure . map ( ( item , index ) => (
68
+ < SidebarItem
69
+ key = { item . path }
174
70
item = { item }
175
71
level = { 0 }
176
72
isActive = { pathname === item . path }
@@ -180,82 +76,20 @@ const DocSidebar: React.FC<DocSidebarProps> = React.memo(
180
76
} }
181
77
index = { index }
182
78
/>
183
- </ li >
184
- ) ) }
185
- </ motion . ul >
186
- ) ,
187
- [ pathname , onItemClick , isMobile , hasAnimated , setIsOpen ]
188
- ) ;
189
-
190
- return (
191
- < >
192
- { /* Mobile sidebar toggle button */ }
193
- { isMobile && (
194
- < button
195
- ref = { toggleButtonRef }
196
- id = "sidebar-toggle"
197
- onClick = { toggleSidebar }
198
- className = "mb-4 flex w-full items-center justify-center gap-2 rounded-md bg-gray-100 px-3 py-2 text-sm font-medium text-gray-900 dark:bg-gray-800 dark:text-white"
199
- aria-expanded = { isOpen }
200
- aria-controls = "doc-sidebar"
201
- >
202
- { isOpen ? (
203
- < >
204
- < X className = "h-4 w-4" />
205
- < span > Close Navigation</ span >
206
- </ >
207
- ) : (
208
- < >
209
- < Menu className = "h-4 w-4" />
210
- < span > Open Navigation</ span >
211
- </ >
212
- ) }
213
- </ button >
214
- ) }
215
-
216
- { /* Sidebar navigation */ }
217
- < AnimatePresence >
218
- { ( isOpen || ! isMobile ) && (
219
- < motion . nav
220
- ref = { sidebarRef }
221
- id = "doc-sidebar"
222
- className = { cn (
223
- "w-full" ,
224
- isMobile
225
- ? "fixed inset-y-0 left-0 z-50 w-64 overflow-y-auto bg-white p-4 shadow-lg dark:bg-gray-900"
226
- : className
227
- ) }
228
- role = "navigation"
229
- aria-label = "Documentation navigation"
230
- variants = { sidebarFadeInLeft }
231
- initial = { ! hasAnimated ? "hidden" : false }
232
- animate = { ! hasAnimated ? "visible" : false }
233
- exit = { { opacity : 0 , x : - 20 } }
234
- transition = { { duration : 0.2 } }
235
- layout
236
- >
237
- { sidebarContent }
238
- </ motion . nav >
239
- ) }
240
- </ AnimatePresence >
241
-
242
- { /* Mobile sidebar backdrop */ }
243
- { isMobile && isOpen && (
244
- < motion . div
245
- className = "fixed inset-0 z-40 bg-black bg-opacity-50"
246
- onClick = { toggleSidebar }
247
- aria-hidden = "true"
248
- initial = { { opacity : 0 } }
249
- animate = { { opacity : 1 } }
250
- exit = { { opacity : 0 } }
251
- transition = { { duration : 0.2 } }
252
- />
253
- ) }
254
- </ >
255
- ) ;
256
- }
257
- ) ;
258
-
259
- DocSidebar . displayName = "DocSidebar" ;
79
+ ) ) }
80
+ </ div >
81
+ </ nav >
82
+ ) }
83
+
84
+ { isMobile && isOpen && (
85
+ < div
86
+ className = "fixed inset-0 z-40 bg-black bg-opacity-50 lg:hidden"
87
+ onClick = { toggleSidebar }
88
+ aria-hidden = "true"
89
+ />
90
+ ) }
91
+ </ >
92
+ ) ;
93
+ } ;
260
94
261
95
export default DocSidebar ;
0 commit comments