@@ -4,6 +4,7 @@ import { GithubIcon } from '~/components/icons/GithubIcon'
44import { DiscordIcon } from '~/components/icons/DiscordIcon'
55import { Link , useMatches , useParams } from '@tanstack/react-router'
66import { useLocalStorage } from '~/utils/useLocalStorage'
7+ import { useClickOutside } from '~/hooks/useClickOutside'
78import { last } from '~/utils/utils'
89import type { ConfigSchema , MenuItem } from '~/utils/config'
910import { Framework } from '~/libraries'
@@ -92,10 +93,13 @@ function DocsMenuStrip({
9293 }
9394
9495 return (
95- < div
96- className = "flex flex-col gap-2 py-2 px-2 cursor-pointer h-full w-full"
96+ < button
97+ type = "button"
98+ className = "flex flex-col gap-2 py-2 px-2 cursor-pointer h-full w-full focus:outline-none focus:ring-2 focus:ring-inset focus:ring-gray-400/50"
9799 onPointerEnter = { onHover }
100+ onFocus = { onHover }
98101 onClick = { onClick }
102+ aria-label = "Open documentation menu"
99103 >
100104 { /* FrameworkSelect + VersionSelect icons */ }
101105 < div className = "flex flex-col gap-2 shrink-0" >
@@ -140,7 +144,7 @@ function DocsMenuStrip({
140144 )
141145 } ) }
142146 </ div >
143- </ div >
147+ </ button >
144148 )
145149}
146150
@@ -346,9 +350,9 @@ export function DocsLayout({
346350 { group ?. label }
347351 </ LabelComp >
348352 < div className = "h-2" />
349- < ul className = "text-[.85em] leading-6 list-none" >
353+ < ul className = "text-[.85em] leading-snug list-none" >
350354 { group ?. children ?. map ( ( child , i ) => {
351- const linkClasses = `flex gap-2 items-center justify-between group px-2 py-0 .5 rounded-lg hover:bg-gray-500/10 opacity-60 hover:opacity-100`
355+ const linkClasses = `flex gap-2 items-center justify-between group px-2 py-1 .5 rounded-lg hover:bg-gray-500/10 opacity-60 hover:opacity-100`
352356
353357 return (
354358 < li key = { i } >
@@ -386,7 +390,7 @@ export function DocsLayout({
386390 >
387391 < div
388392 className = { twMerge (
389- 'overflow-auto w-full' ,
393+ 'w-full' ,
390394 props . isActive
391395 ? `font-bold text-transparent bg-clip-text bg-linear-to-r ${ colorFrom } ${ colorTo } `
392396 : '' ,
@@ -423,7 +427,7 @@ export function DocsLayout({
423427 Documentation
424428 </ div >
425429 </ summary >
426- < div className = "flex flex-col gap-4 p-4 whitespace-nowrap overflow-y-auto border-t border-gray-500/20 bg-white/20 text-lg dark:bg-black/20" >
430+ < div className = "flex flex-col gap-4 p-4 overflow-y-auto border-t border-gray-500/20 bg-white/20 text-lg dark:bg-black/20" >
427431 < div className = "flex flex-col gap-1" >
428432 < FrameworkSelect libraryId = { libraryId } />
429433 < VersionSelect libraryId = { libraryId } />
@@ -439,6 +443,15 @@ export function DocsLayout({
439443 const [ showLargeMenu , setShowLargeMenu ] = React . useState ( false )
440444 const leaveTimer = React . useRef < NodeJS . Timeout | undefined > ( undefined )
441445
446+ // Close menu when clicking outside (only on sm-xl screens where it's an overlay)
447+ const expandedMenuRef = useClickOutside < HTMLDivElement > ( {
448+ enabled :
449+ showLargeMenu &&
450+ typeof window !== 'undefined' &&
451+ window . innerWidth < 1280 ,
452+ onClickOutside : ( ) => setShowLargeMenu ( false ) ,
453+ } )
454+
442455 const largeMenu = (
443456 < >
444457 { /* Collapsed strip - visible on sm to xl, hidden on xl+. Lower z-index so expanded menu covers it */ }
@@ -467,20 +480,22 @@ export function DocsLayout({
467480 } }
468481 onClick = { ( ) => {
469482 if ( window . innerWidth < 1280 ) {
470- setShowLargeMenu ( ( prev ) => ! prev )
483+ clearTimeout ( leaveTimer . current )
484+ setShowLargeMenu ( true )
471485 }
472486 } }
473487 />
474488 </ div >
475489
476490 { /* Expanded menu - always visible on xl+, toggleable overlay on sm-xl */ }
477491 < div
492+ ref = { expandedMenuRef }
478493 className = { twMerge (
479494 'max-w-[250px] xl:max-w-[300px] 2xl:max-w-[400px]' ,
480- 'flex-col gap-4 ' ,
495+ 'flex-col' ,
481496 'h-[calc(100dvh-var(--navbar-height))] top-[var(--navbar-height)]' ,
482497 'z-20 border-r border-gray-500/20' ,
483- 'transition-all duration-300 p-4 ' ,
498+ 'transition-all duration-300' ,
484499 // Hidden on smallest screens, flex on sm+
485500 'hidden sm:flex' ,
486501 // On sm to xl: fixed overlay that slides in from left-0 (covers the strip)
@@ -505,12 +520,14 @@ export function DocsLayout({
505520 }
506521 } }
507522 >
508- < div className = "flex flex-col gap-1" >
509- < FrameworkSelect libraryId = { libraryId } />
510- < VersionSelect libraryId = { libraryId } />
511- </ div >
512- < div className = "flex-1 flex flex-col gap-4 whitespace-nowrap overflow-y-auto text-base pb-4" >
513- { menuItems }
523+ < div className = "flex-1 flex flex-col overflow-y-auto" >
524+ < div className = "flex flex-col gap-1 p-4" >
525+ < FrameworkSelect libraryId = { libraryId } />
526+ < VersionSelect libraryId = { libraryId } />
527+ </ div >
528+ < div className = "flex-1 flex flex-col gap-4 text-base px-4 pt-0 pb-4" >
529+ { menuItems }
530+ </ div >
514531 </ div >
515532 </ div >
516533 </ >
0 commit comments