style: add VS Code tree view for doc sidebar and improve typography
This commit is contained in:
@@ -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>
|
||||
),
|
||||
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user