header (#283)
This commit is contained in:
+11
-6
@@ -1,6 +1,8 @@
|
|||||||
import type { Metadata } from "next";
|
import type { Metadata } from "next";
|
||||||
import { Geist, Geist_Mono } from "next/font/google";
|
import { Geist, Geist_Mono } from "next/font/google";
|
||||||
import "./globals.css";
|
import "./globals.css";
|
||||||
|
import { MobileNavProvider } from "@/components/mobile-nav-context";
|
||||||
|
import { Header } from "@/components/header";
|
||||||
import { Sidebar } from "@/components/sidebar";
|
import { Sidebar } from "@/components/sidebar";
|
||||||
|
|
||||||
const geist = Geist({
|
const geist = Geist({
|
||||||
@@ -28,12 +30,15 @@ export default function RootLayout({
|
|||||||
<body
|
<body
|
||||||
className={`${geist.variable} ${geistMono.variable} antialiased bg-zinc-950 text-zinc-100`}
|
className={`${geist.variable} ${geistMono.variable} antialiased bg-zinc-950 text-zinc-100`}
|
||||||
>
|
>
|
||||||
<div className="flex min-h-screen">
|
<MobileNavProvider>
|
||||||
<Sidebar />
|
<Header />
|
||||||
<main className="flex-1 overflow-auto pt-14 lg:pt-0">
|
<div className="flex min-h-[calc(100vh-3.5rem)]">
|
||||||
{children}
|
<Sidebar />
|
||||||
</main>
|
<main className="flex-1 overflow-auto">
|
||||||
</div>
|
{children}
|
||||||
|
</main>
|
||||||
|
</div>
|
||||||
|
</MobileNavProvider>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -0,0 +1,89 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import Link from "next/link";
|
||||||
|
import { useMobileNav } from "./mobile-nav-context";
|
||||||
|
|
||||||
|
export function Header() {
|
||||||
|
const { isOpen, toggle } = useMobileNav();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<header className="sticky top-0 z-50 bg-black/90 backdrop-blur-sm">
|
||||||
|
<div className="flex h-14 items-center justify-between px-4 gap-6">
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<Link href="https://vercel.com" title="Made with love by Vercel">
|
||||||
|
<svg
|
||||||
|
data-testid="geist-icon"
|
||||||
|
height="18"
|
||||||
|
strokeLinejoin="round"
|
||||||
|
viewBox="0 0 16 16"
|
||||||
|
width="18"
|
||||||
|
style={{ color: "currentcolor" }}
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
fillRule="evenodd"
|
||||||
|
clipRule="evenodd"
|
||||||
|
d="M8 1L16 15H0L8 1Z"
|
||||||
|
fill="currentColor"
|
||||||
|
></path>
|
||||||
|
</svg>
|
||||||
|
</Link>
|
||||||
|
<span className="text-[#333]">
|
||||||
|
<svg
|
||||||
|
data-testid="geist-icon"
|
||||||
|
height="16"
|
||||||
|
strokeLinejoin="round"
|
||||||
|
viewBox="0 0 16 16"
|
||||||
|
width="16"
|
||||||
|
style={{ color: "currentcolor" }}
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
fillRule="evenodd"
|
||||||
|
clipRule="evenodd"
|
||||||
|
d="M4.01526 15.3939L4.3107 14.7046L10.3107 0.704556L10.6061 0.0151978L11.9849 0.606077L11.6894 1.29544L5.68942 15.2954L5.39398 15.9848L4.01526 15.3939Z"
|
||||||
|
fill="currentColor"
|
||||||
|
></path>
|
||||||
|
</svg>
|
||||||
|
</span>
|
||||||
|
<Link href="/">
|
||||||
|
<span className="font-medium tracking-tight text-lg">
|
||||||
|
agent-browser
|
||||||
|
</span>
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
<nav className="flex items-center gap-4">
|
||||||
|
<a
|
||||||
|
href="https://github.com/vercel-labs/agent-browser"
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
className="hidden sm:block text-sm text-[#666] hover:text-[#999] transition-colors"
|
||||||
|
>
|
||||||
|
GitHub
|
||||||
|
</a>
|
||||||
|
<a
|
||||||
|
href="https://www.npmjs.com/package/agent-browser"
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
className="hidden sm:block text-sm text-[#666] hover:text-[#999] transition-colors"
|
||||||
|
>
|
||||||
|
npm
|
||||||
|
</a>
|
||||||
|
<button
|
||||||
|
onClick={toggle}
|
||||||
|
className="lg:hidden p-2 -mr-2 text-[#888] hover:text-white transition-colors"
|
||||||
|
aria-label="Toggle menu"
|
||||||
|
>
|
||||||
|
{isOpen ? (
|
||||||
|
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={1.5} d="M6 18L18 6M6 6l12 12" />
|
||||||
|
</svg>
|
||||||
|
) : (
|
||||||
|
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={1.5} d="M4 6h16M4 12h16M4 18h16" />
|
||||||
|
</svg>
|
||||||
|
)}
|
||||||
|
</button>
|
||||||
|
</nav>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,45 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import { createContext, useContext, useState, useEffect } from "react";
|
||||||
|
import { usePathname } from "next/navigation";
|
||||||
|
|
||||||
|
type MobileNavContextType = {
|
||||||
|
isOpen: boolean;
|
||||||
|
setIsOpen: (open: boolean) => void;
|
||||||
|
toggle: () => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
const MobileNavContext = createContext<MobileNavContextType | null>(null);
|
||||||
|
|
||||||
|
export function MobileNavProvider({ children }: { children: React.ReactNode }) {
|
||||||
|
const [isOpen, setIsOpen] = useState(false);
|
||||||
|
const pathname = usePathname();
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setIsOpen(false);
|
||||||
|
}, [pathname]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const handleEscape = (e: KeyboardEvent) => {
|
||||||
|
if (e.key === "Escape") setIsOpen(false);
|
||||||
|
};
|
||||||
|
document.addEventListener("keydown", handleEscape);
|
||||||
|
return () => document.removeEventListener("keydown", handleEscape);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const toggle = () => setIsOpen(!isOpen);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<MobileNavContext.Provider value={{ isOpen, setIsOpen, toggle }}>
|
||||||
|
{children}
|
||||||
|
</MobileNavContext.Provider>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useMobileNav() {
|
||||||
|
const context = useContext(MobileNavContext);
|
||||||
|
if (!context) {
|
||||||
|
throw new Error("useMobileNav must be used within a MobileNavProvider");
|
||||||
|
}
|
||||||
|
return context;
|
||||||
|
}
|
||||||
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import { usePathname } from "next/navigation";
|
import { usePathname } from "next/navigation";
|
||||||
import { useState, useEffect } from "react";
|
import { useMobileNav } from "./mobile-nav-context";
|
||||||
|
|
||||||
const navigation = [
|
const navigation = [
|
||||||
{ name: "Introduction", href: "/" },
|
{ name: "Introduction", href: "/" },
|
||||||
@@ -20,46 +20,10 @@ const navigation = [
|
|||||||
|
|
||||||
export function Sidebar() {
|
export function Sidebar() {
|
||||||
const pathname = usePathname();
|
const pathname = usePathname();
|
||||||
const [isOpen, setIsOpen] = useState(false);
|
const { isOpen, setIsOpen } = useMobileNav();
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
setIsOpen(false);
|
|
||||||
}, [pathname]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
const handleEscape = (e: KeyboardEvent) => {
|
|
||||||
if (e.key === "Escape") setIsOpen(false);
|
|
||||||
};
|
|
||||||
document.addEventListener("keydown", handleEscape);
|
|
||||||
return () => document.removeEventListener("keydown", handleEscape);
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{/* Mobile header */}
|
|
||||||
<header className="lg:hidden fixed top-0 left-0 right-0 z-50 bg-black/90 backdrop-blur-sm border-b border-[#222] px-4 py-3">
|
|
||||||
<div className="flex items-center justify-between">
|
|
||||||
<Link href="/" className="text-sm font-medium">
|
|
||||||
agent-browser
|
|
||||||
</Link>
|
|
||||||
<button
|
|
||||||
onClick={() => setIsOpen(!isOpen)}
|
|
||||||
className="p-2 -mr-2 text-[#888] hover:text-white transition-colors"
|
|
||||||
aria-label="Toggle menu"
|
|
||||||
>
|
|
||||||
{isOpen ? (
|
|
||||||
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
||||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={1.5} d="M6 18L18 6M6 6l12 12" />
|
|
||||||
</svg>
|
|
||||||
) : (
|
|
||||||
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
||||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={1.5} d="M4 6h16M4 12h16M4 18h16" />
|
|
||||||
</svg>
|
|
||||||
)}
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</header>
|
|
||||||
|
|
||||||
{/* Mobile overlay */}
|
{/* Mobile overlay */}
|
||||||
{isOpen && (
|
{isOpen && (
|
||||||
<div
|
<div
|
||||||
@@ -71,22 +35,14 @@ export function Sidebar() {
|
|||||||
{/* Sidebar */}
|
{/* Sidebar */}
|
||||||
<aside
|
<aside
|
||||||
className={`
|
className={`
|
||||||
fixed lg:sticky top-0 left-0 z-50 lg:z-auto
|
fixed lg:sticky top-14 left-0 z-50 lg:z-auto
|
||||||
w-56 lg:w-48 h-screen
|
w-56 lg:w-48 h-[calc(100vh-3.5rem)]
|
||||||
bg-black border-r border-[#222]
|
bg-black
|
||||||
transform transition-transform duration-150 ease-out
|
transform transition-transform duration-150 ease-out
|
||||||
${isOpen ? "translate-x-0" : "-translate-x-full lg:translate-x-0"}
|
${isOpen ? "translate-x-0" : "-translate-x-full lg:translate-x-0"}
|
||||||
pt-14 lg:pt-0
|
|
||||||
`}
|
`}
|
||||||
>
|
>
|
||||||
<div className="h-full overflow-y-auto p-5">
|
<div className="h-full overflow-y-auto py-5 pl-3 pr-5">
|
||||||
{/* Desktop header */}
|
|
||||||
<div className="mb-8 hidden lg:block">
|
|
||||||
<Link href="/" className="text-sm font-medium">
|
|
||||||
agent-browser
|
|
||||||
</Link>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<nav className="space-y-0.5">
|
<nav className="space-y-0.5">
|
||||||
{navigation.map((item) => {
|
{navigation.map((item) => {
|
||||||
const isActive = pathname === item.href;
|
const isActive = pathname === item.href;
|
||||||
@@ -95,7 +51,7 @@ export function Sidebar() {
|
|||||||
<Link
|
<Link
|
||||||
key={item.name}
|
key={item.name}
|
||||||
href={item.href}
|
href={item.href}
|
||||||
className={`block px-2 py-1.5 text-[13px] transition-colors ${
|
className={`block px-2 py-1.5 text-sm transition-colors ${
|
||||||
isActive
|
isActive
|
||||||
? "text-white"
|
? "text-white"
|
||||||
: "text-[#666] hover:text-[#999]"
|
: "text-[#666] hover:text-[#999]"
|
||||||
@@ -106,25 +62,6 @@ export function Sidebar() {
|
|||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
</nav>
|
</nav>
|
||||||
|
|
||||||
<div className="mt-8 pt-4 border-t border-[#222] space-y-0.5">
|
|
||||||
<a
|
|
||||||
href="https://github.com/vercel-labs/agent-browser"
|
|
||||||
target="_blank"
|
|
||||||
rel="noopener noreferrer"
|
|
||||||
className="block px-2 py-1.5 text-[13px] text-[#666] hover:text-[#999] transition-colors"
|
|
||||||
>
|
|
||||||
GitHub
|
|
||||||
</a>
|
|
||||||
<a
|
|
||||||
href="https://www.npmjs.com/package/agent-browser"
|
|
||||||
target="_blank"
|
|
||||||
rel="noopener noreferrer"
|
|
||||||
className="block px-2 py-1.5 text-[13px] text-[#666] hover:text-[#999] transition-colors"
|
|
||||||
>
|
|
||||||
npm
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</aside>
|
</aside>
|
||||||
</>
|
</>
|
||||||
|
|||||||
Reference in New Issue
Block a user