style: add VS Code tree view for doc sidebar and improve typography

This commit is contained in:
2026-03-18 19:26:46 +08:00
parent 683b3d66ab
commit c3ecebea89
2 changed files with 116 additions and 63 deletions

View File

@@ -11,7 +11,7 @@ export const DocContent = ({ content, onReferenceClick }: DocContentProps) => {
if (!content) {
return (
<div className="flex items-center justify-center h-full text-zinc-500">
<p className="text-sm"></p>
<p className="text-base"></p>
</div>
)
}
@@ -44,19 +44,19 @@ export const DocContent = ({ content, onReferenceClick }: DocContentProps) => {
)
}
return (
<code className="text-zinc-300 font-mono text-sm" {...props}>
<code className="text-zinc-300 font-mono text-base" {...props}>
{children}
</code>
)
},
pre: ({ children }) => (
<pre className="bg-zinc-900 rounded-lg p-4 overflow-x-auto my-6 text-sm leading-relaxed">
<pre className="bg-zinc-900 rounded-lg p-4 overflow-x-auto my-6 text-base leading-relaxed">
{children}
</pre>
),
table: ({ children }) => (
<div className="overflow-x-auto my-6">
<table className="w-full text-sm">
<table className="w-full text-base">
{children}
</table>
</div>
@@ -87,22 +87,22 @@ export const DocContent = ({ content, onReferenceClick }: DocContentProps) => {
</h1>
),
h2: ({ children }) => (
<h2 className="text-xl font-semibold text-white mt-10 mb-4">
<h2 className="text-2xl font-semibold text-white mt-10 mb-4">
{children}
</h2>
),
h3: ({ children }) => (
<h3 className="text-lg font-medium text-zinc-200 mt-8 mb-3">
<h3 className="text-xl font-medium text-zinc-200 mt-8 mb-3">
{children}
</h3>
),
h4: ({ children }) => (
<h4 className="text-base font-medium text-zinc-300 mt-6 mb-2">
<h4 className="text-lg font-medium text-zinc-300 mt-6 mb-2">
{children}
</h4>
),
p: ({ children }) => (
<p className="text-zinc-400 leading-relaxed my-4">
<p className="text-base text-zinc-400 leading-relaxed my-4">
{children}
</p>
),

View File

@@ -1,5 +1,4 @@
import React, { useState } from 'react'
import { ChevronRight, ChevronDown, Folder, FileText } from 'lucide-react'
import { clsx } from 'clsx'
import type { DocFile } from '@/lib/types'
import { getDisplayName } from '@/lib/parser'
@@ -10,88 +9,142 @@ interface DocTreeProps {
onSelect: (file: DocFile) => void
}
interface DocTreeNodeProps {
interface TreeNodeProps {
file: DocFile
level: number
isLast: boolean
ancestorsLast: boolean[]
selectedPath?: string
onSelect: (file: DocFile) => void
expandedSet: Set<string>
onToggle: (path: string) => void
}
const DocTreeNode = React.memo(({ file, level, selectedPath, onSelect }: DocTreeNodeProps) => {
const [expanded, setExpanded] = useState(level === 0)
const isSelected = selectedPath === file.relativePath
const TreeNode = React.memo(({
file,
level,
isLast,
ancestorsLast,
selectedPath,
onSelect,
expandedSet,
onToggle,
}: TreeNodeProps) => {
const isDir = file.isDir
const isExpanded = expandedSet.has(file.relativePath)
const isSelected = selectedPath === file.relativePath
const handleClick = (e: React.MouseEvent) => {
e.stopPropagation()
const handleClick = () => {
if (isDir) {
setExpanded(!expanded)
onToggle(file.relativePath)
} else {
onSelect(file)
}
}
const renderIndent = () => {
const parts: React.ReactNode[] = []
for (let i = 0; i < level; i++) {
const showLine = !ancestorsLast[i]
parts.push(
<span key={i} className="inline-block w-4 text-center text-zinc-600">
{showLine ? '│' : ' '}
</span>
)
}
const connector = isLast ? '└──' : '├──'
parts.push(
<span key="connector" className="text-zinc-500">
{connector}
</span>
)
return parts
}
const showChildren = isDir && isExpanded && file.children
return (
<div>
<>
<div
className={clsx(
'flex items-center gap-2 py-1 px-2 rounded-md cursor-pointer transition-colors text-sm',
isSelected
? 'bg-gray-600 text-white'
: 'hover:bg-gray-700 text-gray-300'
)}
onClick={handleClick}
style={{ paddingLeft: `${level * 16 + 8}px` }}
className={clsx(
'flex items-center py-1 cursor-pointer text-sm select-none rounded',
isDir
? 'text-zinc-400 hover:text-zinc-200'
: isSelected
? 'text-blue-400'
: 'text-zinc-400 hover:text-zinc-200'
)}
>
<span className="text-gray-400 shrink-0">
{isDir ? (
expanded ? <ChevronDown size={16} /> : <ChevronRight size={16} />
) : (
<span className="w-4" />
)}
<span className="font-mono text-xs leading-none">
{renderIndent()}
</span>
<span className="text-gray-400 shrink-0">
{isDir ? <Folder size={16} /> : <FileText size={16} />}
<span className={clsx(
'ml-1 truncate',
isDir && 'font-medium'
)}>
{getDisplayName(file.name)}
</span>
<span className="truncate">{getDisplayName(file.name)}</span>
</div>
{isDir && expanded && file.children && (
<div>
{file.children.map((child) => (
<DocTreeNode
key={child.relativePath}
file={child}
level={level + 1}
selectedPath={selectedPath}
onSelect={onSelect}
/>
))}
</div>
)}
</div>
{showChildren && file.children!.map((child, idx) => (
<TreeNode
key={child.relativePath}
file={child}
level={level + 1}
isLast={idx === file.children!.length - 1}
ancestorsLast={[...ancestorsLast, isLast]}
selectedPath={selectedPath}
onSelect={onSelect}
expandedSet={expandedSet}
onToggle={onToggle}
/>
))}
</>
)
})
DocTreeNode.displayName = 'DocTreeNode'
TreeNode.displayName = 'TreeNode'
export const DocTree = ({ files, selectedPath, onSelect }: DocTreeProps) => {
const [expandedSet, setExpandedSet] = useState<Set<string>>(() => {
const set = new Set<string>()
files.forEach(f => {
if (f.isDir) set.add(f.relativePath)
})
return set
})
const handleToggle = (path: string) => {
setExpandedSet(prev => {
const next = new Set(prev)
if (next.has(path)) {
next.delete(path)
} else {
next.add(path)
}
return next
})
}
return (
<div className="select-none w-full h-full overflow-auto py-2">
<div className="space-y-1">
{files.map((file) => (
<DocTreeNode
key={file.relativePath}
file={file}
level={0}
selectedPath={selectedPath}
onSelect={onSelect}
/>
))}
</div>
<div className="select-none w-full h-full overflow-auto py-2 px-1">
{files.map((file, idx) => (
<TreeNode
key={file.relativePath}
file={file}
level={0}
isLast={idx === files.length - 1}
ancestorsLast={[]}
selectedPath={selectedPath}
onSelect={onSelect}
expandedSet={expandedSet}
onToggle={handleToggle}
/>
))}
</div>
)
}