"use client" import * as React from "react" import { Menu, Search } from "lucide-react" import { Link } from "@/i18n/routing" import { Button } from "@/components/ui/button" import { Input } from "@/components/ui/input" import { DOC_SECTION_IDS, type DocSectionId } from "@/lib/docs-sections" type DocsShellSection = { id: DocSectionId title: string } export function DocsShell({ mobileHeader, searchPlaceholder, sections, children, }: { mobileHeader: string searchPlaceholder: string sections: DocsShellSection[] children: React.ReactNode }) { const [searchQuery, setSearchQuery] = React.useState("") const [activeSection, setActiveSection] = React.useState("overview") const [isMobileMenuOpen, setIsMobileMenuOpen] = React.useState(false) const activeSectionRef = React.useRef("overview") const findHashSectionId = React.useCallback((hash: string): DocSectionId | null => { const id = hash.replace(/^#/, "") return DOC_SECTION_IDS.find((sectionId) => sectionId === id) ?? null }, []) const scrollToSection = React.useCallback((id: DocSectionId, updateHash = true) => { const element = document.getElementById(id) if (!element) return window.scrollTo({ top: element.offsetTop - 80, behavior: "auto" }) if (updateHash && window.location.hash !== `#${id}`) { window.history.pushState(null, "", `#${id}`) } activeSectionRef.current = id setActiveSection(id) setIsMobileMenuOpen(false) }, []) React.useEffect(() => { activeSectionRef.current = activeSection }, [activeSection]) React.useEffect(() => { const scrollToHashSection = () => { const sectionId = findHashSectionId(window.location.hash) if (!sectionId) return window.requestAnimationFrame(() => scrollToSection(sectionId, false)) } scrollToHashSection() window.addEventListener("hashchange", scrollToHashSection) return () => window.removeEventListener("hashchange", scrollToHashSection) }, [findHashSectionId, scrollToSection]) const filteredSections = React.useMemo(() => { const query = searchQuery.trim().toLowerCase() if (!query) return sections return sections.filter((section) => section.title.toLowerCase().includes(query)) }, [searchQuery, sections]) React.useEffect(() => { const sectionEls = DOC_SECTION_IDS.map((id) => document.getElementById(id)) let rafId: number | null = null const handleScroll = () => { if (rafId !== null) return rafId = window.requestAnimationFrame(() => { rafId = null const scrollPosition = window.scrollY + 100 let nextActive: DocSectionId | null = null for (let i = 0; i < sectionEls.length; i++) { const el = sectionEls[i] if (!el) continue if (el.offsetTop <= scrollPosition && el.offsetTop + el.offsetHeight > scrollPosition) { nextActive = el.id as DocSectionId break } } if (nextActive && nextActive !== activeSectionRef.current) { activeSectionRef.current = nextActive setActiveSection(nextActive) } }) } window.addEventListener("scroll", handleScroll, { passive: true }) handleScroll() return () => { window.removeEventListener("scroll", handleScroll) if (rafId !== null) window.cancelAnimationFrame(rafId) } }, []) const handleDocsClick = (event: React.MouseEvent) => { if (!(event.target instanceof Element)) return const anchor = event.target.closest("a[href]") if (!(anchor instanceof HTMLAnchorElement)) return const sectionId = findHashSectionId(anchor.hash) if (!sectionId) return const href = anchor.getAttribute("href") const isSamePath = anchor.origin === window.location.origin && anchor.pathname === window.location.pathname if (!href?.startsWith("#") && !isSamePath) return event.preventDefault() scrollToSection(sectionId) } return (
{mobileHeader}
{children}
) }