Files
XCSDD/src/components/DocTree.tsx

151 lines
3.6 KiB
TypeScript

import React, { useState } from 'react'
import { clsx } from 'clsx'
import type { DocFile } from '@/lib/types'
import { getDisplayName } from '@/lib/parser'
interface DocTreeProps {
files: DocFile[]
selectedPath?: string
onSelect: (file: DocFile) => void
onFolderClick?: (folderPath: string) => void
}
interface TreeNodeProps {
file: DocFile
level: number
isLast: boolean
ancestorsLast: boolean[]
selectedPath?: string
onSelect: (file: DocFile) => void
onFolderClick?: (folderPath: string) => void
expandedSet: Set<string>
onToggle: (path: string) => void
}
const TreeNode = React.memo(({
file,
level,
isLast,
ancestorsLast,
selectedPath,
onSelect,
onFolderClick,
expandedSet,
onToggle,
}: TreeNodeProps) => {
const isDir = file.isDir
const isExpanded = expandedSet.has(file.relativePath)
const isSelected = selectedPath === file.relativePath
const handleClick = () => {
if (isDir) {
onFolderClick?.(file.relativePath)
onToggle(file.relativePath)
} else {
onSelect(file)
}
}
const getIndent = (): string => {
let result = ''
for (let i = 0; i < level; i++) {
result += ancestorsLast[i] ? ' ' : '│ '
}
result += isLast ? '└─ ' : '├─ '
return result
}
const getIndentWidth = (): number => {
return level * 3 + 3
}
const showChildren = isDir && isExpanded && file.children
return (
<>
<div
onClick={handleClick}
className={clsx(
'flex items-center py-0.5 cursor-pointer text-base 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-zinc-500 font-mono whitespace-pre"
style={{ width: `${getIndentWidth()}ch` }}
>
{getIndent()}
</span>
<span className={clsx(
'truncate',
isDir && 'font-medium'
)}>
{getDisplayName(file.name)}
</span>
</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}
onFolderClick={onFolderClick}
expandedSet={expandedSet}
onToggle={onToggle}
/>
))}
</>
)
})
TreeNode.displayName = 'TreeNode'
export const DocTree = ({ files, selectedPath, onSelect, onFolderClick }: 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 px-1">
{files.map((file, idx) => (
<TreeNode
key={file.relativePath}
file={file}
level={0}
isLast={idx === files.length - 1}
ancestorsLast={[]}
selectedPath={selectedPath}
onSelect={onSelect}
onFolderClick={onFolderClick}
expandedSet={expandedSet}
onToggle={handleToggle}
/>
))}
</div>
)
}