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:
parent
894a0fa849
commit
58459e692b
@ -7,6 +7,7 @@
|
|||||||
"dependencies": {
|
"dependencies": {
|
||||||
"class-variance-authority": "^0.7.1",
|
"class-variance-authority": "^0.7.1",
|
||||||
"clsx": "^2.1.1",
|
"clsx": "^2.1.1",
|
||||||
|
"framer-motion": "^12.29.0",
|
||||||
"lucide-react": "^0.563.0",
|
"lucide-react": "^0.563.0",
|
||||||
"next": "16.1.4",
|
"next": "16.1.4",
|
||||||
"next-intl": "^4.7.0",
|
"next-intl": "^4.7.0",
|
||||||
@ -1129,6 +1130,8 @@
|
|||||||
|
|
||||||
"forwarded": ["forwarded@0.2.0", "", {}, "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow=="],
|
"forwarded": ["forwarded@0.2.0", "", {}, "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow=="],
|
||||||
|
|
||||||
|
"framer-motion": ["framer-motion@12.29.0", "", { "dependencies": { "motion-dom": "^12.29.0", "motion-utils": "^12.27.2", "tslib": "^2.4.0" }, "peerDependencies": { "@emotion/is-prop-valid": "*", "react": "^18.0.0 || ^19.0.0", "react-dom": "^18.0.0 || ^19.0.0" }, "optionalPeers": ["@emotion/is-prop-valid", "react", "react-dom"] }, "sha512-1gEFGXHYV2BD42ZPTFmSU9buehppU+bCuOnHU0AD18DKh9j4DuTx47MvqY5ax+NNWRtK32qIcJf1UxKo1WwjWg=="],
|
||||||
|
|
||||||
"fresh": ["fresh@2.0.0", "", {}, "sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A=="],
|
"fresh": ["fresh@2.0.0", "", {}, "sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A=="],
|
||||||
|
|
||||||
"fs.realpath": ["fs.realpath@1.0.0", "", {}, "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw=="],
|
"fs.realpath": ["fs.realpath@1.0.0", "", {}, "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw=="],
|
||||||
@ -1387,6 +1390,10 @@
|
|||||||
|
|
||||||
"mnemonist": ["mnemonist@0.38.3", "", { "dependencies": { "obliterator": "^1.6.1" } }, "sha512-2K9QYubXx/NAjv4VLq1d1Ly8pWNC5L3BrixtdkyTegXWJIqY+zLNDhhX/A+ZwWt70tB1S8H4BE8FLYEFyNoOBw=="],
|
"mnemonist": ["mnemonist@0.38.3", "", { "dependencies": { "obliterator": "^1.6.1" } }, "sha512-2K9QYubXx/NAjv4VLq1d1Ly8pWNC5L3BrixtdkyTegXWJIqY+zLNDhhX/A+ZwWt70tB1S8H4BE8FLYEFyNoOBw=="],
|
||||||
|
|
||||||
|
"motion-dom": ["motion-dom@12.29.0", "", { "dependencies": { "motion-utils": "^12.27.2" } }, "sha512-3eiz9bb32yvY8Q6XNM4AwkSOBPgU//EIKTZwsSWgA9uzbPBhZJeScCVcBuwwYVqhfamewpv7ZNmVKTGp5qnzkA=="],
|
||||||
|
|
||||||
|
"motion-utils": ["motion-utils@12.27.2", "", {}, "sha512-B55gcoL85Mcdt2IEStY5EEAsrMSVE2sI14xQ/uAdPL+mfQxhKKFaEag9JmfxedJOR4vZpBGoPeC/Gm13I/4g5Q=="],
|
||||||
|
|
||||||
"ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="],
|
"ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="],
|
||||||
|
|
||||||
"nanoid": ["nanoid@3.3.11", "", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w=="],
|
"nanoid": ["nanoid@3.3.11", "", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w=="],
|
||||||
|
|||||||
@ -13,6 +13,7 @@
|
|||||||
"dependencies": {
|
"dependencies": {
|
||||||
"class-variance-authority": "^0.7.1",
|
"class-variance-authority": "^0.7.1",
|
||||||
"clsx": "^2.1.1",
|
"clsx": "^2.1.1",
|
||||||
|
"framer-motion": "^12.29.0",
|
||||||
"lucide-react": "^0.563.0",
|
"lucide-react": "^0.563.0",
|
||||||
"next": "16.1.4",
|
"next": "16.1.4",
|
||||||
"next-intl": "^4.7.0",
|
"next-intl": "^4.7.0",
|
||||||
|
|||||||
@ -6,6 +6,7 @@ import { NextIntlClientProvider } from 'next-intl';
|
|||||||
import { getMessages } from 'next-intl/server';
|
import { getMessages } from 'next-intl/server';
|
||||||
import { notFound } from 'next/navigation';
|
import { notFound } from 'next/navigation';
|
||||||
import { routing } from '@/i18n/routing';
|
import { routing } from '@/i18n/routing';
|
||||||
|
import { Header, Sidebar, Footer } from '@/components/layout';
|
||||||
|
|
||||||
const geistSans = Geist({
|
const geistSans = Geist({
|
||||||
variable: "--font-geist-sans",
|
variable: "--font-geist-sans",
|
||||||
@ -49,7 +50,14 @@ export default async function RootLayout({
|
|||||||
enableSystem
|
enableSystem
|
||||||
disableTransitionOnChange
|
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>
|
</ThemeProvider>
|
||||||
</NextIntlClientProvider>
|
</NextIntlClientProvider>
|
||||||
</body>
|
</body>
|
||||||
|
|||||||
16
website/src/components/layout/footer.tsx
Normal file
16
website/src/components/layout/footer.tsx
Normal 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
website/src/components/layout/header.tsx
Normal file
67
website/src/components/layout/header.tsx
Normal 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
website/src/components/layout/index.ts
Normal file
4
website/src/components/layout/index.ts
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
export { Header } from "./header"
|
||||||
|
export { Sidebar } from "./sidebar"
|
||||||
|
export { Footer } from "./footer"
|
||||||
|
export { MobileNav } from "./mobile-nav"
|
||||||
67
website/src/components/layout/mobile-nav.tsx
Normal file
67
website/src/components/layout/mobile-nav.tsx
Normal file
@ -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
website/src/components/layout/sidebar.tsx
Normal file
39
website/src/components/layout/sidebar.tsx
Normal 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
website/src/config/navigation.ts
Normal file
88
website/src/config/navigation.ts
Normal 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: [],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}
|
||||||
Loading…
x
Reference in New Issue
Block a user