Skip to content

Commit 06b5985

Browse files
committed
feat: directory double-click, Ctrl+W close tab, tree animations
- Double-click directory to expand + switch to code view - Ctrl+W / Cmd+W closes current file tab - Added to keyboard shortcuts dialog - FileTree AnimatePresence expand/collapse animation - onDirDoubleClick handler propagated through FileTree
1 parent 858d020 commit 06b5985

3 files changed

Lines changed: 47 additions & 18 deletions

File tree

structure-insight/components/FileTree.tsx

Lines changed: 38 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11

22
import React from 'react';
3+
import { motion, AnimatePresence } from 'framer-motion';
34
import { FileNode } from '../types';
45

56
interface FileTreeProps {
@@ -8,6 +9,7 @@ interface FileTreeProps {
89
onDeleteFile: (path: string) => void;
910
onCopyPath: (path: string) => void;
1011
onToggleExclude: (path: string) => void;
12+
onDirDoubleClick?: () => void;
1113
selectedFilePath: string | null;
1214
showCharCount: boolean;
1315
}
@@ -138,16 +140,17 @@ const getFileIcon = (fileName: string): IconEntry => {
138140
export { getFileIcon };
139141
export type { IconEntry };
140142

141-
const FileTreeNode: React.FC<{
142-
node: FileNode;
143-
onFileSelect: (path: string) => void;
144-
onDeleteFile: (path: string) => void;
145-
onCopyPath: (path: string) => void;
143+
const FileTreeNode: React.FC<{
144+
node: FileNode;
145+
onFileSelect: (path: string) => void;
146+
onDeleteFile: (path: string) => void;
147+
onCopyPath: (path: string) => void;
146148
onToggleExclude: (path: string) => void;
147-
level: number;
148-
selectedFilePath: string | null;
149-
showCharCount: boolean;
150-
}> = React.memo(({ node, onFileSelect, onDeleteFile, onCopyPath, onToggleExclude, level, selectedFilePath, showCharCount }) => {
149+
onDirDoubleClick?: () => void;
150+
level: number;
151+
selectedFilePath: string | null;
152+
showCharCount: boolean;
153+
}> = React.memo(({ node, onFileSelect, onDeleteFile, onCopyPath, onToggleExclude, onDirDoubleClick, level, selectedFilePath, showCharCount }) => {
151154
const [isOpen, setIsOpen] = React.useState(true);
152155

153156
const handleToggle = () => {
@@ -163,6 +166,13 @@ const FileTreeNode: React.FC<{
163166
handleToggle();
164167
}
165168
};
169+
170+
const handleDoubleClick = () => {
171+
if (node.isDirectory && onDirDoubleClick) {
172+
setIsOpen(!isOpen);
173+
onDirDoubleClick();
174+
}
175+
};
166176

167177
const handleDelete = (e: React.MouseEvent) => {
168178
e.stopPropagation();
@@ -214,6 +224,7 @@ const FileTreeNode: React.FC<{
214224
<div
215225
className={`group flex flex-col py-1 px-2 rounded-md cursor-pointer hover:bg-light-border dark:hover:bg-dark-border/50 transition-colors duration-150 ${statusClass} ${isSelected ? 'bg-primary/10 dark:bg-primary/20' : ''}`}
216226
onClick={handleSelect}
227+
onDoubleClick={handleDoubleClick}
217228
title={title}
218229
>
219230
{/* Top Row: Icon, Name, Stats */}
@@ -275,18 +286,28 @@ const FileTreeNode: React.FC<{
275286
</div>
276287
)}
277288
</div>
278-
{node.isDirectory && isOpen && (
279-
<ul className="pl-0">
280-
{node.children.map(child => (
281-
<FileTreeNode key={child.path} node={child} onFileSelect={onFileSelect} onDeleteFile={onDeleteFile} onCopyPath={onCopyPath} onToggleExclude={onToggleExclude} level={level + 1} selectedFilePath={selectedFilePath} showCharCount={showCharCount} />
282-
))}
283-
</ul>
289+
{node.isDirectory && (
290+
<AnimatePresence initial={false}>
291+
{isOpen && (
292+
<motion.ul
293+
className="pl-0 overflow-hidden"
294+
initial={{ height: 0, opacity: 0 }}
295+
animate={{ height: 'auto', opacity: 1 }}
296+
exit={{ height: 0, opacity: 0 }}
297+
transition={{ duration: 0.15, ease: 'easeInOut' }}
298+
>
299+
{node.children.map(child => (
300+
<FileTreeNode key={child.path} node={child} onFileSelect={onFileSelect} onDeleteFile={onDeleteFile} onCopyPath={onCopyPath} onToggleExclude={onToggleExclude} onDirDoubleClick={onDirDoubleClick} level={level + 1} selectedFilePath={selectedFilePath} showCharCount={showCharCount} />
301+
))}
302+
</motion.ul>
303+
)}
304+
</AnimatePresence>
284305
)}
285306
</li>
286307
);
287308
});
288309

289-
const FileTree: React.FC<FileTreeProps> = ({ nodes, onFileSelect, onDeleteFile, onCopyPath, onToggleExclude, selectedFilePath, showCharCount }) => {
310+
const FileTree: React.FC<FileTreeProps> = ({ nodes, onFileSelect, onDeleteFile, onCopyPath, onToggleExclude, onDirDoubleClick, selectedFilePath, showCharCount }) => {
290311
if (!nodes || nodes.length === 0) {
291312
return <div className="p-4 text-center text-sm text-light-subtle-text dark:text-dark-subtle-text">未加载文件。</div>;
292313
}
@@ -295,7 +316,7 @@ const FileTree: React.FC<FileTreeProps> = ({ nodes, onFileSelect, onDeleteFile,
295316
<h3 className="text-xs font-semibold px-2 mb-2 text-light-subtle-text dark:text-dark-subtle-text uppercase tracking-wider">资源管理器</h3>
296317
<ul className="pl-0">
297318
{nodes.map(node => (
298-
<FileTreeNode key={node.path} node={node} onFileSelect={onFileSelect} onDeleteFile={onDeleteFile} onCopyPath={onCopyPath} onToggleExclude={onToggleExclude} level={1} selectedFilePath={selectedFilePath} showCharCount={showCharCount} />
319+
<FileTreeNode key={node.path} node={node} onFileSelect={onFileSelect} onDeleteFile={onDeleteFile} onCopyPath={onCopyPath} onToggleExclude={onToggleExclude} onDirDoubleClick={onDirDoubleClick} level={1} selectedFilePath={selectedFilePath} showCharCount={showCharCount} />
299320
))}
300321
</ul>
301322
</div>

structure-insight/components/KeyboardShortcutsDialog.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ const shortcuts = [
1212
{ keys: ['Ctrl', 'S'], description: 'Save as text' },
1313
{ keys: ['Escape'], description: 'Close dialog' },
1414
{ keys: ['Ctrl', '/'], description: 'Show this help' },
15+
{ keys: ['Ctrl', 'W'], description: 'Close current tab' },
1516
];
1617

1718
const KeyboardShortcutsDialog: React.FC<KeyboardShortcutsDialogProps> = ({ isOpen, onClose }) => {

structure-insight/hooks/useAppLogic.ts

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -123,6 +123,11 @@ export const useAppLogic = (
123123
processedData, isMobile, setMobileView, setSelectedFilePath, setActiveView,
124124
});
125125

126+
const handleDirDoubleClick = React.useCallback(() => {
127+
setActiveView('code');
128+
setMobileView('editor');
129+
}, [setActiveView, setMobileView]);
130+
126131
// Update structure string when showCharCount changes
127132
React.useEffect(() => {
128133
if (processedData) {
@@ -250,6 +255,7 @@ export const useAppLogic = (
250255
if (e.key === 's') { e.preventDefault(); if (processedData) handleSave(); }
251256
if (e.key === 'o') { e.preventDefault(); handleFileSelect(); }
252257
if (e.key === '/') { e.preventDefault(); setIsShortcutsOpen(p => !p); }
258+
if (e.key === 'w') { e.preventDefault(); if (selectedFilePath) closeTab(selectedFilePath); }
253259
}
254260
if (e.key === 'Escape') {
255261
if (isShortcutsOpen) { e.preventDefault(); setIsShortcutsOpen(false); }
@@ -262,7 +268,7 @@ export const useAppLogic = (
262268
};
263269
window.addEventListener('keydown', handleGlobalKeys);
264270
return () => window.removeEventListener('keydown', handleGlobalKeys);
265-
}, [isSearchOpen, isSettingsOpen, isAiChatOpen, isFileRankOpen, isShortcutsOpen, isLoading, processedData, handleSave, handleFileSelect, handleCancel]);
271+
}, [isSearchOpen, isSettingsOpen, isAiChatOpen, isFileRankOpen, isShortcutsOpen, isLoading, processedData, handleSave, handleFileSelect, handleCancel, selectedFilePath, closeTab]);
266272

267273
// --- Memoized Stats ---
268274
const stats = React.useMemo(() => {
@@ -299,6 +305,7 @@ export const useAppLogic = (
299305
setActiveView,
300306
handleCopyPath,
301307
handleToggleExclude,
308+
handleDirDoubleClick,
302309
},
303310
settings: {
304311
setIsDark, setExtractContent, setFontSize, handleClearCache, setShowCharCount, setMaxCharsThreshold, setWordWrap

0 commit comments

Comments
 (0)