chore(web): move site under packages
This commit is contained in:
@@ -0,0 +1,193 @@
|
||||
"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<DocSectionId>("overview")
|
||||
const [isMobileMenuOpen, setIsMobileMenuOpen] = React.useState(false)
|
||||
|
||||
const activeSectionRef = React.useRef<DocSectionId>("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<HTMLElement>) => {
|
||||
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 (
|
||||
<div className="bg-background text-foreground flex min-h-screen">
|
||||
<div className="bg-background/95 fixed top-0 right-0 left-0 z-50 flex items-center justify-between border-b px-4 py-3 backdrop-blur md:hidden">
|
||||
<Link href="/" className="font-bold">
|
||||
{mobileHeader}
|
||||
</Link>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
type="button"
|
||||
onClick={() => setIsMobileMenuOpen(!isMobileMenuOpen)}
|
||||
aria-label="Toggle sidebar menu"
|
||||
>
|
||||
<Menu className="h-5 w-5" />
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<aside
|
||||
className={`bg-background fixed inset-y-0 left-0 z-40 w-64 transform border-r transition-transform duration-200 ease-in-out md:translate-x-0 ${isMobileMenuOpen ? "translate-x-0" : "-translate-x-full"} pt-16`}
|
||||
>
|
||||
<div className="flex h-full flex-col">
|
||||
<div className="p-4">
|
||||
<div className="relative">
|
||||
<Search className="text-muted-foreground absolute top-2.5 left-2 h-4 w-4" />
|
||||
<Input
|
||||
placeholder={searchPlaceholder}
|
||||
className="pl-8"
|
||||
value={searchQuery}
|
||||
onChange={(e) => setSearchQuery(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<nav className="flex-1 overflow-y-auto px-2 pb-4">
|
||||
<ul className="space-y-1">
|
||||
{filteredSections.map((section) => (
|
||||
<li key={section.id}>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => scrollToSection(section.id)}
|
||||
className={`w-full rounded-md px-3 py-2 text-left text-sm font-medium transition-colors ${
|
||||
activeSection === section.id
|
||||
? "bg-primary/10 text-primary"
|
||||
: "text-muted-foreground hover:bg-muted hover:text-foreground"
|
||||
} `}
|
||||
>
|
||||
{section.title}
|
||||
</button>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</nav>
|
||||
</div>
|
||||
</aside>
|
||||
|
||||
<main
|
||||
className="flex-1 px-4 pt-20 pb-20 md:ml-64 md:px-8 md:pt-8"
|
||||
onClickCapture={handleDocsClick}
|
||||
>
|
||||
<div className="mx-auto max-w-4xl space-y-12">{children}</div>
|
||||
</main>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,140 @@
|
||||
import type * as React from "react"
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
type WithChildren = { children?: React.ReactNode; className?: string }
|
||||
|
||||
export const mdxComponents = {
|
||||
h1: ({ children, className, ...props }: WithChildren) => (
|
||||
<h1
|
||||
className={cn(
|
||||
"mt-8 mb-4 scroll-mt-24 text-4xl font-bold tracking-tight first:mt-0",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
</h1>
|
||||
),
|
||||
h2: ({ children, className, ...props }: WithChildren) => (
|
||||
<h2
|
||||
className={cn("mt-12 mb-4 scroll-mt-24 text-2xl font-semibold tracking-tight", className)}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
</h2>
|
||||
),
|
||||
h3: ({ children, className, ...props }: WithChildren) => (
|
||||
<h3
|
||||
className={cn("mt-8 mb-3 scroll-mt-24 text-xl font-semibold tracking-tight", className)}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
</h3>
|
||||
),
|
||||
h4: ({ children, className, ...props }: WithChildren) => (
|
||||
<h4
|
||||
className={cn("mt-6 mb-2 scroll-mt-24 text-lg font-semibold tracking-tight", className)}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
</h4>
|
||||
),
|
||||
p: ({ children, className, ...props }: WithChildren) => (
|
||||
<p className={cn("text-muted-foreground my-4 leading-7", className)} {...props}>
|
||||
{children}
|
||||
</p>
|
||||
),
|
||||
a: ({ children, href, className, ...props }: WithChildren & { href?: string }) => (
|
||||
<a
|
||||
href={href}
|
||||
className={cn("text-primary font-medium underline underline-offset-4", className)}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
</a>
|
||||
),
|
||||
ul: ({ children, className, ...props }: WithChildren) => (
|
||||
<ul className={cn("text-muted-foreground my-4 ml-6 list-disc space-y-1", className)} {...props}>
|
||||
{children}
|
||||
</ul>
|
||||
),
|
||||
ol: ({ children, className, ...props }: WithChildren) => (
|
||||
<ol
|
||||
className={cn("text-muted-foreground my-4 ml-6 list-decimal space-y-1", className)}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
</ol>
|
||||
),
|
||||
li: ({ children, className, ...props }: WithChildren) => (
|
||||
<li className={cn("leading-7", className)} {...props}>
|
||||
{children}
|
||||
</li>
|
||||
),
|
||||
blockquote: ({ children, className, ...props }: WithChildren) => (
|
||||
<blockquote
|
||||
className={cn("border-primary/40 my-6 border-l-4 pl-4 italic", className)}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
</blockquote>
|
||||
),
|
||||
code: ({ children, className, ...props }: WithChildren) => (
|
||||
<code
|
||||
className={cn("bg-muted text-foreground rounded px-1.5 py-0.5 font-mono text-sm", className)}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
</code>
|
||||
),
|
||||
pre: ({ children, className, ...props }: WithChildren) => (
|
||||
<pre
|
||||
className={cn(
|
||||
"border-border/50 my-4 overflow-x-auto rounded-lg border bg-[#1e1e2e] p-4 font-mono text-sm text-[#cdd6f4] shadow-sm [&_code]:bg-transparent [&_code]:p-0 [&_code]:text-inherit",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
</pre>
|
||||
),
|
||||
table: ({ children, className, ...props }: WithChildren) => (
|
||||
<div className="my-6 overflow-x-auto">
|
||||
<table
|
||||
className={cn("border-border w-full border-collapse border text-sm", className)}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
</table>
|
||||
</div>
|
||||
),
|
||||
thead: ({ children, className, ...props }: WithChildren) => (
|
||||
<thead className={cn("bg-muted", className)} {...props}>
|
||||
{children}
|
||||
</thead>
|
||||
),
|
||||
th: ({ children, className, ...props }: WithChildren) => (
|
||||
<th
|
||||
className={cn("border-border border px-3 py-2 text-left font-semibold", className)}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
</th>
|
||||
),
|
||||
td: ({ children, className, ...props }: WithChildren) => (
|
||||
<td
|
||||
className={cn("border-border text-muted-foreground border px-3 py-2", className)}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
</td>
|
||||
),
|
||||
hr: ({ className, ...props }: { className?: string }) => (
|
||||
<hr className={cn("border-border my-8", className)} {...props} />
|
||||
),
|
||||
strong: ({ children, className, ...props }: WithChildren) => (
|
||||
<strong className={cn("text-foreground font-semibold", className)} {...props}>
|
||||
{children}
|
||||
</strong>
|
||||
),
|
||||
}
|
||||
Reference in New Issue
Block a user