11"use client" ;
22
3- import { motion , AnimatePresence } from "framer-motion" ;
43import { Menu , X } from "lucide-react" ;
5- import Link from "next/link" ;
64import { usePathname } from "next/navigation" ;
7- import React , { useMemo , useCallback , useEffect } from "react" ;
5+ import React , { useEffect } from "react" ;
86
9- import Folder from "@/components/icons/folder" ;
107import { cn } from "@/lib/utils" ;
118
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"
7438 >
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+ ) }
10852
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"
11457 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
11962 ) }
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"
17065 >
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 }
17470 item = { item }
17571 level = { 0 }
17672 isActive = { pathname === item . path }
@@ -180,82 +76,20 @@ const DocSidebar: React.FC<DocSidebarProps> = React.memo(
18076 } }
18177 index = { index }
18278 />
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+ } ;
26094
26195export default DocSidebar ;
0 commit comments