refactor: docubook@latest template nextjs-docker
This commit is contained in:
@@ -1,30 +1,82 @@
|
||||
"use client"
|
||||
"use client";
|
||||
|
||||
import { ArrowUpRight, ChevronDown, ChevronUp } from "lucide-react"
|
||||
import Link from "next/link"
|
||||
import Image from "next/image"
|
||||
import Search from "@/components/SearchBox"
|
||||
import Anchor from "@/components/anchor"
|
||||
import { Separator } from "@/components/ui/separator"
|
||||
import docuConfig from "@/docu.json"
|
||||
import { Button } from "@/components/ui/button"
|
||||
import { useState, useCallback } from "react"
|
||||
import { motion, AnimatePresence } from "framer-motion"
|
||||
import { ModeToggle } from "@/components/ThemeToggle"
|
||||
import { ArrowUpRight, ChevronDown, ChevronUp } from "lucide-react";
|
||||
import Link from "next/link";
|
||||
import Image from "next/image";
|
||||
import Search from "@/components/SearchBox";
|
||||
import Anchor from "@/components/anchor";
|
||||
import { Separator } from "@/components/ui/separator";
|
||||
import docuConfig from "@/docu.json";
|
||||
import GitHubButton from "@/components/Github";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { useState, useCallback, useRef, useEffect } from "react";
|
||||
import { ModeToggle } from "@/components/ThemeToggle";
|
||||
|
||||
interface NavbarProps {
|
||||
id?: string
|
||||
id?: string;
|
||||
}
|
||||
|
||||
export function Navbar({ id }: NavbarProps) {
|
||||
const [isMenuOpen, setIsMenuOpen] = useState(false)
|
||||
const [isMenuOpen, setIsMenuOpen] = useState(false);
|
||||
const navRef = useRef<HTMLDivElement>(null);
|
||||
const menuRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
const toggleMenu = useCallback(() => {
|
||||
setIsMenuOpen((prev) => !prev)
|
||||
}, [])
|
||||
setIsMenuOpen((prev) => !prev);
|
||||
}, []);
|
||||
|
||||
// Close menu when the user clicks/taps anywhere outside the navbar, or presses Escape
|
||||
useEffect(() => {
|
||||
if (!isMenuOpen) return;
|
||||
const handleClickOutside = (event: MouseEvent) => {
|
||||
if (navRef.current && !navRef.current.contains(event.target as Node)) {
|
||||
setIsMenuOpen(false);
|
||||
}
|
||||
};
|
||||
const handleKeyDown = (event: KeyboardEvent) => {
|
||||
if (event.key === "Escape") setIsMenuOpen(false);
|
||||
};
|
||||
document.addEventListener("mousedown", handleClickOutside);
|
||||
document.addEventListener("keydown", handleKeyDown);
|
||||
return () => {
|
||||
document.removeEventListener("mousedown", handleClickOutside);
|
||||
document.removeEventListener("keydown", handleKeyDown);
|
||||
};
|
||||
}, [isMenuOpen]);
|
||||
|
||||
// Focus trap: keep Tab within the mobile menu when open
|
||||
useEffect(() => {
|
||||
if (!isMenuOpen || !menuRef.current) return;
|
||||
const menu = menuRef.current;
|
||||
const focusableSelector =
|
||||
'a[href], button, input, textarea, select, [tabindex]:not([tabindex="-1"])';
|
||||
|
||||
const handleTrap = (e: KeyboardEvent) => {
|
||||
if (e.key !== "Tab") return;
|
||||
const focusables = menu.querySelectorAll<HTMLElement>(focusableSelector);
|
||||
if (focusables.length === 0) return;
|
||||
const first = focusables[0];
|
||||
const last = focusables[focusables.length - 1];
|
||||
|
||||
if (e.shiftKey && document.activeElement === first) {
|
||||
e.preventDefault();
|
||||
last.focus();
|
||||
} else if (!e.shiftKey && document.activeElement === last) {
|
||||
e.preventDefault();
|
||||
first.focus();
|
||||
}
|
||||
};
|
||||
|
||||
// Move focus into the menu
|
||||
const focusables = menu.querySelectorAll<HTMLElement>(focusableSelector);
|
||||
if (focusables.length > 0) focusables[0].focus();
|
||||
|
||||
menu.addEventListener("keydown", handleTrap);
|
||||
return () => menu.removeEventListener("keydown", handleTrap);
|
||||
}, [isMenuOpen]);
|
||||
|
||||
return (
|
||||
<div className="sticky top-0 z-50 w-full">
|
||||
<div ref={navRef} className="sticky top-0 z-50 w-full">
|
||||
<nav id={id} className="bg-background h-16 w-full border-b">
|
||||
<div className="mx-auto flex h-full w-[95vw] items-center justify-between sm:container md:gap-2">
|
||||
<div className="flex items-center gap-6">
|
||||
@@ -42,6 +94,7 @@ export function Navbar({ id }: NavbarProps) {
|
||||
onClick={toggleMenu}
|
||||
aria-label={isMenuOpen ? "Close navigation menu" : "Open navigation menu"}
|
||||
aria-expanded={isMenuOpen}
|
||||
aria-controls="mobile-nav-menu"
|
||||
className="flex items-center gap-1 px-2 text-sm font-medium md:hidden"
|
||||
>
|
||||
{isMenuOpen ? (
|
||||
@@ -50,38 +103,47 @@ export function Navbar({ id }: NavbarProps) {
|
||||
<ChevronDown className="text-muted-foreground h-6 w-6" />
|
||||
)}
|
||||
</Button>
|
||||
|
||||
<Separator className="my-4 hidden h-9 md:flex" orientation="vertical" />
|
||||
<Search />
|
||||
<div className="hidden md:flex">
|
||||
<GitHubButton />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
<AnimatePresence>
|
||||
{isMenuOpen && (
|
||||
<motion.div
|
||||
initial={{ opacity: 0, height: 0 }}
|
||||
animate={{ opacity: 1, height: "auto" }}
|
||||
exit={{ opacity: 0, height: 0 }}
|
||||
transition={{ duration: 0.2, ease: "easeInOut" }}
|
||||
className="bg-background/95 w-full border-b shadow-sm backdrop-blur-sm md:hidden"
|
||||
>
|
||||
<div className="mx-auto w-[95vw] sm:container">
|
||||
<ul className="flex flex-col py-2">
|
||||
<NavMenuCollapsible onItemClick={() => setIsMenuOpen(false)} />
|
||||
</ul>
|
||||
<div className="flex items-center justify-between border-t px-1 py-3">
|
||||
<ModeToggle />
|
||||
</div>
|
||||
<div
|
||||
id="mobile-nav-menu"
|
||||
ref={menuRef}
|
||||
role="dialog"
|
||||
aria-modal={isMenuOpen ? true : undefined}
|
||||
aria-label="Navigation menu"
|
||||
className="bg-background/95 grid w-full border-b shadow-sm backdrop-blur-sm transition-[grid-template-rows,opacity] duration-200 ease-in-out md:hidden"
|
||||
style={{
|
||||
gridTemplateRows: isMenuOpen ? "1fr" : "0fr",
|
||||
opacity: isMenuOpen ? 1 : 0,
|
||||
borderBottomWidth: isMenuOpen ? undefined : 0,
|
||||
}}
|
||||
>
|
||||
<div className="overflow-hidden">
|
||||
<div className="mx-auto w-[95vw] sm:container">
|
||||
<ul className="flex flex-col py-2">
|
||||
<NavMenuCollapsible onItemClick={() => setIsMenuOpen(false)} />
|
||||
</ul>
|
||||
<div className="flex items-center justify-between border-t px-1 py-3">
|
||||
<GitHubButton />
|
||||
<ModeToggle />
|
||||
</div>
|
||||
</motion.div>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
export function Logo() {
|
||||
const { navbar } = docuConfig
|
||||
const { navbar } = docuConfig;
|
||||
|
||||
return (
|
||||
<Link href="/" className="flex items-center gap-1.5">
|
||||
@@ -98,17 +160,17 @@ export function Logo() {
|
||||
{navbar.logoText}
|
||||
</h2>
|
||||
</Link>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
// Desktop NavMenu — horizontal list
|
||||
export function NavMenu() {
|
||||
const { navbar } = docuConfig
|
||||
const { navbar } = docuConfig;
|
||||
|
||||
return (
|
||||
<>
|
||||
{navbar?.menu?.map((item) => {
|
||||
const isExternal = item.href.startsWith("http")
|
||||
const isExternal = item.href.startsWith("http");
|
||||
return (
|
||||
<Anchor
|
||||
key={`${item.title}-${item.href}`}
|
||||
@@ -122,20 +184,20 @@ export function NavMenu() {
|
||||
{item.title}
|
||||
{isExternal && <ArrowUpRight className="text-foreground/80 h-4 w-4" />}
|
||||
</Anchor>
|
||||
)
|
||||
);
|
||||
})}
|
||||
</>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
// Mobile Collapsible NavMenu — vertical list items
|
||||
function NavMenuCollapsible({ onItemClick }: { onItemClick: () => void }) {
|
||||
const { navbar } = docuConfig
|
||||
const { navbar } = docuConfig;
|
||||
|
||||
return (
|
||||
<>
|
||||
{navbar?.menu?.map((item) => {
|
||||
const isExternal = item.href.startsWith("http")
|
||||
const isExternal = item.href.startsWith("http");
|
||||
return (
|
||||
<li key={item.title + item.href}>
|
||||
<Anchor
|
||||
@@ -151,8 +213,8 @@ function NavMenuCollapsible({ onItemClick }: { onItemClick: () => void }) {
|
||||
{isExternal && <ArrowUpRight className="text-foreground/60 h-4 w-4 shrink-0" />}
|
||||
</Anchor>
|
||||
</li>
|
||||
)
|
||||
);
|
||||
})}
|
||||
</>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user