chore(web): move site under packages

This commit is contained in:
YeonGyu-Kim
2026-05-18 16:44:02 +09:00
parent 5e9a26c1c5
commit bf96794599
82 changed files with 27 additions and 27 deletions
+193
View File
@@ -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>
),
}