187 lines
5.7 KiB
TypeScript
187 lines
5.7 KiB
TypeScript
'use client'
|
|
|
|
import {
|
|
MenuItem as _MenuItem,
|
|
Menu,
|
|
MenuButton,
|
|
MenuItems
|
|
} from '@headlessui/react'
|
|
import cn from 'clsx'
|
|
import { Anchor, Button } from 'nextra/components'
|
|
import { useFSRoute } from 'nextra/hooks'
|
|
import { ArrowRightIcon, MenuIcon } from 'nextra/icons'
|
|
import type { MenuItem } from 'nextra/normalize-pages'
|
|
import type { FC, ReactNode } from 'react'
|
|
import { setMenu, useConfig, useMenu, useThemeConfig } from 'nextra-theme-docs'
|
|
import { usePathname } from 'next/navigation'
|
|
import { normalizePages } from 'nextra/normalize-pages'
|
|
import { PageMapItem } from 'nextra'
|
|
|
|
const classes = {
|
|
link: cn(
|
|
'x:text-sm x:contrast-more:text-gray-700 x:contrast-more:dark:text-gray-100 x:whitespace-nowrap',
|
|
'x:text-gray-600 x:hover:text-black x:dark:text-gray-400 x:dark:hover:text-gray-200',
|
|
'x:ring-inset x:transition-colors'
|
|
)
|
|
}
|
|
|
|
const NavbarMenu: FC<{
|
|
menu: MenuItem
|
|
children: ReactNode
|
|
}> = ({ menu, children }) => {
|
|
const routes = Object.fromEntries(
|
|
(menu.children || []).map(route => [route.name, route])
|
|
)
|
|
return (
|
|
<Menu>
|
|
<MenuButton
|
|
className={({ focus }) =>
|
|
cn(
|
|
classes.link,
|
|
'x:items-center x:flex x:gap-1.5 x:cursor-pointer',
|
|
focus && 'x:nextra-focus'
|
|
)
|
|
}
|
|
>
|
|
{children}
|
|
<ArrowRightIcon
|
|
height="14"
|
|
className="x:*:origin-center x:*:transition-transform x:*:rotate-90"
|
|
/>
|
|
</MenuButton>
|
|
<MenuItems
|
|
transition
|
|
className={cn(
|
|
'x:focus-visible:nextra-focus',
|
|
'nextra-scrollbar x:motion-reduce:transition-none',
|
|
// From https://headlessui.com/react/menu#adding-transitions
|
|
'x:origin-top x:transition x:duration-200 x:ease-out x:data-closed:scale-95 x:data-closed:opacity-0',
|
|
'x:border x:border-black/5 x:dark:border-white/20',
|
|
'x:z-30 x:rounded-md x:py-1 x:text-sm x:shadow-lg',
|
|
'x:backdrop-blur-md x:bg-nextra-bg/70',
|
|
// headlessui adds max-height as style, use !important to override
|
|
'x:max-h-[min(calc(100vh-5rem),256px)]!'
|
|
)}
|
|
anchor={{ to: 'bottom', gap: 10, padding: 16 }}
|
|
>
|
|
{Object.entries(
|
|
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition -- fixme
|
|
(menu.items as Record<string, { title: string; href?: string }>) || {}
|
|
).map(([key, item]) => (
|
|
<_MenuItem
|
|
key={key}
|
|
as={Anchor}
|
|
href={item.href || routes[key]?.route}
|
|
className={({ focus }) =>
|
|
cn(
|
|
'x:block x:py-1.5 x:transition-colors x:ps-3 x:pe-9',
|
|
focus
|
|
? 'x:text-gray-900 x:dark:text-gray-100'
|
|
: 'x:text-gray-600 x:dark:text-gray-400'
|
|
)
|
|
}
|
|
>
|
|
{item.title}
|
|
</_MenuItem>
|
|
))}
|
|
</MenuItems>
|
|
</Menu>
|
|
)
|
|
}
|
|
|
|
const isMenu = (page: any): page is MenuItem => page.type === 'menu'
|
|
|
|
export const ClientNavbar: FC<{
|
|
pageMap: PageMapItem[]
|
|
children: ReactNode
|
|
className?: string
|
|
}> = ({ pageMap, children, className }) => {
|
|
|
|
const { topLevelNavbarItems } = normalizePages({
|
|
list: pageMap,
|
|
route: usePathname()
|
|
})
|
|
|
|
// filter out titles for elements in topLevelNavbarItems with non empty route
|
|
const existingCourseNames = new Set(
|
|
topLevelNavbarItems.filter(
|
|
item => !('href' in item)
|
|
).map(item => item.title)
|
|
)
|
|
console.log(existingCourseNames)
|
|
|
|
// filter out elements in topLevelNavbarItems with url but have title in existingCourseNames
|
|
const filteredTopLevelNavbarItems = topLevelNavbarItems.filter(item => !('href' in item && existingCourseNames.has(item.title)))
|
|
|
|
// const items = topLevelNavbarItems
|
|
// use filteredTopLevelNavbarItems to generate items
|
|
const items = filteredTopLevelNavbarItems
|
|
|
|
console.log(filteredTopLevelNavbarItems)
|
|
const themeConfig = useThemeConfig()
|
|
|
|
const pathname = useFSRoute()
|
|
const menu = useMenu()
|
|
|
|
return (
|
|
<>
|
|
<div
|
|
className={cn(
|
|
'x:flex x:gap-4 x:overflow-x-auto nextra-scrollbar x:py-1.5 x:max-md:hidden',
|
|
className
|
|
)}
|
|
>
|
|
{items.map((page, _index, arr) => {
|
|
if ('display' in page && page.display === 'hidden') return
|
|
if (isMenu(page)) {
|
|
return (
|
|
<NavbarMenu key={page.name} menu={page}>
|
|
{page.title}
|
|
</NavbarMenu>
|
|
)
|
|
}
|
|
const href =
|
|
// If it's a directory
|
|
('frontMatter' in page ? page.route : page.firstChildRoute) ||
|
|
page.href ||
|
|
page.route
|
|
|
|
const isCurrentPage =
|
|
href === pathname ||
|
|
(pathname.startsWith(page.route + '/') &&
|
|
arr.every(item => !('href' in item) || item.href !== pathname)) ||
|
|
undefined
|
|
|
|
return (
|
|
<Anchor
|
|
href={href}
|
|
key={page.name}
|
|
className={cn(
|
|
classes.link,
|
|
'x:aria-[current]:font-medium x:aria-[current]:subpixel-antialiased x:aria-[current]:text-current'
|
|
)}
|
|
aria-current={isCurrentPage}
|
|
>
|
|
{page.title}
|
|
</Anchor>
|
|
)
|
|
})}
|
|
</div>
|
|
{themeConfig.search && (
|
|
<div className="x:max-md:hidden">{themeConfig.search}</div>
|
|
)}
|
|
|
|
{children}
|
|
|
|
<Button
|
|
aria-label="Menu"
|
|
className={({ active }) =>
|
|
cn('nextra-hamburger x:md:hidden', active && 'x:bg-gray-400/20')
|
|
}
|
|
onClick={() => setMenu(prev => !prev)}
|
|
>
|
|
<MenuIcon height="24" className={cn({ open: menu })} />
|
|
</Button>
|
|
</>
|
|
)
|
|
} |