feature: swap default code editor to sidebar and make the modal optional

This commit is contained in:
simosmik
2026-02-16 14:02:18 +00:00
parent 412102c531
commit 33b0ea4c4a
5 changed files with 68 additions and 64 deletions

View File

@@ -143,7 +143,7 @@ function MarkdownPreview({ content }) {
); );
} }
function CodeEditor({ file, onClose, projectPath, isSidebar = false, isExpanded = false, onToggleExpand = null }) { function CodeEditor({ file, onClose, projectPath, isSidebar = false, isExpanded = false, onToggleExpand = null, onPopOut = null }) {
const { t } = useTranslation('codeEditor'); const { t } = useTranslation('codeEditor');
const [content, setContent] = useState(''); const [content, setContent] = useState('');
const [loading, setLoading] = useState(true); const [loading, setLoading] = useState(true);
@@ -165,7 +165,7 @@ function CodeEditor({ file, onClose, projectPath, isSidebar = false, isExpanded
return localStorage.getItem('codeEditorLineNumbers') !== 'false'; return localStorage.getItem('codeEditorLineNumbers') !== 'false';
}); });
const [fontSize, setFontSize] = useState(() => { const [fontSize, setFontSize] = useState(() => {
return localStorage.getItem('codeEditorFontSize') || '14'; return localStorage.getItem('codeEditorFontSize') || '12';
}); });
const [markdownPreview, setMarkdownPreview] = useState(false); const [markdownPreview, setMarkdownPreview] = useState(false);
const editorRef = useRef(null); const editorRef = useRef(null);
@@ -304,6 +304,16 @@ function CodeEditor({ file, onClose, projectPath, isSidebar = false, isExpanded
</button> </button>
`; `;
if (isSidebar && onPopOut) {
toolbarHTML += `
<button class="cm-toolbar-btn cm-popout-btn" title="Open in modal">
<svg width="16" height="16" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M18 13v6a2 2 0 01-2 2H5a2 2 0 01-2-2V8a2 2 0 012-2h6M15 3h6v6M10 14L21 3" />
</svg>
</button>
`;
}
// Expand button (only in sidebar mode) // Expand button (only in sidebar mode)
if (isSidebar && onToggleExpand) { if (isSidebar && onToggleExpand) {
toolbarHTML += ` toolbarHTML += `
@@ -323,7 +333,6 @@ function CodeEditor({ file, onClose, projectPath, isSidebar = false, isExpanded
dom.innerHTML = toolbarHTML; dom.innerHTML = toolbarHTML;
// Attach event listeners for diff navigation
if (hasDiff) { if (hasDiff) {
const prevBtn = dom.querySelector('.cm-diff-nav-prev'); const prevBtn = dom.querySelector('.cm-diff-nav-prev');
const nextBtn = dom.querySelector('.cm-diff-nav-next'); const nextBtn = dom.querySelector('.cm-diff-nav-next');
@@ -355,7 +364,6 @@ function CodeEditor({ file, onClose, projectPath, isSidebar = false, isExpanded
}); });
} }
// Attach event listener for toggle diff button
if (file.diffInfo) { if (file.diffInfo) {
const toggleDiffBtn = dom.querySelector('.cm-toggle-diff-btn'); const toggleDiffBtn = dom.querySelector('.cm-toggle-diff-btn');
toggleDiffBtn?.addEventListener('click', () => { toggleDiffBtn?.addEventListener('click', () => {
@@ -363,7 +371,6 @@ function CodeEditor({ file, onClose, projectPath, isSidebar = false, isExpanded
}); });
} }
// Attach event listener for settings button
const settingsBtn = dom.querySelector('.cm-settings-btn'); const settingsBtn = dom.querySelector('.cm-settings-btn');
settingsBtn?.addEventListener('click', () => { settingsBtn?.addEventListener('click', () => {
if (window.openSettings) { if (window.openSettings) {
@@ -371,7 +378,13 @@ function CodeEditor({ file, onClose, projectPath, isSidebar = false, isExpanded
} }
}); });
// Attach event listener for expand button if (isSidebar && onPopOut) {
const popoutBtn = dom.querySelector('.cm-popout-btn');
popoutBtn?.addEventListener('click', () => {
onPopOut();
});
}
if (isSidebar && onToggleExpand) { if (isSidebar && onToggleExpand) {
const expandBtn = dom.querySelector('.cm-expand-btn'); const expandBtn = dom.querySelector('.cm-expand-btn');
expandBtn?.addEventListener('click', () => { expandBtn?.addEventListener('click', () => {
@@ -390,7 +403,7 @@ function CodeEditor({ file, onClose, projectPath, isSidebar = false, isExpanded
}; };
return [showPanel.of(createPanel)]; return [showPanel.of(createPanel)];
}, [file.diffInfo, showDiff, isSidebar, isExpanded, onToggleExpand]); }, [file.diffInfo, showDiff, isSidebar, isExpanded, onToggleExpand, onPopOut]);
// Get language extension based on file extension // Get language extension based on file extension
const getLanguageExtension = (filename) => { const getLanguageExtension = (filename) => {
@@ -666,16 +679,16 @@ function CodeEditor({ file, onClose, projectPath, isSidebar = false, isExpanded
/* Editor toolbar panel styling */ /* Editor toolbar panel styling */
.cm-editor-toolbar-panel { .cm-editor-toolbar-panel {
padding: 8px 12px; padding: 4px 10px;
background-color: ${isDarkMode ? '#1f2937' : '#ffffff'}; background-color: ${isDarkMode ? '#1f2937' : '#ffffff'};
border-bottom: 1px solid ${isDarkMode ? '#374151' : '#e5e7eb'}; border-bottom: 1px solid ${isDarkMode ? '#374151' : '#e5e7eb'};
color: ${isDarkMode ? '#d1d5db' : '#374151'}; color: ${isDarkMode ? '#d1d5db' : '#374151'};
font-size: 14px; font-size: 12px;
} }
.cm-diff-nav-btn, .cm-diff-nav-btn,
.cm-toolbar-btn { .cm-toolbar-btn {
padding: 4px; padding: 3px;
background: transparent; background: transparent;
border: none; border: none;
cursor: pointer; cursor: pointer;
@@ -712,72 +725,67 @@ function CodeEditor({ file, onClose, projectPath, isSidebar = false, isExpanded
(isFullscreen ? ' md:w-full md:h-full md:rounded-none' : ' md:w-full md:max-w-6xl md:h-[80vh] md:max-h-[80vh]') (isFullscreen ? ' md:w-full md:h-full md:rounded-none' : ' md:w-full md:max-w-6xl md:h-[80vh] md:max-h-[80vh]')
}`}> }`}>
{/* Header */} {/* Header */}
<div className="flex items-center justify-between p-4 border-b border-border flex-shrink-0 min-w-0"> <div className="flex items-center justify-between px-3 py-1.5 border-b border-border flex-shrink-0 min-w-0">
<div className="flex items-center gap-3 min-w-0 flex-1"> <div className="flex items-center gap-2 min-w-0 flex-1">
<div className="min-w-0 flex-1"> <div className="min-w-0 flex-1">
<div className="flex items-center gap-2 min-w-0"> <div className="flex items-center gap-2 min-w-0">
<h3 className="font-medium text-gray-900 dark:text-white truncate">{file.name}</h3> <h3 className="text-sm font-medium text-gray-900 dark:text-white truncate">{file.name}</h3>
{file.diffInfo && ( {file.diffInfo && (
<span className="text-xs bg-blue-100 dark:bg-blue-900 text-blue-600 dark:text-blue-300 px-2 py-1 rounded whitespace-nowrap"> <span className="text-[10px] bg-blue-100 dark:bg-blue-900 text-blue-600 dark:text-blue-300 px-1.5 py-0.5 rounded whitespace-nowrap">
{t('header.showingChanges')} {t('header.showingChanges')}
</span> </span>
)} )}
</div> </div>
<p className="text-sm text-gray-500 dark:text-gray-400 truncate">{file.path}</p> <p className="text-xs text-gray-500 dark:text-gray-400 truncate">{file.path}</p>
</div> </div>
</div> </div>
<div className="flex items-center gap-1 md:gap-2 flex-shrink-0"> <div className="flex items-center gap-0.5 md:gap-1 flex-shrink-0">
{isMarkdownFile && ( {isMarkdownFile && (
<button <button
onClick={() => setMarkdownPreview(!markdownPreview)} onClick={() => setMarkdownPreview(!markdownPreview)}
className={`p-2 md:p-2 rounded-md min-w-[44px] min-h-[44px] md:min-w-0 md:min-h-0 flex items-center justify-center transition-colors ${ className={`p-1.5 rounded-md min-w-[36px] min-h-[36px] md:min-w-0 md:min-h-0 flex items-center justify-center transition-colors ${
markdownPreview markdownPreview
? 'text-blue-600 dark:text-blue-400 bg-blue-50 dark:bg-blue-900/30' ? 'text-blue-600 dark:text-blue-400 bg-blue-50 dark:bg-blue-900/30'
: 'text-gray-600 dark:text-gray-400 hover:text-gray-900 dark:hover:text-white hover:bg-gray-100 dark:hover:bg-gray-800' : 'text-gray-600 dark:text-gray-400 hover:text-gray-900 dark:hover:text-white hover:bg-gray-100 dark:hover:bg-gray-800'
}`} }`}
title={markdownPreview ? t('actions.editMarkdown') : t('actions.previewMarkdown')} title={markdownPreview ? t('actions.editMarkdown') : t('actions.previewMarkdown')}
> >
{markdownPreview ? <Code2 className="w-5 h-5 md:w-4 md:h-4" /> : <Eye className="w-5 h-5 md:w-4 md:h-4" />} {markdownPreview ? <Code2 className="w-4 h-4" /> : <Eye className="w-4 h-4" />}
</button> </button>
)} )}
<button <button
onClick={handleDownload} onClick={handleDownload}
className="p-2 md:p-2 text-gray-600 dark:text-gray-400 hover:text-gray-900 dark:hover:text-white rounded-md hover:bg-gray-100 dark:hover:bg-gray-800 min-w-[44px] min-h-[44px] md:min-w-0 md:min-h-0 flex items-center justify-center" className="p-1.5 text-gray-600 dark:text-gray-400 hover:text-gray-900 dark:hover:text-white rounded-md hover:bg-gray-100 dark:hover:bg-gray-800 min-w-[36px] min-h-[36px] md:min-w-0 md:min-h-0 flex items-center justify-center"
title={t('actions.download')} title={t('actions.download')}
> >
<Download className="w-5 h-5 md:w-4 md:h-4" /> <Download className="w-4 h-4" />
</button> </button>
<button <button
onClick={handleSave} onClick={handleSave}
disabled={saving} disabled={saving}
className={`px-3 py-2 text-white rounded-md disabled:opacity-50 flex items-center gap-2 transition-colors min-h-[44px] md:min-h-0 ${ className={`p-1.5 rounded-md disabled:opacity-50 flex items-center justify-center transition-colors min-w-[36px] min-h-[36px] md:min-w-0 md:min-h-0 ${
saveSuccess saveSuccess
? 'bg-green-600 hover:bg-green-700' ? 'text-green-600 dark:text-green-400 bg-green-50 dark:bg-green-900/30'
: 'bg-blue-600 hover:bg-blue-700' : 'text-gray-600 dark:text-gray-400 hover:text-gray-900 dark:hover:text-white hover:bg-gray-100 dark:hover:bg-gray-800'
}`} }`}
title={saveSuccess ? t('actions.saved') : saving ? t('actions.saving') : t('actions.save')}
> >
{saveSuccess ? ( {saveSuccess ? (
<> <svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<svg className="w-5 h-5 md:w-4 md:h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"> <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M5 13l4 4L19 7" />
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M5 13l4 4L19 7" /> </svg>
</svg>
<span className="hidden sm:inline">{t('actions.saved')}</span>
</>
) : ( ) : (
<> <Save className="w-4 h-4" />
<Save className="w-5 h-5 md:w-4 md:h-4" />
<span className="hidden sm:inline">{saving ? t('actions.saving') : t('actions.save')}</span>
</>
)} )}
</button> </button>
{!isSidebar && ( {!isSidebar && (
<button <button
onClick={toggleFullscreen} onClick={toggleFullscreen}
className="hidden md:flex p-2 text-gray-600 dark:text-gray-400 hover:text-gray-900 dark:hover:text-white rounded-md hover:bg-gray-100 dark:hover:bg-gray-800 items-center justify-center" className="hidden md:flex p-1.5 text-gray-600 dark:text-gray-400 hover:text-gray-900 dark:hover:text-white rounded-md hover:bg-gray-100 dark:hover:bg-gray-800 items-center justify-center"
title={isFullscreen ? t('actions.exitFullscreen') : t('actions.fullscreen')} title={isFullscreen ? t('actions.exitFullscreen') : t('actions.fullscreen')}
> >
{isFullscreen ? <Minimize2 className="w-4 h-4" /> : <Maximize2 className="w-4 h-4" />} {isFullscreen ? <Minimize2 className="w-4 h-4" /> : <Maximize2 className="w-4 h-4" />}
@@ -786,10 +794,10 @@ function CodeEditor({ file, onClose, projectPath, isSidebar = false, isExpanded
<button <button
onClick={onClose} onClick={onClose}
className="p-2 md:p-2 text-gray-600 dark:text-gray-400 hover:text-gray-900 dark:hover:text-white rounded-md hover:bg-gray-100 dark:hover:bg-gray-800 min-w-[44px] min-h-[44px] md:min-w-0 md:min-h-0 flex items-center justify-center" className="p-1.5 text-gray-600 dark:text-gray-400 hover:text-gray-900 dark:hover:text-white rounded-md hover:bg-gray-100 dark:hover:bg-gray-800 min-w-[36px] min-h-[36px] md:min-w-0 md:min-h-0 flex items-center justify-center"
title={t('actions.close')} title={t('actions.close')}
> >
<X className="w-6 h-6 md:w-4 md:h-4" /> <X className="w-4 h-4" />
</button> </button>
</div> </div>
</div> </div>
@@ -851,13 +859,13 @@ function CodeEditor({ file, onClose, projectPath, isSidebar = false, isExpanded
</div> </div>
{/* Footer */} {/* Footer */}
<div className="flex items-center justify-between p-3 border-t border-border bg-muted flex-shrink-0"> <div className="flex items-center justify-between px-3 py-1.5 border-t border-border bg-muted flex-shrink-0">
<div className="flex items-center gap-4 text-sm text-gray-600 dark:text-gray-400"> <div className="flex items-center gap-3 text-xs text-gray-600 dark:text-gray-400">
<span>{t('footer.lines')} {content.split('\n').length}</span> <span>{t('footer.lines')} {content.split('\n').length}</span>
<span>{t('footer.characters')} {content.length}</span> <span>{t('footer.characters')} {content.length}</span>
</div> </div>
<div className="text-sm text-gray-500 dark:text-gray-400"> <div className="text-xs text-gray-500 dark:text-gray-400">
{t('footer.shortcuts')} {t('footer.shortcuts')}
</div> </div>
</div> </div>

View File

@@ -13,7 +13,6 @@ import {
Scroll, FlaskConical, NotebookPen, FileCheck, Workflow, Blocks Scroll, FlaskConical, NotebookPen, FileCheck, Workflow, Blocks
} from 'lucide-react'; } from 'lucide-react';
import { cn } from '../lib/utils'; import { cn } from '../lib/utils';
import CodeEditor from './CodeEditor';
import ImageViewer from './ImageViewer'; import ImageViewer from './ImageViewer';
import { api } from '../utils/api'; import { api } from '../utils/api';
@@ -260,12 +259,11 @@ function getFileIconData(filename) {
// ─── Component ─────────────────────────────────────────────────────── // ─── Component ───────────────────────────────────────────────────────
function FileTree({ selectedProject }) { function FileTree({ selectedProject, onFileOpen }) {
const { t } = useTranslation(); const { t } = useTranslation();
const [files, setFiles] = useState([]); const [files, setFiles] = useState([]);
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
const [expandedDirs, setExpandedDirs] = useState(new Set()); const [expandedDirs, setExpandedDirs] = useState(new Set());
const [selectedFile, setSelectedFile] = useState(null);
const [selectedImage, setSelectedImage] = useState(null); const [selectedImage, setSelectedImage] = useState(null);
const [viewMode, setViewMode] = useState('detailed'); const [viewMode, setViewMode] = useState('detailed');
const [searchQuery, setSearchQuery] = useState(''); const [searchQuery, setSearchQuery] = useState('');
@@ -403,13 +401,8 @@ function FileTree({ selectedProject }) {
projectPath: selectedProject.path, projectPath: selectedProject.path,
projectName: selectedProject.name projectName: selectedProject.name
}); });
} else { } else if (onFileOpen) {
setSelectedFile({ onFileOpen(item.path);
name: item.name,
path: item.path,
projectPath: selectedProject.path,
projectName: selectedProject.name
});
} }
}; };
@@ -722,15 +715,6 @@ function FileTree({ selectedProject }) {
)} )}
</ScrollArea> </ScrollArea>
{/* Code Editor Modal */}
{selectedFile && (
<CodeEditor
file={selectedFile}
onClose={() => setSelectedFile(null)}
projectPath={selectedFile.projectPath}
/>
)}
{/* Image Viewer Modal */} {/* Image Viewer Modal */}
{selectedImage && ( {selectedImage && (
<ImageViewer <ImageViewer

View File

@@ -100,6 +100,7 @@ export interface EditorSidebarProps {
onCloseEditor: () => void; onCloseEditor: () => void;
onToggleEditorExpand: () => void; onToggleEditorExpand: () => void;
projectPath?: string; projectPath?: string;
fillSpace?: boolean;
} }
export interface TaskMasterPanelProps { export interface TaskMasterPanelProps {

View File

@@ -109,7 +109,7 @@ function MainContent({
/> />
<div className="flex-1 flex min-h-0 overflow-hidden"> <div className="flex-1 flex min-h-0 overflow-hidden">
<div className={`flex-1 flex flex-col min-h-0 overflow-hidden ${editorExpanded ? 'hidden' : ''}`}> <div className={`flex flex-col min-h-0 overflow-hidden ${editorExpanded ? 'hidden' : ''} ${activeTab === 'files' && editingFile ? 'w-[280px] flex-shrink-0' : 'flex-1'}`}>
<div className={`h-full ${activeTab === 'chat' ? 'block' : 'hidden'}`}> <div className={`h-full ${activeTab === 'chat' ? 'block' : 'hidden'}`}>
<ErrorBoundary showDetails> <ErrorBoundary showDetails>
<ChatInterface <ChatInterface
@@ -141,7 +141,7 @@ function MainContent({
{activeTab === 'files' && ( {activeTab === 'files' && (
<div className="h-full overflow-hidden"> <div className="h-full overflow-hidden">
<FileTree selectedProject={selectedProject} /> <FileTree selectedProject={selectedProject} onFileOpen={handleFileOpen} />
</div> </div>
)} )}
@@ -172,6 +172,7 @@ function MainContent({
onCloseEditor={handleCloseEditor} onCloseEditor={handleCloseEditor}
onToggleEditorExpand={handleToggleEditorExpand} onToggleEditorExpand={handleToggleEditorExpand}
projectPath={selectedProject.path} projectPath={selectedProject.path}
fillSpace={activeTab === 'files'}
/> />
</div> </div>
</div> </div>

View File

@@ -1,3 +1,4 @@
import { useState } from 'react';
import CodeEditor from '../../../CodeEditor'; import CodeEditor from '../../../CodeEditor';
import type { EditorSidebarProps } from '../../types/types'; import type { EditorSidebarProps } from '../../types/types';
@@ -13,22 +14,30 @@ export default function EditorSidebar({
onCloseEditor, onCloseEditor,
onToggleEditorExpand, onToggleEditorExpand,
projectPath, projectPath,
fillSpace,
}: EditorSidebarProps) { }: EditorSidebarProps) {
const [poppedOut, setPoppedOut] = useState(false);
if (!editingFile) { if (!editingFile) {
return null; return null;
} }
if (isMobile) { if (isMobile || poppedOut) {
return ( return (
<AnyCodeEditor <AnyCodeEditor
file={editingFile} file={editingFile}
onClose={onCloseEditor} onClose={() => {
setPoppedOut(false);
onCloseEditor();
}}
projectPath={projectPath} projectPath={projectPath}
isSidebar={false} isSidebar={false}
/> />
); );
} }
const useFlex = editorExpanded || fillSpace;
return ( return (
<> <>
{!editorExpanded && ( {!editorExpanded && (
@@ -43,8 +52,8 @@ export default function EditorSidebar({
)} )}
<div <div
className={`flex-shrink-0 border-l border-gray-200 dark:border-gray-700 h-full overflow-hidden ${editorExpanded ? 'flex-1' : ''}`} className={`flex-shrink-0 border-l border-gray-200 dark:border-gray-700 h-full overflow-hidden ${useFlex ? 'flex-1' : ''}`}
style={editorExpanded ? undefined : { width: `${editorWidth}px` }} style={useFlex ? undefined : { width: `${editorWidth}px` }}
> >
<AnyCodeEditor <AnyCodeEditor
file={editingFile} file={editingFile}
@@ -53,6 +62,7 @@ export default function EditorSidebar({
isSidebar isSidebar
isExpanded={editorExpanded} isExpanded={editorExpanded}
onToggleExpand={onToggleEditorExpand} onToggleExpand={onToggleEditorExpand}
onPopOut={() => setPoppedOut(true)}
/> />
</div> </div>
</> </>