diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 000000000..429e43e56 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,126 @@ +# CLAUDE.md + +This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. + +## Repository Overview +This is the Optimism Documentation website repository that powers docs.optimism.io - the official technical documentation for the Optimism Collective, covering the OP Stack, Superchain, and interoperability features. + +## Tech Stack +- **Framework**: Next.js 14.2.21 with React 18.2.0 +- **Documentation Engine**: Nextra 2.13.2 (docs theme) +- **Language**: TypeScript +- **Package Manager**: pnpm (required - do not use npm or yarn) +- **Content Format**: MDX (Markdown with React components) +- **Deployment**: Netlify + +## Essential Commands + +### Development +```bash +pnpm dev # Start development server at localhost:3001 +pnpm build # Create production build +``` + +### Quality Checks (Run before committing) +```bash +pnpm lint # Run all linting (ESLint + spellcheck + breadcrumbs + redirects + metadata) +pnpm fix # Auto-fix all fixable issues (runs on pre-push automatically) +``` + +### Individual Linting Commands +```bash +pnpm spellcheck:lint # Check spelling +pnpm spellcheck:fix # Add words to dictionary (words.txt) +pnpm lint:eslint # Run ESLint +pnpm lint:breadcrumbs # Validate breadcrumb structure +pnpm lint:redirects # Check redirect configuration +pnpm lint:metadata # Validate page metadata +``` + +## Architecture & Structure + +### Content Organization +``` +pages/ # All documentation content (MDX files) +├── app-developers/ # Application developer guides +├── operators/ # Node & chain operator documentation +├── stack/ # OP Stack protocol documentation +├── superchain/ # Superchain network documentation +├── interop/ # Interoperability documentation +└── connect/ # Contributing guides and resources +``` + +### Key Directories +- `components/`: Reusable React components for documentation +- `public/`: Static assets, images, and tutorial files +- `utils/`: Utility scripts for linting, validation, and build processes +- `providers/`: React context providers for global state + +## Important Patterns + +### MDX Page Structure +All documentation pages use MDX format with frontmatter metadata: +```mdx +--- +title: Page Title +lang: en-US +description: Page description for SEO +--- + +import { ComponentName } from '@/components/ComponentName' + +# Content here... +``` + +### Component Imports +Use the configured path alias for component imports: +```typescript +import { ComponentName } from '@/components/ComponentName' +``` + +### Adding New Documentation +1. Create MDX file in appropriate `pages/` subdirectory +2. Include required frontmatter (title, lang, description) +3. Run `pnpm lint` to validate metadata and content +4. Use existing components from `components/` directory when possible + +### Spell Checking +- Custom dictionary maintained in `words.txt` +- Add technical terms using `pnpm spellcheck:fix` +- Spell checking runs automatically in the lint pipeline + +## Git Workflow +- **Pre-push hook**: Automatically runs `pnpm fix` via Husky +- **Auto-commit**: Fixes are automatically committed if changes are made +- **No pre-commit hooks**: Only validation on push + +## Special Features +- **Kapa.ai Widget**: AI assistant integrated for documentation queries +- **Algolia Search**: Full-text search across documentation +- **Feelback**: User feedback collection system +- **Growth Book**: A/B testing framework for feature experiments + +## Common Tasks + +### Adding a New Page +1. Create `.mdx` file in appropriate `pages/` directory +2. Add frontmatter with title, lang, and description +3. Write content using Markdown and import React components as needed +4. Run `pnpm dev` to preview +5. Run `pnpm lint` before committing + +### Updating Components +- Components are in `components/` directory +- Follow existing patterns and TypeScript types +- Test component changes across multiple pages that use them + +### Working with Images +- Place images in `public/img/` directory +- Reference using `/img/filename.ext` in MDX files +- Optimize images before adding to repository + +## Notes +- The repository uses automated quality checks - always run `pnpm lint` before pushing +- Netlify handles deployment automatically on merge to main +- TypeScript is configured with relaxed strict mode - follow existing patterns +- MDX allows mixing Markdown with React components - leverage this for interactive content \ No newline at end of file diff --git a/components/AskAIButton.tsx b/components/AskAIButton.tsx index 344e20d17..1a6b13c0d 100644 --- a/components/AskAIButton.tsx +++ b/components/AskAIButton.tsx @@ -2,7 +2,13 @@ import { RiSparkling2Fill } from '@remixicon/react'; import { useFeature } from '@growthbook/growthbook-react'; import { useEffect, useState } from 'react'; -const AskAIButton = () => { +interface AskAIButtonProps { + fullWidth?: boolean; + large?: boolean; + id?: string; +} + +const AskAIButton = ({ fullWidth = false, large = false, id = 'custom-ask-ai-button' }: AskAIButtonProps) => { const [mounted, setMounted] = useState(false); const enableDocsAIWidget = useFeature('enable_docs_ai_widget').on; @@ -19,10 +25,17 @@ const AskAIButton = () => { return null; } + const baseClasses = 'nx-flex nx-gap-2 nx-items-center nx-rounded-lg nx-font-semibold nx-justify-center'; + const sizeClasses = large + ? 'nx-py-3 nx-px-6 nx-text-base' + : 'nx-py-1.5 nx-px-3 nx-text-sm'; + const widthClasses = fullWidth ? 'nx-w-full' : ''; + const iconSize = large ? 16 : 14; + return ( ); }; diff --git a/components/CustomHeader/index.tsx b/components/CustomHeader/index.tsx new file mode 100644 index 000000000..997853ba0 --- /dev/null +++ b/components/CustomHeader/index.tsx @@ -0,0 +1,273 @@ +/** @format */ + +import React, { useState, useEffect } from 'react'; +import Link from 'next/link'; +import { useRouter } from 'next/router'; +import { Search } from '../Search'; +import { AskAIButton } from '../AskAIButton'; +import { useTheme } from 'nextra-theme-docs'; +import { MoonIcon, SunIcon, Bars3Icon, XMarkIcon } from '@heroicons/react/24/outline'; + +const CustomHeader = () => { + const router = useRouter(); + const { theme, setTheme } = useTheme(); + const [mounted, setMounted] = useState(false); + const [isMobileMenuOpen, setIsMobileMenuOpen] = useState(false); + const [isDesktop, setIsDesktop] = useState(false); + const [isNavigating, setIsNavigating] = useState(false); + + useEffect(() => { + setMounted(true); + + const checkScreenSize = () => { + setIsDesktop(window.innerWidth >= 768); + }; + + checkScreenSize(); + window.addEventListener('resize', checkScreenSize); + + return () => window.removeEventListener('resize', checkScreenSize); + }, []); + + useEffect(() => { + const handleStart = () => setIsNavigating(true); + const handleComplete = () => setIsNavigating(false); + + router.events.on('routeChangeStart', handleStart); + router.events.on('routeChangeComplete', handleComplete); + router.events.on('routeChangeError', handleComplete); + + return () => { + router.events.off('routeChangeStart', handleStart); + router.events.off('routeChangeComplete', handleComplete); + router.events.off('routeChangeError', handleComplete); + }; + }, [router]); + + useEffect(() => { + const handleEscape = (e: KeyboardEvent) => { + if (e.key === 'Escape') { + setIsMobileMenuOpen(false); + } + }; + + if (isMobileMenuOpen) { + document.addEventListener('keydown', handleEscape); + document.body.style.overflow = 'hidden'; + return () => { + document.removeEventListener('keydown', handleEscape); + document.body.style.overflow = ''; + }; + } else { + document.body.style.overflow = ''; + } + }, [isMobileMenuOpen]); + + const navItems = [ + { title: 'Get started', href: '/get-started/superchain' }, + { title: 'Superchain', href: '/superchain/superchain-explainer' }, + { title: 'Interoperability', href: '/interop/get-started' }, + { title: 'App Devs', href: '/app-developers/get-started' }, + { title: 'Operators', href: '/operators/chain-operators/architecture' }, + { title: 'OP Stack', href: '/stack/getting-started' } + ]; + + const isActive = (href: string) => { + return router.pathname.startsWith(href); + }; + + const handleMouseEnter = (href: string) => { + router.prefetch(href); + }; + + if (!mounted) return null; + + return ( +
+ {/* Top Section: Logo, Search, Actions */} +
+ {/* Logo */} + + + + + + + + {/* Search Bar - hidden on mobile, shown on larger screens */} + {isDesktop && ( +
+ +
+ )} + + {/* Right side: Ask AI + Theme Toggle + Mobile Menu Button */} +
+ +
+ +
+ + {/* Mobile menu button - only visible on mobile */} + {!isDesktop && ( + + )} +
+
+ + {/* Mobile search bar - only visible on mobile */} + {!isDesktop && ( +
+ +
+ )} + + {/* Desktop Navigation */} + {isDesktop && ( + + )} + + {/* Mobile Full-Screen Menu Overlay */} + {!isDesktop && ( +
+ {/* Mobile Menu Header */} +
+ setIsMobileMenuOpen(false)} + className='nx-flex nx-items-center' + > + + + + + + +
+ + {/* Mobile Menu Content - Scrollable */} +
+ {/* Navigation Links */} + + + {/* Mobile Actions - Bottom of menu */} +
+
+ +
+
+
+
+ )} +
+ ); +}; + +export default CustomHeader; diff --git a/components/HomeCard.tsx b/components/HomeCard.tsx new file mode 100644 index 000000000..2d040e020 --- /dev/null +++ b/components/HomeCard.tsx @@ -0,0 +1,134 @@ +import React from 'react'; + +interface CardListItemProps { + number?: string; + title: string; + description?: string; + href: string; + badge?: { + text: string; + variant: 'easy' | 'medium' | 'hard'; + }; +} + +export function CardListItem({ + number, + title, + description, + href, + badge +}: CardListItemProps) { + return ( + + {number && ( +
+ {number} +
+ )} +
+
+

{title}

+ {badge && ( + + {badge.text} + + )} +
+ {description && ( +

{description}

+ )} +
+
+ + + +
+
+ ); +} + +interface CardListProps { + children: React.ReactNode; +} + +export function CardList({ children }: CardListProps) { + return ( +
+ {children} +
+ ); +} + +interface HomeCardProps { + title: string; + content?: React.ReactNode + className?: string; + footerLink?: { + text: string; + href: string; + }; +} + +export function HomeCard({ + title, + content, + className = '', + footerLink +}: HomeCardProps) { + return ( +
+ +
+

{title}

+ {content} +
+ + {footerLink && ( +
+ + {footerLink.text} → + +
+ )} +
+ ); +} + +interface HomeCardsProps { + children: React.ReactNode; + layout?: 'equal' | 'unequal'; + columns?: string; + gap?: string; +} + +export function HomeCards({ + children, + layout = 'equal', + columns, + gap = '2rem' +}: HomeCardsProps) { + const getGridColumns = () => { + if (columns) return columns; + + switch (layout) { + case 'equal': return '1fr 1fr'; + case 'unequal': return '1fr 2fr'; // 2:1 ratio + default: return '1fr 1fr'; + } + }; + + return ( +
+ {children} +
+ ); +} \ No newline at end of file diff --git a/components/LoadingBar/index.tsx b/components/LoadingBar/index.tsx new file mode 100644 index 000000000..33de15ac7 --- /dev/null +++ b/components/LoadingBar/index.tsx @@ -0,0 +1,55 @@ +import React, { useState, useEffect } from 'react'; +import { useRouter } from 'next/router'; + +const LoadingBar = () => { + const [loading, setLoading] = useState(false); + const [progress, setProgress] = useState(0); + const router = useRouter(); + + useEffect(() => { + let progressTimer: NodeJS.Timeout; + let finishTimer: NodeJS.Timeout; + + const handleStart = () => { + setLoading(true); + setProgress(0); + + progressTimer = setTimeout(() => { + setProgress(70); + }, 200); + }; + + const handleComplete = () => { + setProgress(100); + finishTimer = setTimeout(() => { + setLoading(false); + setProgress(0); + }, 300); + }; + + router.events.on('routeChangeStart', handleStart); + router.events.on('routeChangeComplete', handleComplete); + router.events.on('routeChangeError', handleComplete); + + return () => { + router.events.off('routeChangeStart', handleStart); + router.events.off('routeChangeComplete', handleComplete); + router.events.off('routeChangeError', handleComplete); + clearTimeout(progressTimer); + clearTimeout(finishTimer); + }; + }, [router]); + + if (!loading) return null; + + return ( +
+
+
+ ); +}; + +export default LoadingBar; \ No newline at end of file diff --git a/components/Search/docsearch.tsx b/components/Search/docsearch.tsx index 902a5c34c..1687b6808 100644 --- a/components/Search/docsearch.tsx +++ b/components/Search/docsearch.tsx @@ -1,26 +1,16 @@ -import { Transition } from "@headlessui/react"; -import cn from "clsx"; -import { useRouter } from "next/router"; -import { useMounted } from "nextra/hooks"; -import { InformationCircleIcon, SpinnerIcon } from "nextra/icons"; -import type { - ReactNode, - CompositionEvent, - KeyboardEvent, - ReactElement, -} from "react"; -import { - Fragment, - useCallback, - useEffect, - useRef, - useState, - useContext, -} from "react"; -import { Input } from "./input"; -import Link from "next/link"; -import * as aa from "search-insights"; -import AlgoliaContext from "@/utils/contexts/AlgoliaContext"; +/** @format */ + +import { Transition } from '@headlessui/react'; +import cn from 'clsx'; +import { useRouter } from 'next/router'; +import { useMounted } from 'nextra/hooks'; +import { InformationCircleIcon, SpinnerIcon } from 'nextra/icons'; +import type { ReactNode, CompositionEvent, KeyboardEvent, ReactElement } from 'react'; +import { Fragment, useCallback, useEffect, useRef, useState, useContext } from 'react'; +import { Input } from './input'; +import Link from 'next/link'; +import * as aa from 'search-insights'; +import AlgoliaContext from '@/utils/contexts/AlgoliaContext'; type SearchResult = { children: ReactNode; @@ -40,7 +30,7 @@ type SearchProps = { results: SearchResult[]; }; -const INPUTS = ["input", "select", "button", "textarea"]; +const INPUTS = ['input', 'select', 'button', 'textarea']; export function DocSearch({ className, @@ -50,7 +40,7 @@ export function DocSearch({ onActive, loading, error, - results, + results }: SearchProps): ReactElement { const [show, setShow] = useState(false); const [active, setActive] = useState(0); @@ -78,22 +68,21 @@ export function DocSearch({ ) return; if ( - e.key === "/" || - (e.key === "k" && - (e.metaKey /* for Mac */ || /* for non-Mac */ e.ctrlKey)) + e.key === '/' || + (e.key === 'k' && (e.metaKey /* for Mac */ || /* for non-Mac */ e.ctrlKey)) ) { e.preventDefault(); // prevent scrolling to the top input.current.focus({ preventScroll: true }); - } else if (e.key === "Escape") { + } else if (e.key === 'Escape') { setShow(false); input.current.blur(); } }; - window.addEventListener("keydown", down); + window.addEventListener('keydown', down); return () => { - window.removeEventListener("keydown", down); + window.removeEventListener('keydown', down); }; }, []); @@ -102,31 +91,28 @@ export function DocSearch({ const result = results[i]; - aa.default("clickedObjectIDsAfterSearch", { - index: "docs", - eventName: "Search Option Clicked", + aa.default('clickedObjectIDsAfterSearch', { + index: 'docs', + eventName: 'Search Option Clicked', queryID: queryID, objectIDs: [result.id], - positions: [results.indexOf(result) + 1], + positions: [results.indexOf(result) + 1] }); setObjectID(result.id); - onChange(""); + onChange(''); setShow(false); }; - const handleActive = useCallback( - (e: { currentTarget: { dataset: DOMStringMap } }) => { - const { index } = e.currentTarget.dataset; - setActive(Number(index)); - }, - [] - ); + const handleActive = useCallback((e: { currentTarget: { dataset: DOMStringMap } }) => { + const { index } = e.currentTarget.dataset; + setActive(Number(index)); + }, []); const handleKeyDown = useCallback( function (e: KeyboardEvent) { switch (e.key) { - case "ArrowDown": { + case 'ArrowDown': { if (active + 1 < results.length) { const el = ulRef.current?.querySelector( `li:nth-of-type(${active + 2}) > a` @@ -139,7 +125,7 @@ export function DocSearch({ } break; } - case "ArrowUp": { + case 'ArrowUp': { if (active - 1 >= 0) { const el = ulRef.current?.querySelector( `li:nth-of-type(${active}) > a` @@ -152,7 +138,7 @@ export function DocSearch({ } break; } - case "Enter": { + case 'Enter': { const result = results[active]; if (result && composition) { void router.push(result.route); @@ -160,7 +146,7 @@ export function DocSearch({ } break; } - case "Escape": { + case 'Escape': { setShow(false); input.current?.blur(); break; @@ -177,57 +163,49 @@ export function DocSearch({ { - onChange(""); + onChange(''); }} > {value && focused - ? "ESC" + ? 'ESC' : mounted && - (navigator.userAgent.includes("Macintosh") ? ( + (navigator.userAgent.includes('Macintosh') ? ( <> - K + K ) : ( - "CTRL K" + 'CTRL K' ))} ); - const handleComposition = useCallback( - (e: CompositionEvent) => { - setComposition(e.type === "compositionend"); - }, - [] - ); + const handleComposition = useCallback((e: CompositionEvent) => { + setComposition(e.type === 'compositionend'); + }, []); return ( -
- {renderList && ( -
setShow(false)} - /> - )} +
+ {renderList &&
setShow(false)} />} @@ -256,41 +234,47 @@ export function DocSearch({ show={renderList} // Transition.Child is required here, otherwise popup will be still present in DOM after focus out as={Transition.Child} - leave="nx-transition-opacity nx-duration-100" - leaveFrom="nx-opacity-100" - leaveTo="nx-opacity-0" + leave='nx-transition-opacity nx-duration-100' + leaveFrom='nx-opacity-100' + leaveTo='nx-opacity-0' >
    {error ? ( - - - {"Error"} + + + {'Error'} ) : loading ? ( - - - {"Loading"} + + + {'Loading'} ) : results.length > 0 ? ( results.map(({ route, prefix, children, id }, i) => ( @@ -298,15 +282,15 @@ export function DocSearch({ {prefix}
  • )) ) : ( - + No results found. )}
- Algolia logo + Algolia logo
diff --git a/components/Search/index.tsx b/components/Search/index.tsx index f1442cf1a..230e1a1cb 100644 --- a/components/Search/index.tsx +++ b/components/Search/index.tsx @@ -1,18 +1,20 @@ -import type { Item as NormalItem } from "nextra/normalize-pages"; -import type { ReactElement } from "react"; -import { useContext, useEffect, useState } from "react"; -import { HighlightMatches } from "./highlight-matches"; -import { DocSearch } from "./docsearch"; -import algoliasearch from "algoliasearch"; -import AlgoliaContext from "@/utils/contexts/AlgoliaContext"; +/** @format */ + +import type { Item as NormalItem } from 'nextra/normalize-pages'; +import type { ReactElement } from 'react'; +import { useContext, useEffect, useState } from 'react'; +import { HighlightMatches } from './highlight-matches'; +import { DocSearch } from './docsearch'; +import algoliasearch from 'algoliasearch'; +import AlgoliaContext from '@/utils/contexts/AlgoliaContext'; // Using environment variables for Algolia configuration const client = algoliasearch( - process.env.NEXT_PUBLIC_ALGOLIA_APPLICATION_ID || "", - process.env.NEXT_PUBLIC_ALGOLIA_SEARCH_API_KEY || "" + process.env.NEXT_PUBLIC_ALGOLIA_APPLICATION_ID || '', + process.env.NEXT_PUBLIC_ALGOLIA_SEARCH_API_KEY || '' ); -const index = client.initIndex(process.env.NEXT_PUBLIC_ALGOLIA_INDEX_NAME || "docs"); +const index = client.initIndex(process.env.NEXT_PUBLIC_ALGOLIA_INDEX_NAME || 'docs'); type AlgoliaHits = { hits: AlgoliaHit[]; @@ -28,13 +30,13 @@ export type AlgoliaHit = { export function Search({ className, - directories, + directories }: Readonly<{ className?: string; directories: NormalItem[]; }>): ReactElement { // Rest of your code remains the same - const [search, setSearch] = useState(""); + const [search, setSearch] = useState(''); const [results, setResults] = useState([]); const { queryID, setQueryID } = useContext(AlgoliaContext); @@ -42,7 +44,7 @@ export function Search({ useEffect(() => { async function fetchData() { const hits: AlgoliaHits = await index.search(search, { - clickAnalytics: true, + clickAnalytics: true }); setQueryID(hits.queryID); @@ -53,11 +55,11 @@ export function Search({ children: ( <> -
+
- ), + ) })); setResults(mappedHits); } @@ -70,8 +72,8 @@ export function Search({ value={search} onChange={setSearch} className={className} - overlayClassName="nx-w-screen nx-min-h-[100px] nx-max-w-[min(calc(100vw-2rem),calc(100%+20rem))]" + overlayClassName='nx-min-h-[100px] nx-w-[600px] nx-max-w-[calc(100vw-2rem)] nx-left-1/2 -nx-translate-x-1/2' results={results} /> ); -} \ No newline at end of file +} diff --git a/package.json b/package.json index 97163bd87..9ad08ac64 100644 --- a/package.json +++ b/package.json @@ -28,6 +28,7 @@ "@feelback/react": "^0.3.4", "@growthbook/growthbook-react": "^1.3.1", "@headlessui/react": "^2.1.8", + "@heroicons/react": "^2.2.0", "@remixicon/react": "^4.6.0", "algoliasearch": "^4.23.3", "clsx": "^2.1.1", diff --git a/pages/_app.tsx b/pages/_app.tsx index b32bfeab1..8ee602e60 100644 --- a/pages/_app.tsx +++ b/pages/_app.tsx @@ -6,6 +6,7 @@ import * as gtag from '../utils/gtag'; import * as aa from 'search-insights'; import AlgoliaContext from '@/utils/contexts/AlgoliaContext'; import ScrollDispatcher from '@/components/ScrollDispatcher'; +import LoadingBar from '@/components/LoadingBar'; import { CustomGrowthBookProvider } from '../providers/GrowthbookProvider'; export default function App({ Component, pageProps }) { @@ -31,6 +32,7 @@ export default function App({ Component, pageProps }) { return ( + - } /> - } /> - } /> - - -## Builder guides - -Whether you're a developer building an app on OP Mainnet, a node operator running an OP Mainnet node, or a chain operator launching your own OP Stack chain, you'll find everything you need to get started right here. - - - } /> - - } /> - - } /> - - } /> - - -## Featured tools - -Check out these amazing tools, so you can get building with Optimism. - - - } /> - - } /> - - } /> - - } /> - - } /> - - } /> - - - -## About Optimism - -The OP Stack is the standardized, shared, and open-source development stack that makes it easy to spin up your own production-ready Layer 2 blockchain. -The Superchain is a network of OP Stack chains that share a bridging protocol, governance system, and more. -We've got you covered with these detailed guides to help you learn all about Optimism's tech stack. - - - } /> - - } /> - - } /> - +import { HomeCards, HomeCard, CardList, CardListItem } from '@/components/HomeCard' + +
+
+

Deploy a chain with the OP Stack

+

Launch your own OP Chain using standard configuration, or customize your setup.

+ +
+ + +
+
+ +
+ OP Stack deployment visualization +
+
+ + + + + + + + + } + footerLink={{ + text: "View full deployment guide", + href: "https://github.com/ethereum-optimism/docs/issues" + }} + /> + + + + + + + + } + footerLink={{ + text: "View all tutorials", + href: "https://github.com/ethereum-optimism/docs/issues" + }} + /> + + + + + + + + + + + } + /> + + + + + + + + + } + /> + diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 1af8e1d27..b4184220d 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -34,6 +34,9 @@ importers: '@headlessui/react': specifier: ^2.1.8 version: 2.1.8(react-dom@18.2.0(react@18.2.0))(react@18.2.0) + '@heroicons/react': + specifier: ^2.2.0 + version: 2.2.0(react@18.2.0) '@remixicon/react': specifier: ^4.6.0 version: 4.6.0(react@18.2.0) @@ -63,10 +66,10 @@ importers: version: 4.2.3(next@14.2.21(react-dom@18.2.0(react@18.2.0))(react@18.2.0)) nextra: specifier: 2.13.2 - version: 2.13.2(patch_hash=a4rp2hgojklggjmthmkiyqaek4)(next@14.2.21(react-dom@18.2.0(react@18.2.0))(react@18.2.0))(react-dom@18.2.0(react@18.2.0))(react@18.2.0) + version: 2.13.2(patch_hash=81936321c37741ec218dc19817c4a4939f4655b8371e793561fc236bebccc249)(next@14.2.21(react-dom@18.2.0(react@18.2.0))(react@18.2.0))(react-dom@18.2.0(react@18.2.0))(react@18.2.0) nextra-theme-docs: specifier: 2.13.2 - version: 2.13.2(next@14.2.21(react-dom@18.2.0(react@18.2.0))(react@18.2.0))(nextra@2.13.2(patch_hash=a4rp2hgojklggjmthmkiyqaek4)(next@14.2.21(react-dom@18.2.0(react@18.2.0))(react@18.2.0))(react-dom@18.2.0(react@18.2.0))(react@18.2.0))(react-dom@18.2.0(react@18.2.0))(react@18.2.0) + version: 2.13.2(next@14.2.21(react-dom@18.2.0(react@18.2.0))(react@18.2.0))(nextra@2.13.2(patch_hash=81936321c37741ec218dc19817c4a4939f4655b8371e793561fc236bebccc249)(next@14.2.21(react-dom@18.2.0(react@18.2.0))(react@18.2.0))(react-dom@18.2.0(react@18.2.0))(react@18.2.0))(react-dom@18.2.0(react@18.2.0))(react@18.2.0) react: specifier: ^18.2.0 version: 18.2.0 @@ -130,7 +133,7 @@ importers: version: 15.0.1 remark-code-import: specifier: ^1.2.0 - version: 1.2.0(patch_hash=heylvfasxh3ubj2edns2svea2m) + version: 1.2.0(patch_hash=f6b78667b2fd0da0247b6e898a35f71bfde2a83adfa62fa6017a02dc7fb5d436) remark-frontmatter: specifier: ^5.0.0 version: 5.0.0 @@ -139,7 +142,7 @@ importers: version: 3.0.1 remark-lint-frontmatter-schema: specifier: ^3.15.4 - version: 3.15.4(patch_hash=jaxvkozlhcbn7zjsiti5ocoubi) + version: 3.15.4(patch_hash=32c1574b8fd989888047ea0226d42029f451eb7c875349c28b6259a73cc7e59f) remark-lint-heading-style: specifier: ^3.1.2 version: 3.1.2 @@ -653,6 +656,11 @@ packages: react: ^18 react-dom: ^18 + '@heroicons/react@2.2.0': + resolution: {integrity: sha512-LMcepvRaS9LYHJGsF0zzmgKCUim/X3N/DQKc4jepAXJ7l8QxJ1PmxJzqplF2Z3FE4PqBAIGyJAQ/w4B5dsqbtQ==} + peerDependencies: + react: '>= 16 || ^19.0.0-rc' + '@humanwhocodes/config-array@0.11.13': resolution: {integrity: sha512-JSBDMiDKSzQVngfRjOdFXgFfklaXI4K9nLF49Auh21lmBWRLIK3+xTErTWD4KU54pb6coM6ESE7Awz/FNU3zgQ==} engines: {node: '>=10.10.0'} @@ -4718,6 +4726,10 @@ snapshots: react: 18.2.0 react-dom: 18.2.0(react@18.2.0) + '@heroicons/react@2.2.0(react@18.2.0)': + dependencies: + react: 18.2.0 + '@humanwhocodes/config-array@0.11.13': dependencies: '@humanwhocodes/object-schema': 2.0.1 @@ -7617,7 +7629,7 @@ snapshots: - '@babel/core' - babel-plugin-macros - nextra-theme-docs@2.13.2(next@14.2.21(react-dom@18.2.0(react@18.2.0))(react@18.2.0))(nextra@2.13.2(patch_hash=a4rp2hgojklggjmthmkiyqaek4)(next@14.2.21(react-dom@18.2.0(react@18.2.0))(react@18.2.0))(react-dom@18.2.0(react@18.2.0))(react@18.2.0))(react-dom@18.2.0(react@18.2.0))(react@18.2.0): + nextra-theme-docs@2.13.2(next@14.2.21(react-dom@18.2.0(react@18.2.0))(react@18.2.0))(nextra@2.13.2(patch_hash=81936321c37741ec218dc19817c4a4939f4655b8371e793561fc236bebccc249)(next@14.2.21(react-dom@18.2.0(react@18.2.0))(react@18.2.0))(react-dom@18.2.0(react@18.2.0))(react@18.2.0))(react-dom@18.2.0(react@18.2.0))(react@18.2.0): dependencies: '@headlessui/react': 1.7.17(react-dom@18.2.0(react@18.2.0))(react@18.2.0) '@popperjs/core': 2.11.8 @@ -7631,13 +7643,13 @@ snapshots: next: 14.2.21(react-dom@18.2.0(react@18.2.0))(react@18.2.0) next-seo: 6.4.0(next@14.2.21(react-dom@18.2.0(react@18.2.0))(react@18.2.0))(react-dom@18.2.0(react@18.2.0))(react@18.2.0) next-themes: 0.2.1(next@14.2.21(react-dom@18.2.0(react@18.2.0))(react@18.2.0))(react-dom@18.2.0(react@18.2.0))(react@18.2.0) - nextra: 2.13.2(patch_hash=a4rp2hgojklggjmthmkiyqaek4)(next@14.2.21(react-dom@18.2.0(react@18.2.0))(react@18.2.0))(react-dom@18.2.0(react@18.2.0))(react@18.2.0) + nextra: 2.13.2(patch_hash=81936321c37741ec218dc19817c4a4939f4655b8371e793561fc236bebccc249)(next@14.2.21(react-dom@18.2.0(react@18.2.0))(react@18.2.0))(react-dom@18.2.0(react@18.2.0))(react@18.2.0) react: 18.2.0 react-dom: 18.2.0(react@18.2.0) scroll-into-view-if-needed: 3.1.0 zod: 3.22.4 - nextra@2.13.2(patch_hash=a4rp2hgojklggjmthmkiyqaek4)(next@14.2.21(react-dom@18.2.0(react@18.2.0))(react@18.2.0))(react-dom@18.2.0(react@18.2.0))(react@18.2.0): + nextra@2.13.2(patch_hash=81936321c37741ec218dc19817c4a4939f4655b8371e793561fc236bebccc249)(next@14.2.21(react-dom@18.2.0(react@18.2.0))(react@18.2.0))(react-dom@18.2.0(react@18.2.0))(react@18.2.0): dependencies: '@headlessui/react': 1.7.17(react-dom@18.2.0(react@18.2.0))(react@18.2.0) '@mdx-js/mdx': 2.3.0 @@ -7959,7 +7971,7 @@ snapshots: hast-util-raw: 9.0.1 vfile: 6.0.1 - remark-code-import@1.2.0(patch_hash=heylvfasxh3ubj2edns2svea2m): + remark-code-import@1.2.0(patch_hash=f6b78667b2fd0da0247b6e898a35f71bfde2a83adfa62fa6017a02dc7fb5d436): dependencies: strip-indent: 4.0.0 to-gatsby-remark-plugin: 0.1.0 @@ -8032,7 +8044,7 @@ snapshots: unified: 10.1.2 unified-lint-rule: 2.1.2 - remark-lint-frontmatter-schema@3.15.4(patch_hash=jaxvkozlhcbn7zjsiti5ocoubi): + remark-lint-frontmatter-schema@3.15.4(patch_hash=32c1574b8fd989888047ea0226d42029f451eb7c875349c28b6259a73cc7e59f): dependencies: '@apidevtools/json-schema-ref-parser': 11.1.0 ajv: 8.12.0 diff --git a/public/img/cube.png b/public/img/cube.png new file mode 100644 index 000000000..48375e5cc Binary files /dev/null and b/public/img/cube.png differ diff --git a/styles/global.css b/styles/global.css index 06a25fa39..68c566995 100644 --- a/styles/global.css +++ b/styles/global.css @@ -289,4 +289,317 @@ div.footer-columns { background-color: transparent !important; --tw-shadow-color: transparent !important; box-shadow: none !important; - } \ No newline at end of file + } + +/* Hero section styles */ +.hero-section { + display: flex; + align-items: center; + justify-content: space-between; + min-height: 500px; + padding: 4rem 0; + margin-bottom: 4rem; +} + +.hero-content { + flex: 1; + max-width: 505px; +} + +.hero-content h1 { + font-size: 2rem; + font-weight: 400; + line-height: 1.1; + margin-bottom: 1.5rem; +} + +.hero-content p { + font-size: 1.125rem; + line-height: 1.6; + margin-bottom: 2rem; + color: var(--op-neutral-500); +} + +:is(html[class~=dark]) .hero-content p { + color: var(--op-neutral-500); +} + +.hero-buttons { + display: flex; + gap: 1rem; +} + +.btn-primary { + background-color: var(--op-red-500); + color: white; + padding: 0.75rem 1rem; + border: none; + border-radius: 6px; + font-weight: 400; + cursor: pointer; + transition: background-color 0.2s; +} + +.btn-primary:hover { + background-color: var(--op-red-600); +} + +.btn-secondary { + background-color: transparent; + color: var(--op-neutral-800); + padding: 0.75rem 1.5rem; + border: 1px solid var(--op-neutral-400); + border-radius: 6px; + font-weight: 400; + cursor: pointer; + transition: all 0.2s; +} + +.btn-secondary:hover { + background-color: var(--op-neutral-100); + border-color: var(--op-neutral-500); +} + +:is(html[class~=dark]) .btn-secondary { + color: white; + border-color: var(--op-neutral-600); +} + +:is(html[class~=dark]) .btn-secondary:hover { + background-color: var(--op-neutral-800); + border-color: var(--op-neutral-500); +} + +@media (max-width: 768px) { + .hero-section { + flex-direction: column; + text-align: center; + min-height: auto; + padding: 2rem 0; + } + + .hero-content { + padding-right: 0; + margin-bottom: 2rem; + } + + .hero-content h1 { + font-size: 2rem; + } + + .hero-buttons { + justify-content: center; + } +} + +/* Home Cards Component Styles */ +.home-cards { + width: 100%; + margin-bottom: 2rem; +} + +.home-card { + display: flex; + flex-direction: column; + padding: 1.5rem; + background: var(--op-neutral-50); + border: 1px solid var(--op-neutral-200); + border-radius: 8px; + transition: all 0.2s ease; +} + +:is(html[class~=dark]) .home-card { + background: var(--op-neutral-800); + border-color: var(--op-neutral-700); +} + +/* Card Content */ +.home-card .home-card__title { + font-size: 1.25rem !important; + font-weight: 400 !important; + margin: 0 0 2rem 0; + color: var(--op-neutral-900); +} + +:is(html[class~=dark]) .home-card .home-card__title { + color: var(--op-neutral-100) !important; +} + +.home-card__footer { + margin-top: 2rem; + padding-top: 1rem; +} + +:is(html[class~=dark]) .home-card__footer { + border-top-color: var(--op-neutral-700); +} + +.home-card__footer-link { + color: var(--op-neutral-600); + text-decoration: none; + font-size: 0.875rem; + font-weight: 500; + transition: font-weight 0.2s ease; +} + +.home-card__footer-link:hover { + font-weight: 600; +} + +:is(html[class~=dark]) .home-card__footer-link { + color: var(--op-neutral-400); +} + +/* Card List Components */ +.card-list { + display: flex; + flex-direction: column; + gap: 0.5rem; +} + +.card-list-item { + display: flex; + align-items: center; + gap: 1rem; + padding-bottom: 1.25rem; + border-radius: 6px; + text-decoration: none; + color: inherit; + transition: all 0.2s ease; +} + +.card-list-item:hover .card-list-item__title { + font-weight: 600; +} + +.card-list-item:hover .card-list-item__description { + color: var(--op-neutral-800); +} + +:is(html[class~=dark]) .card-list-item:hover .card-list-item__description { + color: var(--op-neutral-200); +} + +.card-list-item__number { + width: 2rem; + height: 2rem; + border-radius: 50%; + background: var(--op-neutral-100); + display: flex; + align-items: center; + justify-content: center; + font-weight: 600; + font-size: 0.875rem; + flex-shrink: 0; +} + +:is(html[class~=dark]) .card-list-item__number { + background: var(--op-neutral-700); + color: white; +} + +.card-list-item__content { + flex: 1; +} + +.card-list-item__header { + display: flex; + align-items: center; + gap: 0.75rem; + margin-bottom: 0.25rem; +} + +.card-list-item__title { + margin: 0; + font-size: 1rem; + font-weight: 400; + color: var(--op-text-900); +} + +:is(html[class~=dark]) .card-list-item__title { + color: #BCBFCD; +} + +.card-list-item__description { + margin: 0; + font-size: 0.875rem; + color: var(--op-neutral-600); + line-height: 1.4; +} + +:is(html[class~=dark]) .card-list-item__description { + color: var(--op-neutral-400); +} + +.card-list-item__badge { + padding: 0.125rem 0.5rem; + border-radius: 12px; + font-size: 0.75rem; + font-weight: 600; + text-transform: capitalize; +} + +.card-list-item__badge--easy { + background: #dcfce7; + color: #166534; +} + +.card-list-item__badge--medium { + background: #fef3c7; + color: #92400e; +} + +.card-list-item__badge--hard { + background: #fee2e2; + color: #991b1b; +} + +:is(html[class~=dark]) .card-list-item__badge--easy { + background: var(--op-yellow-green-900); + color: var(--op-green-600); +} + +:is(html[class~=dark]) .card-list-item__badge--medium { + background: var(--op-orange-900); + color: var(--op-orange-500); +} + +:is(html[class~=dark]) .card-list-item__badge--hard { + background: #991b1b; + color: #fee2e2; +} + +.card-list-item__arrow { + color: var(--op-neutral-400); + font-size: 1.25rem; + transition: transform 0.2s ease; + display: flex; + align-items: center; + justify-content: center; +} + +.card-list-item:hover .card-list-item__arrow { + transform: translateX(2px); + color: var(--op-neutral-600); +} + +:is(html[class~=dark]) .card-list-item__arrow { + color: var(--op-neutral-500); +} + +:is(html[class~=dark]) .card-list-item:hover .card-list-item__arrow { + color: var(--op-neutral-300); +} + +/* Responsive */ +@media (max-width: 768px) { + .home-cards { + grid-template-columns: 1fr !important; + } + + .card-list-item__header { + flex-direction: column; + align-items: flex-start; + gap: 0.5rem; + } +} \ No newline at end of file diff --git a/theme.config.tsx b/theme.config.tsx index 143030bac..a3798144e 100644 --- a/theme.config.tsx +++ b/theme.config.tsx @@ -9,29 +9,13 @@ import '@feelback/react/styles/feelback.css'; import { Search } from './components/Search'; import { Footer } from './components/Footer'; import { AskAIButton } from './components/AskAIButton'; +import CustomHeader from './components/CustomHeader'; import { useFeature } from '@growthbook/growthbook-react'; const config: DocsThemeConfig = { - logo: ( - <> - - - - - - ), + navbar: { + component: CustomHeader + }, darkMode: true, // banner: { // key: 'viem/op-stack', @@ -42,12 +26,6 @@ const config: DocsThemeConfig = { // // ) // }, - search: { - component: Search - }, - navbar: { - extraContent: AskAIButton - }, docsRepositoryBase: 'https://github.com/ethereum-optimism/docs/blob/main/', footer: { text: