Claude Code · 모름 · React (TSX)
결과 코드펼치기
import { useMemo, useState } from "react";
const FONT_URL =
"https://fonts.googleapis.com/css2?family=Gothic+A1:wght@400;500;600;700;800&family=Inter:wght@400;500;600;700&display=swap";
type Node = { id: string; label: string; children?: Node[] };
type Section = { title: string; items: Node[] };
const SECTIONS: Section[] = [
{
title: "Documentation",
items: [
{
id: "1",
label: "1. Getting Started",
children: [
{ id: "1.1", label: "1.1 Introduction" },
{
id: "1.2",
label: "1.2 Installation",
children: [
{ id: "1.2.1", label: "1.2.1 macOS & Linux" },
{ id: "1.2.2", label: "1.2.2 Windows Setup" },
{ id: "1.2.3", label: "1.2.3 Docker Strategy" },
],
},
{
id: "1.3",
label: "1.3 Basic Configuration",
children: [
{ id: "1.3.1", label: "1.3.1 Environment Variables" },
{ id: "1.3.2", label: "1.3.2 Config File" },
],
},
],
},
],
},
{
title: "Advanced Guides",
items: [
{
id: "2",
label: "2. Architecture Overview",
children: [
{ id: "2.1", label: "2.1 Core Modules" },
{ id: "2.2", label: "2.2 Data Flow" },
],
},
{
id: "3",
label: "3. API Reference",
children: [
{ id: "3.1", label: "3.1 Endpoints" },
{ id: "3.2", label: "3.2 Error Codes" },
],
},
{ id: "4", label: "4. Release Notes" },
{
id: "5",
label: "5. Troubleshooting",
children: [
{ id: "5.1", label: "5.1 Network Latency" },
{
id: "5.2",
label: "5.2 Permission Errors",
children: [
{ id: "5.2.1", label: "5.2.1 Root Access" },
{ id: "5.2.2", label: "5.2.2 Keyring Issues" },
],
},
],
},
],
},
];
const INDENT = ["pl-3", "pl-8", "pl-[52px]"];
function filterTree(nodes: Node[], q: string): Node[] {
if (!q) return nodes;
const out: Node[] = [];
for (const n of nodes) {
const kids = n.children ? filterTree(n.children, q) : [];
if (n.label.toLowerCase().includes(q) || kids.length > 0) {
out.push({ ...n, children: n.children ? (kids.length > 0 ? kids : n.children) : undefined });
}
}
return out;
}
function Chevron({ open }: { open: boolean }) {
return (
<svg
width="12"
height="12"
viewBox="0 0 24 24"
fill="none"
stroke="#9ca3af"
strokeWidth="2.5"
strokeLinecap="round"
strokeLinejoin="round"
className={`transition-transform duration-150 ${open ? "rotate-90" : ""}`}
aria-hidden="true"
>
<path d="m9 18 6-6-6-6" />
</svg>
);
}
export default function DocsTreeSidebar() {
const [expanded, setExpanded] = useState<Set<string>>(new Set(["1", "1.2", "5", "5.2"]));
const [active, setActive] = useState("1.2.2");
const [query, setQuery] = useState("");
const q = query.trim().toLowerCase();
const sections = useMemo(
() => SECTIONS.map((s) => ({ ...s, items: filterTree(s.items, q) })).filter((s) => s.items.length > 0),
[q],
);
const toggle = (id: string) =>
setExpanded((prev) => {
const next = new Set(prev);
if (next.has(id)) next.delete(id);
else next.add(id);
return next;
});
const renderNodes = (nodes: Node[], depth: number) =>
nodes.map((n) => {
const hasKids = !!n.children?.length;
const isOpen = hasKids && (expanded.has(n.id) || q.length > 0);
const isActive = active === n.id;
const weight = isActive ? "font-semibold" : depth === 0 ? "font-medium" : "font-normal";
const color = isActive ? "text-[#2563eb]" : depth === 2 ? "text-[#4b5563]" : "text-[#111827]";
return (
<li key={n.id} role="treeitem" aria-expanded={hasKids ? isOpen : undefined} aria-selected={isActive}>
<button
type="button"
onClick={() => {
if (hasKids) toggle(n.id);
else setActive(n.id);
}}
className={`flex h-9 w-full items-center gap-2 rounded-md pr-3 text-left font-['Inter',sans-serif] text-[14px] leading-normal transition-colors duration-150 focus-visible:outline-2 focus-visible:outline-offset-[-2px] focus-visible:outline-[#2563eb] ${INDENT[depth]} ${
isActive ? "bg-[#eff6ff]" : "hover:bg-[#f9fafb]"
}`}
>
<span className="flex size-5 shrink-0 items-center justify-center">
{hasKids ? <Chevron open={isOpen} /> : <span className={`size-1.5 rounded-full ${isActive ? "bg-[#2563eb]" : "bg-[#d1d5db]"}`} />}
</span>
<span className={`min-w-0 flex-1 truncate ${weight} ${color}`}>{n.label}</span>
{isActive && <span className="h-4 w-[3px] shrink-0 rounded-full bg-[#2563eb]" aria-hidden="true" />}
</button>
{isOpen && (
<ul role="group" className="flex flex-col gap-px">
{renderNodes(n.children!, Math.min(depth + 1, 2))}
</ul>
)}
</li>
);
});
return (
<section className="w-full bg-[#f5f5f5] px-4 py-10 sm:px-6">
<link rel="stylesheet" href={FONT_URL} precedence="default" />
<div className="mx-auto flex w-full max-w-[320px] flex-col sm:max-w-[300px]">
<div className="flex min-h-[920px] flex-col justify-between rounded-2xl border border-[#e5e7eb] bg-[#fefeff] p-3 drop-shadow-[0px_4px_6px_rgba(0,0,0,0.03)]">
<div className="flex flex-col gap-5 p-2.5">
<label className="flex items-center gap-2.5 rounded-lg border border-[#e5e7eb] bg-white p-2.5 focus-within:border-[#2563eb]">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="#94a3b8" strokeWidth="2.5" strokeLinecap="round" aria-hidden="true">
<circle cx="11" cy="11" r="7" />
<path d="m21 21-4.35-4.35" />
</svg>
<span className="sr-only">문서 목차 검색</span>
<input
type="search"
value={query}
onChange={(e) => setQuery(e.target.value)}
placeholder="빠른 검색..."
className="min-w-0 flex-1 bg-transparent font-['Gothic_A1',sans-serif] text-[14px] leading-normal text-[#111827] outline-none placeholder:text-[#94a3b8]"
/>
</label>
<nav aria-label="문서 목차">
{sections.length === 0 && <p className="px-3 py-4 font-['Gothic_A1',sans-serif] text-[13px] text-[#9ca3af]">검색 결과가 없습니다.</p>}
{sections.map((s, i) => (
<div key={s.title}>
<p className={`pb-2 pl-3 font-['Inter',sans-serif] text-[11px] font-bold uppercase leading-normal text-[#9ca3af] ${i === 0 ? "pt-2.5" : "pt-5"}`}>
{s.title}
</p>
<ul role="tree" aria-label={s.title} className="flex flex-col gap-px">
{renderNodes(s.items, 0)}
</ul>
</div>
))}
</nav>
</div>
<a
href="#"
onClick={(e) => e.preventDefault()}
className="flex items-center gap-2 rounded-md px-3 pb-2 pt-6 font-['Inter',sans-serif] text-[12px] leading-normal text-[#9ca3af] hover:text-[#4b5563] focus-visible:outline-2 focus-visible:outline-[#2563eb]"
>
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" aria-hidden="true">
<circle cx="12" cy="12" r="10" />
<path d="M9.09 9a3 3 0 0 1 5.83 1c0 2-3 3-3 3M12 17h.01" />
</svg>
Need help with the TOC?
</a>
</div>
</div>
</section>
);
}
적용 사이트 0
이 블록이 실제로 쓰인 사이트입니다.
아직 등록된 사이트가 없습니다. 이 블록을 쓴 사이트가 있다면 알려 주세요.
재현 후기
v1 · 0개아직 이 버전의 프롬프트로 직접 만들어 본 사람이 없습니다. 첫 번째로 재현해 보세요.