Skip to content

Commit bd44fc8

Browse files
committed
feat: tabbed code panel with open files
- TabBar component with file icons, close buttons, and active highlighting - Open files tracked in state, tabs persist across file selections - closeTab navigates to adjacent tab, falls back to structure view - Tabs reset on project reset - Export getFileIcon from FileTree for reuse in TabBar - CSS additions: reduced motion, tab hover, file tree transitions
1 parent 287401b commit bd44fc8

5 files changed

Lines changed: 134 additions & 2 deletions

File tree

structure-insight/components/FileTree.tsx

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -135,6 +135,9 @@ const getFileIcon = (fileName: string): IconEntry => {
135135
return extensionMap.get(ext) ?? defaultIcon;
136136
};
137137

138+
export { getFileIcon };
139+
export type { IconEntry };
140+
138141
const FileTreeNode: React.FC<{
139142
node: FileNode;
140143
onFileSelect: (path: string) => void;

structure-insight/components/MainContent.tsx

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import { motion, AnimatePresence } from 'framer-motion';
44
import FileTree from './FileTree';
55
import CodeView from './CodeView';
66
import InitialPrompt from './InitialPrompt';
7+
import TabBar from './TabBar';
78
import { useAppLogic } from '../hooks/useAppLogic';
89
import ScrollSlider from './ScrollSlider';
910
import StructureView from './StructureView';
@@ -140,6 +141,14 @@ const MainContent: React.FC<MainContentProps> = ({ logic, codeViewRef, leftPanel
140141
)}
141142
{state.mobileView === 'editor' && (
142143
<motion.div key="editor" initial={{x: '0%'}} animate={{x: '0%'}} exit={{x: '100%'}} transition={{duration: 0.3, ease: 'easeInOut'}} className="absolute inset-0 h-full flex flex-col">
144+
{state.activeView === 'code' && state.processedData && (
145+
<TabBar
146+
openFiles={state.openFiles}
147+
selectedFilePath={state.selectedFilePath}
148+
onTabSelect={handlers.handleFileTreeSelect}
149+
onCloseTab={handlers.closeTab}
150+
/>
151+
)}
143152
{state.isLoading ? (
144153
<LoadingIndicator message={state.progressMessage} />
145154
) : state.processedData ? (
@@ -215,7 +224,15 @@ const MainContent: React.FC<MainContentProps> = ({ logic, codeViewRef, leftPanel
215224
<div onMouseDown={handlers.handleMouseDownResize} className="w-1.5 h-full cursor-col-resize group z-10">
216225
<div className="w-full h-full bg-light-border dark:bg-dark-border group-hover:bg-primary transition-colors duration-200" />
217226
</div>
218-
<div className="flex-1 h-full overflow-hidden bg-light-bg dark:bg-dark-bg flex">
227+
<div className="flex-1 h-full overflow-hidden bg-light-bg dark:bg-dark-bg flex flex-col">
228+
{state.activeView === 'code' && state.processedData && (
229+
<TabBar
230+
openFiles={state.openFiles}
231+
selectedFilePath={state.selectedFilePath}
232+
onTabSelect={handlers.handleFileTreeSelect}
233+
onCloseTab={handlers.closeTab}
234+
/>
235+
)}
219236
<div className="flex-1 h-full flex flex-col min-w-0">
220237
{state.isLoading ? (
221238
<LoadingIndicator message={state.progressMessage} />
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
2+
import React from 'react';
3+
import { getFileIcon } from './FileTree';
4+
5+
interface TabBarProps {
6+
openFiles: string[];
7+
selectedFilePath: string | null;
8+
onTabSelect: (path: string) => void;
9+
onCloseTab: (path: string) => void;
10+
}
11+
12+
const TabBar: React.FC<TabBarProps> = ({ openFiles, selectedFilePath, onTabSelect, onCloseTab }) => {
13+
if (openFiles.length === 0) return null;
14+
15+
return (
16+
<div className="flex overflow-x-auto no-scrollbar border-b border-light-border dark:border-dark-border bg-light-panel dark:bg-dark-panel shrink-0">
17+
{openFiles.map(path => {
18+
const fileName = path.split('/').pop() || path;
19+
const isActive = path === selectedFilePath;
20+
const { icon, color } = getFileIcon(fileName);
21+
22+
return (
23+
<div
24+
key={path}
25+
onClick={() => onTabSelect(path)}
26+
className={`group flex items-center gap-1.5 px-3 py-1.5 cursor-pointer border-b-2 transition-colors shrink-0 text-xs ${
27+
isActive
28+
? 'border-primary bg-primary/10 text-light-text dark:text-dark-text'
29+
: 'border-transparent text-light-subtle-text dark:text-dark-subtle-text hover:bg-light-hover dark:hover:bg-dark-hover'
30+
}`}
31+
title={path}
32+
>
33+
<i className={`${icon} ${color} text-[11px]`} />
34+
<span className="truncate max-w-[120px]">{fileName}</span>
35+
<button
36+
onClick={(e) => { e.stopPropagation(); onCloseTab(path); }}
37+
className="ml-1 w-4 h-4 flex items-center justify-center rounded hover:bg-light-border dark:hover:bg-dark-border text-light-subtle-text dark:text-dark-subtle-text opacity-0 group-hover:opacity-100 transition-opacity"
38+
>
39+
<i className="fa-solid fa-xmark text-[10px]" />
40+
</button>
41+
</div>
42+
);
43+
})}
44+
</div>
45+
);
46+
};
47+
48+
export default React.memo(TabBar);

structure-insight/hooks/useAppLogic.ts

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ export const useAppLogic = (
3535
// --- Core Data & Selection State ---
3636
const [selectedFilePath, setSelectedFilePath] = React.useState<string | null>(null);
3737
const [activeView, setActiveView] = React.useState<'structure' | 'code'>('structure');
38+
const [openFiles, setOpenFiles] = React.useState<string[]>([]);
3839

3940
// --- Layout State ---
4041
const windowSize = useWindowSize();
@@ -73,6 +74,25 @@ export const useAppLogic = (
7374
selectedFilePath, setSelectedFilePath, setActiveView, showCharCount
7475
});
7576

77+
// --- Tab Management ---
78+
const handleTabSelect = React.useCallback((path: string) => {
79+
setOpenFiles(prev => prev.includes(path) ? prev : [...prev, path]);
80+
handleFileTreeSelect(path);
81+
}, [handleFileTreeSelect]);
82+
83+
const closeTab = React.useCallback((path: string) => {
84+
setOpenFiles(prev => {
85+
const next = prev.filter(p => p !== path);
86+
if (path === selectedFilePath) {
87+
const closedIdx = prev.indexOf(path);
88+
const newSelected = next[Math.min(closedIdx, next.length - 1)] ?? null;
89+
setSelectedFilePath(newSelected);
90+
if (!newSelected) setActiveView('structure');
91+
}
92+
return next;
93+
});
94+
}, [selectedFilePath, setSelectedFilePath, setActiveView]);
95+
7696
// --- Search Hook ---
7797
const {
7898
isSearchOpen, setIsSearchOpen, searchResults, activeResultIndex,
@@ -129,6 +149,7 @@ export const useAppLogic = (
129149
setIsSettingsOpen(false);
130150
clearInteractionState();
131151
setSelectedFilePath(null);
152+
setOpenFiles([]);
132153
resetSearch();
133154
setIsAiChatOpen(false);
134155
setIsFileRankOpen(false);
@@ -241,14 +262,15 @@ export const useAppLogic = (
241262
lastProcessedFiles, mobileView, stats,
242263
isSearchOpen, isFileRankOpen, isShortcutsOpen, searchResults, activeResultIndex, isMobile, isAiChatOpen,
243264
selectedFilePath, selectedFile, activeView,
265+
openFiles,
244266
searchQuery, searchOptions, activeMatchIndexInFile,
245267
recentProjects,
246268
},
247269
handlers: {
248270
setIsDragging, handleDrop: (e: React.DragEvent) => { setIsDragging(false); handleDrop(e, isLoading); },
249271
handleFileSelect, handleCopyAll, handleSave, handleReset, handleRefresh: () => handleRefresh(handleProcessing), handleCancel,
250272
setIsSettingsOpen, setToastMessage, setConfirmation,
251-
handleDeleteFile, handleFileTreeSelect, setEditingPath, handleSaveEdit, handleToggleMarkdownPreview,
273+
handleDeleteFile, handleFileTreeSelect: handleTabSelect, closeTab, setEditingPath, handleSaveEdit, handleToggleMarkdownPreview,
252274
handleMouseDownResize,
253275
handleMobileViewToggle,
254276
setIsSearchOpen, setIsFileRankOpen, setIsShortcutsOpen, handleSearch, handleNavigate, setIsAiChatOpen,

structure-insight/index.css

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -143,3 +143,45 @@ input[type="range"]::-moz-range-thumb {
143143
display: none !important;
144144
}
145145
}
146+
147+
/* Tab bar horizontal scroll */
148+
.tab-bar-scroll {
149+
scrollbar-width: none;
150+
-ms-overflow-style: none;
151+
}
152+
.tab-bar-scroll::-webkit-scrollbar {
153+
display: none;
154+
}
155+
156+
/* Tab close button hover */
157+
.tab-close-btn {
158+
opacity: 0;
159+
transition: opacity 0.15s ease;
160+
}
161+
.tab-item:hover .tab-close-btn {
162+
opacity: 1;
163+
}
164+
165+
/* Smooth panel resize */
166+
.panel-resize-handle {
167+
transition: background-color 0.2s ease;
168+
}
169+
170+
/* File tree item transition */
171+
.file-tree-item {
172+
transition: background-color 0.1s ease, color 0.1s ease;
173+
}
174+
175+
/* Tooltip styles */
176+
[title] {
177+
cursor: help;
178+
}
179+
180+
/* Reduced motion preference */
181+
@media (prefers-reduced-motion: reduce) {
182+
*, *::before, *::after {
183+
animation-duration: 0.01ms !important;
184+
animation-iteration-count: 1 !important;
185+
transition-duration: 0.01ms !important;
186+
}
187+
}

0 commit comments

Comments
 (0)