@@ -4,20 +4,23 @@ import { AnimatePresence, motion } from "framer-motion";
44import { BookOpen , ChevronLeft , Github , Menu , X } from "lucide-react" ;
55import Link from "next/link" ;
66import { usePathname } from "next/navigation" ;
7- import { type FC , useCallback , useEffect , useMemo , useState } from "react" ;
7+ import { type FC , useEffect , useMemo , useState } from "react" ;
88import { createPortal } from "react-dom" ;
99import { Dropdown , type DropdownOption } from "@/components/ui/dropdown" ;
10+ import { useReducedMotion } from "@/hooks/use-reduced-motion" ;
1011import { getDocProjectIcon } from "@/lib/docs-projects" ;
1112import { cn } from "@/lib/utils" ;
1213import { NetlifyHighlight } from "./netlify-highlight" ;
1314import SidebarItem from "./sidebar-item" ;
1415import type { DocSidebarProps } from "./types" ;
16+ import { useMobileSidebar } from "./use-mobile-sidebar" ;
1517
1618const DocSidebar : FC < DocSidebarProps > = ( { className = "" , onItemClick, sidebarStructure } ) => {
1719 const pathname = usePathname ( ) ;
18- const [ isMobile , setIsMobile ] = useState ( false ) ;
19- const [ isOpen , setIsOpen ] = useState ( false ) ;
20+ const { isOpen , isMobile, toggleSidebar , sidebarRef , toggleButtonRef , setIsOpen } =
21+ useMobileSidebar ( ) ;
2022 const [ isMounted , setIsMounted ] = useState ( false ) ;
23+ const prefersReducedMotion = useReducedMotion ( ) ;
2124
2225 const currentProject = useMemo ( ( ) => {
2326 return (
@@ -49,83 +52,41 @@ const DocSidebar: FC<DocSidebarProps> = ({ className = "", onItemClick, sidebarS
4952 } , [ selectedProject , sidebarStructure ] ) ;
5053
5154 useEffect ( ( ) => {
52- setIsMounted ( true ) ;
53- } , [ ] ) ;
54-
55- useEffect ( ( ) => {
56- const check = ( ) => {
57- const mobile = window . innerWidth < 1024 ;
58- setIsMobile ( mobile ) ;
59- if ( ! mobile ) {
60- setIsOpen ( false ) ;
55+ const handleEscapeKey = ( event : KeyboardEvent ) => {
56+ if ( event . key === "Escape" && isOpen && isMobile ) {
57+ toggleSidebar ( ) ;
6158 }
6259 } ;
63- check ( ) ;
64- window . addEventListener ( "resize" , check ) ;
65- return ( ) => window . removeEventListener ( "resize" , check ) ;
66- } , [ ] ) ;
67-
68- useEffect ( ( ) => {
69- if ( isMobile && isOpen ) {
70- document . body . style . overflow = "hidden" ;
71- } else {
72- document . body . style . overflow = "" ;
73- }
74- return ( ) => {
75- document . body . style . overflow = "" ;
76- } ;
77- } , [ isMobile , isOpen ] ) ;
78-
79- // Close on navigation
80- // biome-ignore lint/correctness/useExhaustiveDependencies: intentionally close on pathname change
81- useEffect ( ( ) => {
82- if ( isMobile && isOpen ) {
83- setIsOpen ( false ) ;
84- }
85- } , [ pathname ] ) ;
86-
87- useEffect ( ( ) => {
88- const handleEscape = ( e : KeyboardEvent ) => {
89- if ( e . key === "Escape" && isOpen && isMobile ) {
90- setIsOpen ( false ) ;
91- }
92- } ;
93- document . addEventListener ( "keydown" , handleEscape ) ;
94- return ( ) => document . removeEventListener ( "keydown" , handleEscape ) ;
95- } , [ isOpen , isMobile ] ) ;
96-
97- const closeSidebar = useCallback ( ( ) => setIsOpen ( false ) , [ ] ) ;
98- const toggleSidebar = useCallback ( ( ) => setIsOpen ( ( v ) => ! v ) , [ ] ) ;
9960
100- const handleItemClick = useCallback (
101- ( path : string ) => {
102- onItemClick ?.( path ) ;
103- if ( isMobile ) {
104- setIsOpen ( false ) ;
105- }
106- } ,
107- [ isMobile , onItemClick ]
108- ) ;
61+ document . addEventListener ( "keydown" , handleEscapeKey ) ;
62+ return ( ) => document . removeEventListener ( "keydown" , handleEscapeKey ) ;
63+ } , [ isOpen , isMobile , toggleSidebar ] ) ;
10964
11065 const sidebarContent = (
11166 < >
112- { /* Header */ }
113- < div className = "shrink-0 border-gray-200/80 border-b bg-gray-50/80 px-4 py-4 backdrop-blur-sm dark:border-gray-800 dark:bg-gray-900/40 " >
67+ { /* Sidebar Header */ }
68+ < div className = "relative z-10 flex shrink-0 flex-col gap-3 border-gray-200 border-b bg-gray-50/50 px-4 py-4 backdrop-blur-sm dark:border-gray-800 dark:bg-gray-900/20 " >
11469 < div className = "flex items-center gap-3" >
115- < div className = "flex h-8 w-8 items-center justify-center rounded-lg bg-blue-600 dark:bg-blue-500" >
116- < BookOpen className = "h-4 w-4 text-white" />
117- </ div >
118- < div >
119- < h2 className = "font-semibold text-gray-900 text-sm leading-tight dark:text-white" >
120- Documentation
121- </ h2 >
70+ < motion . div
71+ className = "flex h-9 w-9 items-center justify-center rounded-lg bg-blue-600 shadow-xs dark:bg-blue-500"
72+ transition = { { duration : prefersReducedMotion ? 0 : 0.3 } }
73+ whileHover = { {
74+ scale : prefersReducedMotion ? 1 : 1.1 ,
75+ rotate : prefersReducedMotion ? 0 : 5 ,
76+ } }
77+ >
78+ < BookOpen className = "h-5 w-5 text-white" />
79+ </ motion . div >
80+ < div className = "flex flex-col" >
81+ < h2 className = "font-bold text-gray-900 text-sm dark:text-white" > Documentation</ h2 >
12282 < p className = "text-gray-500 text-xs dark:text-gray-400" > Browse all topics</ p >
12383 </ div >
12484 </ div >
12585
126- < div className = "relative z-[100] mt-3" >
86+ { /* Project Filter Dropdown */ }
87+ < div className = "relative z-[100] mt-2" >
12788 < Dropdown
128- buttonClassName = "h-9 text-xs"
89+ buttonClassName = "h-10 text-xs"
12990 className = "w-full"
13091 menuClassName = "max-h-[300px] z-[100]"
13192 onChange = { setSelectedProject }
@@ -137,9 +98,6 @@ const DocSidebar: FC<DocSidebarProps> = ({ className = "", onItemClick, sidebarS
13798
13899 < NetlifyHighlight />
139100
140- { /* Items */ }
141- < div className = "scrollbar-hide flex-1 overflow-y-auto px-3 py-3" >
142- < div className = "space-y-0.5" >
143101 { /* Sidebar Content - Scrollable with hidden scrollbar */ }
144102 < div
145103 className = "scrollbar-hide flex-1 px-3 py-4"
@@ -153,42 +111,58 @@ const DocSidebar: FC<DocSidebarProps> = ({ className = "", onItemClick, sidebarS
153111 item = { item }
154112 key = { item . path }
155113 level = { 0 }
156- onItemClick = { handleItemClick }
114+ onItemClick = { ( path ) => {
115+ onItemClick ?.( path ) ;
116+ if ( isMobile ) {
117+ setIsOpen ( false ) ;
118+ }
119+ } }
157120 />
158121 ) ) }
159122 </ div >
160123 </ div >
161124
162- { /* Footer */ }
163- < div className = "shrink-0 border-gray-200/80 border-t bg-gray-50/80 px-4 py-3 backdrop-blur-sm dark:border-gray-800 dark:bg-gray-900/40 " >
125+ { /* Sidebar Footer - Simple */ }
126+ < div className = "shrink-0 border-gray-200 border-t bg-gray-50/50 px-4 py-3 backdrop-blur-sm dark:border-gray-800 dark:bg-gray-900/20 " >
164127 < div className = "flex items-center justify-between" >
165- < p className = "text-gray-400 text-xs dark:text-gray-500 " >
166- © { new Date ( ) . getFullYear ( ) } EternalCodeTeam
128+ < p className = "text-gray-500 text-xs dark:text-gray-400 " >
129+ © { new Date ( ) . getFullYear ( ) } EternalCodeTeam
167130 </ p >
168131 < Link
169- className = "text-gray-400 transition-colors hover:text-gray-600 dark:text-gray-500 dark:hover:text-gray-300 "
132+ className = "group "
170133 href = "https://github.com/eternalcodeteam"
171134 rel = "noopener noreferrer"
172135 target = "_blank"
173136 title = "GitHub"
174137 >
175- < Github className = "h-4 w-4" />
138+ < motion . div
139+ whileHover = { { scale : prefersReducedMotion ? 1 : 1.1 } }
140+ whileTap = { { scale : prefersReducedMotion ? 1 : 0.95 } }
141+ >
142+ < Github className = "h-4 w-4 text-gray-500 transition-colors group-hover:text-gray-900 dark:text-gray-400 dark:group-hover:text-white" />
143+ </ motion . div >
176144 </ Link >
177145 </ div >
178146 </ div >
179147 </ >
180148 ) ;
181149
150+ useEffect ( ( ) => {
151+ setIsMounted ( true ) ;
152+ } , [ ] ) ;
153+
182154 return (
183155 < >
184- { /* Mobile toggle */ }
156+ { /* Mobile toggle button */ }
185157 { ! ! isMobile && (
186- < button
158+ < motion . button
187159 aria-controls = "doc-sidebar"
188160 aria-expanded = { isOpen }
189- className = "group mb-4 flex w-full items-center justify-between gap-3 rounded-xl border border-gray-200 bg-white px-4 py-3 text-left font-medium text-gray-900 text-sm shadow-sm transition-all hover:border-blue-300 hover:shadow-md lg:hidden dark:border-gray-800 dark:bg-gray-900 dark:text-white dark:hover:border-blue-700"
161+ className = "group mb-4 flex w-full items-center justify-between gap-3 rounded-lg border border-gray-200 bg-white px-4 py-3 font-medium text-gray-900 text-sm shadow-xs transition-all hover:border-blue-300 hover:shadow-md lg:hidden dark:border-gray-700 dark:bg-gray-800 dark:text-white dark:hover:border-blue-700"
190162 onClick = { toggleSidebar }
191- type = "button"
163+ ref = { toggleButtonRef }
164+ whileHover = { { scale : prefersReducedMotion ? 1 : 1.01 } }
165+ whileTap = { { scale : prefersReducedMotion ? 1 : 0.99 } }
192166 >
193167 < div className = "flex items-center gap-2" >
194168 { isOpen ? (
@@ -200,14 +174,14 @@ const DocSidebar: FC<DocSidebarProps> = ({ className = "", onItemClick, sidebarS
200174 </ div >
201175 < ChevronLeft
202176 className = { cn (
203- "h-4 w-4 transition-transform duration-200 " ,
204- isOpen ? "rotate-90 " : "- rotate-90 "
177+ "h-4 w-4 transform-gpu transition-transform duration-300 will-change-transform " ,
178+ isOpen ? "rotate-180 " : "rotate-0 "
205179 ) }
206180 />
207- </ button >
181+ </ motion . button >
208182 ) }
209183
210- { /* Desktop sidebar */ }
184+ { /* Desktop Sidebar */ }
211185 { ! isMobile && (
212186 < nav
213187 aria-label = "Documentation navigation"
@@ -218,101 +192,43 @@ const DocSidebar: FC<DocSidebarProps> = ({ className = "", onItemClick, sidebarS
218192 </ nav >
219193 ) }
220194
221- { /* Mobile sidebar portal */ }
222195 { isMounted &&
223196 createPortal (
224- < AnimatePresence >
225- { ! ! isMobile && ! ! isOpen && (
226- < >
227- < motion . div
228- animate = { { opacity : 1 } }
229- aria-hidden = "true"
230- className = "fixed inset-0 z-[60] bg-black/50 backdrop-blur-sm"
231- exit = { { opacity : 0 } }
232- initial = { { opacity : 0 } }
233- onClick = { closeSidebar }
234- />
197+ < >
198+ < AnimatePresence mode = "wait" >
199+ { ! ! isMobile && ! ! isOpen && (
235200 < motion . nav
236201 animate = { { x : 0 } }
237202 aria-label = "Documentation navigation"
238- className = "fixed inset-y-0 left-0 z-[70] flex w-80 max-w-[85vw] flex-col overflow-hidden bg-white shadow-2xl dark:bg-gray-950 "
239- exit = { { x : "-100%" } }
203+ className = "fixed inset-y-0 left-0 z-[70] flex w-72 flex-col overflow-auto overscroll-contain border-gray-200 border-r bg-white shadow-2xl dark:border-gray-700 dark: bg-gray-900 "
204+ exit = { { x : prefersReducedMotion ? 0 : - 280 } }
240205 id = "doc-sidebar-mobile"
241- initial = { { x : "-100%" } }
206+ initial = { { x : prefersReducedMotion ? 0 : - 280 } }
207+ ref = { sidebarRef }
242208 role = "navigation"
243- transition = { { type : "spring" , stiffness : 400 , damping : 40 } }
209+ transition = { {
210+ type : prefersReducedMotion ? "tween" : "spring" ,
211+ stiffness : 300 ,
212+ damping : 30 ,
213+ duration : prefersReducedMotion ? 0 : undefined ,
214+ } }
244215 >
245- { /* Mobile header with close */ }
246- < div className = "flex items-center justify-between border-gray-100 border-b px-4 py-3 dark:border-gray-800" >
247- < div className = "flex items-center gap-2" >
248- < BookOpen className = "h-4 w-4 text-blue-600 dark:text-blue-400" />
249- < span className = "font-semibold text-gray-900 text-sm dark:text-white" >
250- Navigation
251- </ span >
252- </ div >
253- < button
254- aria-label = "Close navigation"
255- className = "flex h-8 w-8 items-center justify-center rounded-lg text-gray-400 transition-colors hover:bg-gray-100 hover:text-gray-600 dark:hover:bg-gray-800 dark:hover:text-gray-200"
256- onClick = { closeSidebar }
257- type = "button"
258- >
259- < X className = "h-4 w-4" />
260- </ button >
261- </ div >
262-
263- { /* Project dropdown */ }
264- < div className = "border-gray-100 border-b px-4 py-3 dark:border-gray-800" >
265- < div className = "relative z-[100]" >
266- < Dropdown
267- buttonClassName = "h-9 text-xs"
268- className = "w-full"
269- menuClassName = "max-h-[300px] z-[100]"
270- onChange = { setSelectedProject }
271- options = { projectOptions }
272- value = { selectedProject }
273- />
274- </ div >
275- </ div >
276-
277- { /* Items */ }
278- < div className = "scrollbar-hide flex-1 overflow-y-auto px-3 py-3" >
279- < div className = "space-y-0.5" >
280- { filteredDocsStructure . map ( ( item , index ) => (
281- < SidebarItem
282- index = { index }
283- isActive = { pathname === item . path }
284- item = { item }
285- key = { item . path }
286- level = { 0 }
287- onItemClick = { handleItemClick }
288- />
289- ) ) }
290- </ div >
291- </ div >
292-
293- < NetlifyHighlight />
294-
295- { /* Footer */ }
296- < div className = "shrink-0 border-gray-100 border-t px-4 py-3 dark:border-gray-800" >
297- < div className = "flex items-center justify-between" >
298- < p className = "text-gray-400 text-xs dark:text-gray-500" >
299- © { new Date ( ) . getFullYear ( ) } EternalCodeTeam
300- </ p >
301- < Link
302- className = "text-gray-400 transition-colors hover:text-gray-600 dark:text-gray-500 dark:hover:text-gray-300"
303- href = "https://github.com/eternalcodeteam"
304- rel = "noopener noreferrer"
305- target = "_blank"
306- title = "GitHub"
307- >
308- < Github className = "h-4 w-4" />
309- </ Link >
310- </ div >
311- </ div >
216+ { sidebarContent }
312217 </ motion . nav >
313- </ >
218+ ) }
219+ </ AnimatePresence >
220+
221+ { ! ! isMobile && ! ! isOpen && (
222+ < motion . div
223+ animate = { { opacity : 1 } }
224+ aria-hidden = "true"
225+ className = "fixed inset-0 z-[60] bg-black/50 backdrop-blur-xs lg:hidden"
226+ exit = { { opacity : 0 } }
227+ initial = { { opacity : 0 } }
228+ onClick = { toggleSidebar }
229+ />
314230 ) }
315- </ AnimatePresence > ,
231+ </ > ,
316232 document . body
317233 ) }
318234 </ >
0 commit comments