feat(website): add layout with header, sidebar, footer and navigation

- Create Header component with logo, nav, theme toggle, language switcher
- Create Sidebar component with doc navigation from config
- Create Footer component
- Create MobileNav component with hamburger menu
- Create navigation config file (docsConfig)
- Integrate all layout components into [locale]/layout.tsx
- Add framer-motion for mobile nav animations
- All tests passing, build successful
This commit is contained in:
justsisyphus
2026-01-24 12:11:59 +09:00
parent f28c8a45dd
commit 5d14e09279
9 changed files with 298 additions and 1 deletions
+9 -1
View File
@@ -6,6 +6,7 @@ import { NextIntlClientProvider } from 'next-intl';
import { getMessages } from 'next-intl/server';
import { notFound } from 'next/navigation';
import { routing } from '@/i18n/routing';
import { Header, Sidebar, Footer } from '@/components/layout';
const geistSans = Geist({
variable: "--font-geist-sans",
@@ -49,7 +50,14 @@ export default async function RootLayout({
enableSystem
disableTransitionOnChange
>
{children}
<div className="relative flex min-h-screen flex-col">
<Header />
<div className="flex-1 flex">
<Sidebar />
<main className="flex-1">{children}</main>
</div>
<Footer />
</div>
</ThemeProvider>
</NextIntlClientProvider>
</body>
+16
View File
@@ -0,0 +1,16 @@
export function Footer() {
return (
<footer className="border-t bg-background/50">
<div className="container py-8 px-4 md:px-8">
<div className="flex flex-col md:flex-row justify-between items-center gap-4">
<div className="text-sm text-foreground/60">
© 2026 Oh My OpenCode. All rights reserved.
</div>
<div className="text-sm text-foreground/60">
Built with for developers
</div>
</div>
</div>
</footer>
)
}
+67
View File
@@ -0,0 +1,67 @@
"use client"
import * as React from "react"
import { Link } from "@/i18n/routing"
import { useTranslations } from "next-intl"
import { ThemeToggle } from "@/components/theme-toggle"
import LanguageSwitcher from "@/components/LanguageSwitcher"
import { docsConfig } from "@/config/navigation"
import { Menu, X } from "lucide-react"
import { motion, AnimatePresence } from "framer-motion"
import { MobileNav } from "./mobile-nav"
export function Header() {
const t = useTranslations("Navigation")
const [isMobileMenuOpen, setIsMobileMenuOpen] = React.useState(false)
return (
<header className="sticky top-0 z-50 w-full border-b bg-background/95 backdrop-blur supports-[backdrop-filter]:bg-background/60">
<div className="container flex h-14 items-center px-4 md:px-8">
<div className="mr-4 hidden md:flex">
<Link href="/" className="mr-6 flex items-center space-x-2">
<span className="hidden font-bold sm:inline-block">
Oh My OpenCode
</span>
</Link>
<nav className="flex items-center space-x-6 text-sm font-medium">
{docsConfig.mainNav.map((item) => (
<Link
key={item.href}
href={item.href!}
className="transition-colors hover:text-foreground/80 text-foreground/60"
>
{item.title}
</Link>
))}
</nav>
</div>
<div className="flex flex-1 items-center justify-between space-x-2 md:justify-end">
<div className="w-full flex-1 md:w-auto md:flex-none">
</div>
<nav className="flex items-center space-x-2">
<LanguageSwitcher />
<ThemeToggle />
<button
type="button"
className="md:hidden p-2"
onClick={() => setIsMobileMenuOpen(!isMobileMenuOpen)}
aria-label="Toggle menu"
>
{isMobileMenuOpen ? <X size={20} /> : <Menu size={20} />}
</button>
</nav>
</div>
</div>
<AnimatePresence>
{isMobileMenuOpen && (
<MobileNav
open={isMobileMenuOpen}
setOpen={setIsMobileMenuOpen}
/>
)}
</AnimatePresence>
</header>
)
}
+4
View File
@@ -0,0 +1,4 @@
export { Header } from "./header"
export { Sidebar } from "./sidebar"
export { Footer } from "./footer"
export { MobileNav } from "./mobile-nav"
@@ -0,0 +1,67 @@
"use client"
import * as React from "react"
import { Link } from "@/i18n/routing"
import { docsConfig } from "@/config/navigation"
import { motion } from "framer-motion"
import { usePathname } from "next/navigation"
import { cn } from "@/lib/utils"
interface MobileNavProps {
open: boolean
setOpen: (open: boolean) => void
}
export function MobileNav({ open, setOpen }: MobileNavProps) {
const pathname = usePathname()
React.useEffect(() => {
if (pathname) {
setOpen(false)
}
}, [pathname, setOpen])
return (
<motion.div
initial={{ opacity: 0, y: -20 }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, y: -20 }}
transition={{ duration: 0.2 }}
className="fixed inset-0 top-14 z-50 grid h-[calc(100vh-3.5rem)] grid-flow-row auto-rows-max overflow-auto p-6 pb-32 shadow-md md:hidden bg-background"
>
<div className="relative z-20 grid gap-6 rounded-md bg-popover p-4 text-popover-foreground shadow-md">
<Link href="/" className="flex items-center space-x-2">
<span className="font-bold">Oh My OpenCode</span>
</Link>
<nav className="grid grid-flow-row auto-rows-max text-sm">
{docsConfig.mainNav.map((item) => (
<Link
key={item.href}
href={item.disabled ? "#" : item.href!}
className={cn(
"flex w-full items-center rounded-md p-2 text-sm font-medium hover:underline",
item.disabled && "cursor-not-allowed opacity-60"
)}
>
{item.title}
</Link>
))}
{docsConfig.sidebarNav.map((item) => (
<div key={item.title} className="flex flex-col space-y-3 pt-6">
<h4 className="font-medium">{item.title}</h4>
{item.items?.map((subItem) => (
<Link
key={subItem.href}
href={subItem.href!}
className="text-muted-foreground hover:text-foreground"
>
{subItem.title}
</Link>
))}
</div>
))}
</nav>
</div>
</motion.div>
)
}
+39
View File
@@ -0,0 +1,39 @@
"use client"
import { usePathname } from "next/navigation"
import { Link } from "@/i18n/routing"
import { docsConfig } from "@/config/navigation"
export function Sidebar() {
const pathname = usePathname()
return (
<aside className="hidden md:block w-64 border-r bg-background/50">
<nav className="p-4 space-y-6">
{docsConfig.sidebarNav.map((section) => (
<div key={section.title} className="space-y-3">
<h4 className="font-semibold text-sm text-foreground/80">
{section.title}
</h4>
<ul className="space-y-2">
{section.items?.map((item) => (
<li key={item.href}>
<Link
href={item.href!}
className={`text-sm transition-colors block py-1 px-2 rounded ${
pathname === item.href
? "font-semibold text-foreground bg-accent/10"
: "text-foreground/60 hover:text-foreground/80"
}`}
>
{item.title}
</Link>
</li>
))}
</ul>
</div>
))}
</nav>
</aside>
)
}
+88
View File
@@ -0,0 +1,88 @@
export type NavItem = {
title: string
href?: string
disabled?: boolean
external?: boolean
label?: string
items?: NavItem[]
}
export type MainNavItem = NavItem
export type SidebarNavItem = NavItem
export interface DocsConfig {
mainNav: MainNavItem[]
sidebarNav: SidebarNavItem[]
}
export const docsConfig: DocsConfig = {
mainNav: [
{
title: "Documentation",
href: "/docs",
},
{
title: "GitHub",
href: "https://github.com/code-yeongyu/oh-my-opencode",
external: true,
},
],
sidebarNav: [
{
title: "Getting Started",
items: [
{
title: "Introduction",
href: "/docs",
items: [],
},
{
title: "Installation",
href: "/docs/installation",
items: [],
},
],
},
{
title: "Configuration",
items: [
{
title: "Overview",
href: "/docs/config",
items: [],
},
{
title: "Reference",
href: "/docs/config/reference",
items: [],
},
],
},
{
title: "Core Concepts",
items: [
{
title: "Agents",
href: "/docs/agents",
items: [],
},
{
title: "Skills",
href: "/docs/skills",
items: [],
},
{
title: "Hooks",
href: "/docs/hooks",
items: [],
},
{
title: "Tools",
href: "/docs/tools",
items: [],
},
],
},
],
}