@@ -4,7 +4,7 @@ import NextLink from "next/link"
44import { Collapse , getComponents , useConfig } from "nextra-theme-docs"
55import { evaluate } from "nextra/components"
66import { useMounted } from "nextra/hooks"
7- import { memo , useCallback , useMemo , useState } from "react"
7+ import { memo , useCallback , useEffect , useMemo , useState } from "react"
88
99import { Card } from "@/components"
1010import { CheckboxTree , type CheckboxTreeItem } from "@/components/checkbox-tree"
@@ -52,6 +52,8 @@ type CodePageProps = {
5252
5353const TAG_PARAM_KEY = "tags"
5454
55+ type Sort = "popularity" | "alphabetical"
56+
5557export function CodePage ( { allTags, data } : CodePageProps ) {
5658 const allTagsMap = useMemo (
5759 ( ) =>
@@ -60,9 +62,7 @@ export function CodePage({ allTags, data }: CodePageProps) {
6062 [ ] ,
6163 )
6264
63- const { activePath } = useConfig ( ) . normalizePagesResult
64-
65- const [ sort , setSort ] = useState ( "popularity" )
65+ const [ sort , setSort ] = useState < Sort > ( "popularity" )
6666
6767 const sidebarState = useToolsAndLibrariesSidebarState ( {
6868 allTagsMap,
@@ -76,6 +76,17 @@ export function CodePage({ allTags, data }: CodePageProps) {
7676 title = `${ sidebarState . selectedTagsAsString } | ${ title } `
7777 }
7878
79+ useEffect ( ( ) => {
80+ document . addEventListener ( "keydown" , e => {
81+ if ( e . key === "Escape" ) {
82+ const toggle = document . getElementById ( "sidebar-toggle" )
83+ if ( toggle && ( toggle as HTMLInputElement ) . checked ) {
84+ toggle . click ( )
85+ }
86+ }
87+ } )
88+ } , [ ] )
89+
7990 return (
8091 < >
8192 < NextHead >
@@ -92,6 +103,7 @@ export function CodePage({ allTags, data }: CodePageProps) {
92103 < style >
93104 { `@media (max-width: 767px) { html:has(#sidebar-toggle:checked) { overscroll-behavior: none; } }` }
94105 </ style >
106+ < SidebarOverlay />
95107 < div className = "relative flex md:gap-6" >
96108 < ToolsAndLibrariesSidebar
97109 searchInputValue = { sidebarState . searchInputValue }
@@ -101,25 +113,8 @@ export function CodePage({ allTags, data }: CodePageProps) {
101113 updateTags = { sidebarState . updateTags }
102114 />
103115 < div className = "flex-1 @container" >
104- < Breadcrumbs
105- className = "mb-2 mt-1 md:mb-6"
106- activePath = { activePath }
107- />
108- < RadioGroup
109- value = { sort }
110- onValueChange = { value => setSort ( value as string ) }
111- className = "typography-menu flex flex-wrap gap-2 text-sm text-neu-800 dark:text-neu-600 md:flex-nowrap"
112- >
113- < div > Sort by:</ div >
114- < label className = "-m-1 flex items-center gap-1 p-1 [&:has([data-checked])]:text-neu-900 [&:has([data-unchecked])]:cursor-pointer [&:has([data-unchecked])]:hover:bg-neu-50/50" >
115- < Radio value = "popularity" />
116- < span > Popularity</ span >
117- </ label >
118- < label className = "-m-1 flex items-center gap-1 p-1 [&:has([data-checked])]:text-neu-900 [&:has([data-unchecked])]:cursor-pointer [&:has([data-unchecked])]:hover:bg-neu-50/50" >
119- < Radio value = "alphabetical" />
120- < span > Alphabetical</ span >
121- </ label >
122- </ RadioGroup >
116+ < ToolsAndLibrariesHeader sort = { sort } setSort = { setSort } />
117+
123118 < div className = "grid gap-2 py-4 @[640px]:grid-cols-2 md:gap-4 md:py-8 lg:grid-cols-2" >
124119 { ( sort === "alphabetical"
125120 ? [ ...sidebarState . filteredData ] . sort ( ( a , b ) =>
@@ -327,7 +322,7 @@ function ToolsAndLibrariesSidebar({
327322} : ToolsAndLibrariesSidebarProps ) {
328323 const [ sidebarShown , setSidebarShown ] = useState ( true )
329324 return (
330- < div className = "sticky top-[calc(var(--navbar-h)+1.5rem)] max-md:fixed max-md:right-0 max-md:top-[--navbar-h] max-md:z-10 max-md:border-l max-md:border-l- white max-md:bg-[rgb(var(--nextra-bg),.8)] max-md:pr-4 max-md:pt-4 max-md:backdrop-blur-[6.4px] dark:max-md:bg -white/10 md:h-[calc(100vh-var(--nextra-navbar-height)-var(--nextra-menu-height))]" >
325+ < div className = "sticky top-[calc(var(--navbar-h)+1.5rem)] duration-200 ease-out max-md:fixed max-md:right-0 max-md:top-[--navbar-h] max-md:z-10 max-md:transform-gpu max-md:border-l max-md:border- white max-md:bg-[rgb(var(--nextra-bg),.8)] max-md:pr-2 max-md:pt-4 max-md:backdrop-blur-[6.4px] max-md:transition-transform dark:max-md:border -white/10 md:h-[calc(100vh-var(--nextra-navbar-height)-var(--nextra-menu-height))] max-md:hover-hover:[html:has(#sidebar-toggle:checked:hover)_&]:translate-x-[calc(100%-0.5rem)] max-md:[html:has(#sidebar-toggle:not(:checked))_&:not(:hover)]:translate-x-[calc(100%+1px)] max-md:hover-hover:[html:has(#sidebar-toggle:not(:checked))_&]:translate-y-16 max-md:hover-hover:[html:has(#sidebar-toggle:not(:checked))_&]:border-t max-md:hover-hover:[html:has(#sidebar-toggle:not(:checked):hover)_&]:translate-x-2 max-md:hover-hover:[html:has(#sidebar-toggle:not(:checked):hover)_&]:shadow-md " >
331326 < Collapse horizontal isOpen = { sidebarShown } >
332327 < section className = "nextra-scrollbar -mt-4 w-[300px] shrink-0 overflow-y-auto p-4 pb-8 md:h-[calc(100vh-var(--nextra-navbar-height)-var(--nextra-menu-height))] lg:pb-16" >
333328 < div className = "sticky top-0 z-10 bg-[rgb(var(--nextra-bg))] shadow-[0_8px_16px_8px_rgb(var(--nextra-bg))] before:absolute before:-top-20 before:bottom-0 before:w-full before:bg-[rgb(var(--nextra-bg))]" >
@@ -605,3 +600,70 @@ function useToolsAndLibrariesSidebarState({
605600 updateTags,
606601 }
607602}
603+
604+ function MobileSidebarToggle ( props : React . HTMLAttributes < HTMLLabelElement > ) {
605+ return (
606+ < Button
607+ as = "label"
608+ variant = "tertiary"
609+ isIconButton
610+ { ...props }
611+ className = { clsx (
612+ props . className ,
613+ "-m-1 cursor-pointer !p-1 !ring-transparent [&:not(:hover)]:!bg-transparent [&:not(:hover)]:!text-neu-700 dark:[&:not(:hover)]:!text-neu-500" ,
614+ ) }
615+ >
616+ < input type = "checkbox" id = "sidebar-toggle" className = "hidden" />
617+ < ArrowBarLeft className = "size-6" />
618+ < span className = "sr-only" > Toggle sidebar</ span >
619+ </ Button >
620+ )
621+ }
622+
623+ function ToolsAndLibrariesHeader ( {
624+ sort,
625+ setSort,
626+ } : {
627+ sort : Sort
628+ setSort : ( value : Sort ) => void
629+ } ) {
630+ const { activePath } = useConfig ( ) . normalizePagesResult
631+
632+ return (
633+ < div className = "top-[--navbar-h] flex items-center justify-between bg-[rgb(var(--nextra-bg))] max-md:sticky" >
634+ < div >
635+ < Breadcrumbs className = "mb-2 mt-1 md:mb-6" activePath = { activePath } />
636+ < RadioGroup
637+ value = { sort }
638+ onValueChange = { value => setSort ( value as Sort ) }
639+ className = "typography-menu flex flex-wrap gap-2 text-sm text-neu-800 dark:text-neu-600 md:flex-nowrap"
640+ >
641+ < div > Sort by:</ div >
642+ < label className = "-m-1 flex items-center gap-1 p-1 [&:has([data-checked])]:text-neu-900 [&:has([data-unchecked])]:cursor-pointer [&:has([data-unchecked])]:hover:bg-neu-50/50" >
643+ < Radio value = "popularity" />
644+ < span > Popularity</ span >
645+ </ label >
646+ < label className = "-m-1 flex items-center gap-1 p-1 [&:has([data-checked])]:text-neu-900 [&:has([data-unchecked])]:cursor-pointer [&:has([data-unchecked])]:hover:bg-neu-50/50" >
647+ < Radio value = "alphabetical" />
648+ < span > Alphabetical</ span >
649+ </ label >
650+ </ RadioGroup >
651+ </ div >
652+ < MobileSidebarToggle className = "md:hidden" />
653+ </ div >
654+ )
655+ }
656+
657+ function SidebarOverlay ( ) {
658+ return (
659+ < button
660+ tabIndex = { - 1 }
661+ title = "Close sidebar"
662+ className = "pointer-events-none fixed inset-0 top-[--navbar-h] z-[9] bg-[rgb(var(--nextra-bg),.25)] opacity-0 transition-opacity focus:outline-none max-md:[html:has(#sidebar-toggle:checked)_&]:pointer-events-auto max-md:[html:has(#sidebar-toggle:checked)_&]:opacity-100"
663+ // this button can't be a label because of hovers
664+ onClick = { ( ) => {
665+ document . getElementById ( "sidebar-toggle" ) ?. click ( )
666+ } }
667+ />
668+ )
669+ }
0 commit comments