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) { if (!content) {
return ( return (
<div className="flex items-center justify-center h-full text-zinc-500"> <div className="flex items-center justify-center h-full text-zinc-500">
<p className="text-sm"></p> <p className="text-base"></p>
</div> </div>
) )
} }
@@ -44,19 +44,19 @@ export const DocContent = ({ content, onReferenceClick }: DocContentProps) => {
) )
} }
return ( return (
<code className="text-zinc-300 font-mono text-sm" {...props}> <code className="text-zinc-300 font-mono text-base" {...props}>
{children} {children}
</code> </code>
) )
}, },
pre: ({ children }) => ( 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} {children}
</pre> </pre>
), ),
table: ({ children }) => ( table: ({ children }) => (
<div className="overflow-x-auto my-6"> <div className="overflow-x-auto my-6">
<table className="w-full text-sm"> <table className="w-full text-base">
{children} {children}
</table> </table>
</div> </div>
@@ -87,22 +87,22 @@ export const DocContent = ({ content, onReferenceClick }: DocContentProps) => {
</h1> </h1>
), ),
h2: ({ children }) => ( 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} {children}
</h2> </h2>
), ),
h3: ({ children }) => ( 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} {children}
</h3> </h3>
), ),
h4: ({ children }) => ( 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} {children}
</h4> </h4>
), ),
p: ({ children }) => ( p: ({ children }) => (
<p className="text-zinc-400 leading-relaxed my-4"> <p className="text-base text-zinc-400 leading-relaxed my-4">
{children} {children}
</p> </p>
), ),

View File

@@ -1,5 +1,4 @@
import React, { useState } from 'react' import React, { useState } from 'react'
import { ChevronRight, ChevronDown, Folder, FileText } from 'lucide-react'
import { clsx } from 'clsx' import { clsx } from 'clsx'
import type { DocFile } from '@/lib/types' import type { DocFile } from '@/lib/types'
import { getDisplayName } from '@/lib/parser' import { getDisplayName } from '@/lib/parser'
@@ -10,88 +9,142 @@ interface DocTreeProps {
onSelect: (file: DocFile) => void onSelect: (file: DocFile) => void
} }
interface DocTreeNodeProps { interface TreeNodeProps {
file: DocFile file: DocFile
level: number level: number
isLast: boolean
ancestorsLast: boolean[]
selectedPath?: string selectedPath?: string
onSelect: (file: DocFile) => void onSelect: (file: DocFile) => void
expandedSet: Set<string>
onToggle: (path: string) => void
} }
const DocTreeNode = React.memo(({ file, level, selectedPath, onSelect }: DocTreeNodeProps) => { const TreeNode = React.memo(({
const [expanded, setExpanded] = useState(level === 0) file,
level,
const isSelected = selectedPath === file.relativePath isLast,
ancestorsLast,
selectedPath,
onSelect,
expandedSet,
onToggle,
}: TreeNodeProps) => {
const isDir = file.isDir const isDir = file.isDir
const isExpanded = expandedSet.has(file.relativePath)
const isSelected = selectedPath === file.relativePath
const handleClick = (e: React.MouseEvent) => { const handleClick = () => {
e.stopPropagation()
if (isDir) { if (isDir) {
setExpanded(!expanded) onToggle(file.relativePath)
} else { } else {
onSelect(file) 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 ( return (
<div> <>
<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} 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"> <span className="font-mono text-xs leading-none">
{isDir ? ( {renderIndent()}
expanded ? <ChevronDown size={16} /> : <ChevronRight size={16} />
) : (
<span className="w-4" />
)}
</span> </span>
<span className={clsx(
<span className="text-gray-400 shrink-0"> 'ml-1 truncate',
{isDir ? <Folder size={16} /> : <FileText size={16} />} isDir && 'font-medium'
)}>
{getDisplayName(file.name)}
</span> </span>
<span className="truncate">{getDisplayName(file.name)}</span>
</div> </div>
{isDir && expanded && file.children && ( {showChildren && file.children!.map((child, idx) => (
<div> <TreeNode
{file.children.map((child) => ( key={child.relativePath}
<DocTreeNode file={child}
key={child.relativePath} level={level + 1}
file={child} isLast={idx === file.children!.length - 1}
level={level + 1} ancestorsLast={[...ancestorsLast, isLast]}
selectedPath={selectedPath} selectedPath={selectedPath}
onSelect={onSelect} onSelect={onSelect}
/> expandedSet={expandedSet}
))} onToggle={onToggle}
</div> />
)} ))}
</div> </>
) )
}) })
DocTreeNode.displayName = 'DocTreeNode' TreeNode.displayName = 'TreeNode'
export const DocTree = ({ files, selectedPath, onSelect }: DocTreeProps) => { 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 ( return (
<div className="select-none w-full h-full overflow-auto py-2"> <div className="select-none w-full h-full overflow-auto py-2 px-1">
<div className="space-y-1"> {files.map((file, idx) => (
{files.map((file) => ( <TreeNode
<DocTreeNode key={file.relativePath}
key={file.relativePath} file={file}
file={file} level={0}
level={0} isLast={idx === files.length - 1}
selectedPath={selectedPath} ancestorsLast={[]}
onSelect={onSelect} selectedPath={selectedPath}
/> onSelect={onSelect}
))} expandedSet={expandedSet}
</div> onToggle={handleToggle}
/>
))}
</div> </div>
) )
} }