diff --git a/website/bun.lock b/website/bun.lock index fb410d88e..322428b51 100644 --- a/website/bun.lock +++ b/website/bun.lock @@ -7,6 +7,7 @@ "dependencies": { "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", + "framer-motion": "^12.29.0", "lucide-react": "^0.563.0", "next": "16.1.4", "next-intl": "^4.7.0", @@ -1129,6 +1130,8 @@ "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=="], "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=="], + "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=="], "nanoid": ["nanoid@3.3.11", "", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w=="], diff --git a/website/package.json b/website/package.json index 78cbfbcdc..2dc0320f0 100644 --- a/website/package.json +++ b/website/package.json @@ -13,6 +13,7 @@ "dependencies": { "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", + "framer-motion": "^12.29.0", "lucide-react": "^0.563.0", "next": "16.1.4", "next-intl": "^4.7.0", diff --git a/website/src/app/[locale]/layout.tsx b/website/src/app/[locale]/layout.tsx index bcf84b94c..68fc4b063 100644 --- a/website/src/app/[locale]/layout.tsx +++ b/website/src/app/[locale]/layout.tsx @@ -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} +
+
+
+ +
{children}
+
+
diff --git a/website/src/components/layout/footer.tsx b/website/src/components/layout/footer.tsx new file mode 100644 index 000000000..46238c099 --- /dev/null +++ b/website/src/components/layout/footer.tsx @@ -0,0 +1,16 @@ +export function Footer() { + return ( + + ) +} diff --git a/website/src/components/layout/header.tsx b/website/src/components/layout/header.tsx new file mode 100644 index 000000000..367e3f12d --- /dev/null +++ b/website/src/components/layout/header.tsx @@ -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 ( +
+
+
+ + + Oh My OpenCode + + + +
+ +
+
+
+ +
+
+ + + {isMobileMenuOpen && ( + + )} + +
+ ) +} diff --git a/website/src/components/layout/index.ts b/website/src/components/layout/index.ts new file mode 100644 index 000000000..b29136328 --- /dev/null +++ b/website/src/components/layout/index.ts @@ -0,0 +1,4 @@ +export { Header } from "./header" +export { Sidebar } from "./sidebar" +export { Footer } from "./footer" +export { MobileNav } from "./mobile-nav" diff --git a/website/src/components/layout/mobile-nav.tsx b/website/src/components/layout/mobile-nav.tsx new file mode 100644 index 000000000..7f47bc900 --- /dev/null +++ b/website/src/components/layout/mobile-nav.tsx @@ -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 ( + +
+ + Oh My OpenCode + + +
+
+ ) +} diff --git a/website/src/components/layout/sidebar.tsx b/website/src/components/layout/sidebar.tsx new file mode 100644 index 000000000..21511956a --- /dev/null +++ b/website/src/components/layout/sidebar.tsx @@ -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 ( + + ) +} diff --git a/website/src/config/navigation.ts b/website/src/config/navigation.ts new file mode 100644 index 000000000..bcd5c867d --- /dev/null +++ b/website/src/config/navigation.ts @@ -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: [], + }, + ], + }, + ], +}