Files
claudecodeui/src/components/CodeEditor.jsx
simos 51431832d8 style(ui): improve mobile responsiveness of status and navigation components
Refactor component spacing, typography, and layout to better support mobile
devices while maintaining desktop experience. Changes include:

- Reduce bottom margins and padding on mobile (mb-3 vs mb-6, px-2.5 vs px-4)
- Use responsive text sizes (text-xs sm:text-sm, text-base sm:text-xl)
- Add truncation and min-w-0 to prevent text overflow on small screens
- Hide non-essential info on mobile (token counts, keyboard shortcuts)
- Adjust gap spacing for tighter mobile layouts (gap-1.5 vs gap-3)
- Improve button touch targets with refined padding
- Update background colors for better contrast (gray-800 vs gray-900)
- Add border styling for enhanced component definition

Affects ClaudeStatus, CodeEditor, GitPanel, MainContent, MobileNav,
QuickSettingsPanel components and global styles. Ensures consistent
mobile-first design across the application.
2025-11-07 03:39:15 +00:00

704 lines
27 KiB
JavaScript

import React, { useState, useEffect, useRef, useMemo } from 'react';
import CodeMirror from '@uiw/react-codemirror';
import { javascript } from '@codemirror/lang-javascript';
import { python } from '@codemirror/lang-python';
import { html } from '@codemirror/lang-html';
import { css } from '@codemirror/lang-css';
import { json } from '@codemirror/lang-json';
import { markdown } from '@codemirror/lang-markdown';
import { oneDark } from '@codemirror/theme-one-dark';
import { EditorView, showPanel, ViewPlugin } from '@codemirror/view';
import { unifiedMergeView, getChunks } from '@codemirror/merge';
import { showMinimap } from '@replit/codemirror-minimap';
import { X, Save, Download, Maximize2, Minimize2 } from 'lucide-react';
import { api } from '../utils/api';
function CodeEditor({ file, onClose, projectPath, isSidebar = false, isExpanded = false, onToggleExpand = null }) {
const [content, setContent] = useState('');
const [loading, setLoading] = useState(true);
const [saving, setSaving] = useState(false);
const [isFullscreen, setIsFullscreen] = useState(false);
const [isDarkMode, setIsDarkMode] = useState(() => {
const savedTheme = localStorage.getItem('codeEditorTheme');
return savedTheme ? savedTheme === 'dark' : true;
});
const [saveSuccess, setSaveSuccess] = useState(false);
const [showDiff, setShowDiff] = useState(!!file.diffInfo);
const [wordWrap, setWordWrap] = useState(() => {
return localStorage.getItem('codeEditorWordWrap') === 'true';
});
const [minimapEnabled, setMinimapEnabled] = useState(() => {
return localStorage.getItem('codeEditorShowMinimap') !== 'false';
});
const [showLineNumbers, setShowLineNumbers] = useState(() => {
return localStorage.getItem('codeEditorLineNumbers') !== 'false';
});
const [fontSize, setFontSize] = useState(() => {
return localStorage.getItem('codeEditorFontSize') || '14';
});
const editorRef = useRef(null);
// Create minimap extension with chunk-based gutters
const minimapExtension = useMemo(() => {
if (!file.diffInfo || !showDiff || !minimapEnabled) return [];
const gutters = {};
return [
showMinimap.compute(['doc'], (state) => {
// Get actual chunks from merge view
const chunksData = getChunks(state);
const chunks = chunksData?.chunks || [];
// Clear previous gutters
Object.keys(gutters).forEach(key => delete gutters[key]);
// Mark lines that are part of chunks
chunks.forEach(chunk => {
// Mark the lines in the B side (current document)
const fromLine = state.doc.lineAt(chunk.fromB).number;
const toLine = state.doc.lineAt(Math.min(chunk.toB, state.doc.length)).number;
for (let lineNum = fromLine; lineNum <= toLine; lineNum++) {
gutters[lineNum] = isDarkMode ? 'rgba(34, 197, 94, 0.8)' : 'rgba(34, 197, 94, 1)';
}
});
return {
create: () => ({ dom: document.createElement('div') }),
displayText: 'blocks',
showOverlay: 'always',
gutters: [gutters]
};
})
];
}, [file.diffInfo, showDiff, minimapEnabled, isDarkMode]);
// Create extension to scroll to first chunk on mount
const scrollToFirstChunkExtension = useMemo(() => {
if (!file.diffInfo || !showDiff) return [];
return [
ViewPlugin.fromClass(class {
constructor(view) {
// Delay to ensure merge view is fully initialized
setTimeout(() => {
const chunksData = getChunks(view.state);
const chunks = chunksData?.chunks || [];
if (chunks.length > 0) {
const firstChunk = chunks[0];
// Scroll to the first chunk
view.dispatch({
effects: EditorView.scrollIntoView(firstChunk.fromB, { y: 'center' })
});
}
}, 100);
}
update() {}
destroy() {}
})
];
}, [file.diffInfo, showDiff]);
// Create editor toolbar panel - always visible
const editorToolbarPanel = useMemo(() => {
const createPanel = (view) => {
const dom = document.createElement('div');
dom.className = 'cm-editor-toolbar-panel';
let currentIndex = 0;
const updatePanel = () => {
// Check if we have diff info and it's enabled
const hasDiff = file.diffInfo && showDiff;
const chunksData = hasDiff ? getChunks(view.state) : null;
const chunks = chunksData?.chunks || [];
const chunkCount = chunks.length;
// Build the toolbar HTML
let toolbarHTML = '<div style="display: flex; align-items: center; justify-content: space-between; width: 100%;">';
// Left side - diff navigation (if applicable)
toolbarHTML += '<div style="display: flex; align-items: center; gap: 8px;">';
if (hasDiff) {
toolbarHTML += `
<span style="font-weight: 500;">${chunkCount > 0 ? `${currentIndex + 1}/${chunkCount}` : '0'} changes</span>
<button class="cm-diff-nav-btn cm-diff-nav-prev" title="Previous change" ${chunkCount === 0 ? 'disabled' : ''}>
<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="M5 15l7-7 7 7" />
</svg>
</button>
<button class="cm-diff-nav-btn cm-diff-nav-next" title="Next change" ${chunkCount === 0 ? 'disabled' : ''}>
<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="M19 9l-7 7-7-7" />
</svg>
</button>
`;
}
toolbarHTML += '</div>';
// Right side - action buttons
toolbarHTML += '<div style="display: flex; align-items: center; gap: 4px;">';
// Show/hide diff button (only if there's diff info)
if (file.diffInfo) {
toolbarHTML += `
<button class="cm-toolbar-btn cm-toggle-diff-btn" title="${showDiff ? 'Hide diff highlighting' : 'Show diff highlighting'}">
<svg width="16" height="16" fill="none" stroke="currentColor" viewBox="0 0 24 24">
${showDiff ?
'<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13.875 18.825A10.05 10.05 0 0112 19c-4.478 0-8.268-2.943-9.543-7a9.97 9.97 0 011.563-3.029m5.858.908a3 3 0 114.243 4.243M9.878 9.878l4.242 4.242M9.88 9.88l-3.29-3.29m7.532 7.532l3.29 3.29M3 3l3.59 3.59m0 0A9.953 9.953 0 0112 5c4.478 0 8.268 2.943 9.543 7a10.025 10.025 0 01-4.132 5.411m0 0L21 21" />' :
'<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z" /><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M2.458 12C3.732 7.943 7.523 5 12 5c4.478 0 8.268 2.943 9.542 7-1.274 4.057-5.064 7-9.542 7-4.477 0-8.268-2.943-9.542-7z" />'
}
</svg>
</button>
`;
}
// Settings button
toolbarHTML += `
<button class="cm-toolbar-btn cm-settings-btn" title="Editor Settings">
<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="M10.325 4.317c.426-1.756 2.924-1.756 3.35 0a1.724 1.724 0 002.573 1.066c1.543-.94 3.31.826 2.37 2.37a1.724 1.724 0 001.065 2.572c1.756.426 1.756 2.924 0 3.35a1.724 1.724 0 00-1.066 2.573c.94 1.543-.826 3.31-2.37 2.37a1.724 1.724 0 00-2.572 1.065c-.426 1.756-2.924 1.756-3.35 0a1.724 1.724 0 00-2.573-1.066c-1.543.94-3.31-.826-2.37-2.37a1.724 1.724 0 00-1.065-2.572c-1.756-.426-1.756-2.924 0-3.35a1.724 1.724 0 001.066-2.573c-.94-1.543.826-3.31 2.37-2.37.996.608 2.296.07 2.572-1.065z" /><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z" />
</svg>
</button>
`;
// Expand button (only in sidebar mode)
if (isSidebar && onToggleExpand) {
toolbarHTML += `
<button class="cm-toolbar-btn cm-expand-btn" title="${isExpanded ? 'Collapse editor' : 'Expand editor to full width'}">
<svg width="16" height="16" fill="none" stroke="currentColor" viewBox="0 0 24 24">
${isExpanded ?
'<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 9V4.5M9 9H4.5M9 9L3.75 3.75M9 15v4.5M9 15H4.5M9 15l-5.25 5.25M15 9h4.5M15 9V4.5M15 9l5.25-5.25M15 15h4.5M15 15v4.5m0-4.5l5.25 5.25" />' :
'<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 8V4m0 0h4M4 4l5 5m11-1V4m0 0h-4m4 0l-5 5M4 16v4m0 0h4m-4 0l5-5m11 5l-5-5m5 5v-4m0 4h-4" />'
}
</svg>
</button>
`;
}
toolbarHTML += '</div>';
toolbarHTML += '</div>';
dom.innerHTML = toolbarHTML;
// Attach event listeners for diff navigation
if (hasDiff) {
const prevBtn = dom.querySelector('.cm-diff-nav-prev');
const nextBtn = dom.querySelector('.cm-diff-nav-next');
prevBtn?.addEventListener('click', () => {
if (chunks.length === 0) return;
currentIndex = currentIndex > 0 ? currentIndex - 1 : chunks.length - 1;
const chunk = chunks[currentIndex];
if (chunk) {
view.dispatch({
effects: EditorView.scrollIntoView(chunk.fromB, { y: 'center' })
});
}
updatePanel();
});
nextBtn?.addEventListener('click', () => {
if (chunks.length === 0) return;
currentIndex = currentIndex < chunks.length - 1 ? currentIndex + 1 : 0;
const chunk = chunks[currentIndex];
if (chunk) {
view.dispatch({
effects: EditorView.scrollIntoView(chunk.fromB, { y: 'center' })
});
}
updatePanel();
});
}
// Attach event listener for toggle diff button
if (file.diffInfo) {
const toggleDiffBtn = dom.querySelector('.cm-toggle-diff-btn');
toggleDiffBtn?.addEventListener('click', () => {
setShowDiff(!showDiff);
});
}
// Attach event listener for settings button
const settingsBtn = dom.querySelector('.cm-settings-btn');
settingsBtn?.addEventListener('click', () => {
if (window.openSettings) {
window.openSettings('appearance');
}
});
// Attach event listener for expand button
if (isSidebar && onToggleExpand) {
const expandBtn = dom.querySelector('.cm-expand-btn');
expandBtn?.addEventListener('click', () => {
onToggleExpand();
});
}
};
updatePanel();
return {
top: true,
dom,
update: updatePanel
};
};
return [showPanel.of(createPanel)];
}, [file.diffInfo, showDiff, isSidebar, isExpanded, onToggleExpand]);
// Get language extension based on file extension
const getLanguageExtension = (filename) => {
const ext = filename.split('.').pop()?.toLowerCase();
switch (ext) {
case 'js':
case 'jsx':
case 'ts':
case 'tsx':
return [javascript({ jsx: true, typescript: ext.includes('ts') })];
case 'py':
return [python()];
case 'html':
case 'htm':
return [html()];
case 'css':
case 'scss':
case 'less':
return [css()];
case 'json':
return [json()];
case 'md':
case 'markdown':
return [markdown()];
default:
return [];
}
};
// Load file content
useEffect(() => {
const loadFileContent = async () => {
try {
setLoading(true);
// If we have diffInfo with both old and new content, we can show the diff directly
// This handles both GitPanel (full content) and ChatInterface (full content from API)
if (file.diffInfo && file.diffInfo.new_string !== undefined && file.diffInfo.old_string !== undefined) {
// Use the new_string as the content to display
// The unifiedMergeView will compare it against old_string
setContent(file.diffInfo.new_string);
setLoading(false);
return;
}
// Otherwise, load from disk
const response = await api.readFile(file.projectName, file.path);
if (!response.ok) {
throw new Error(`Failed to load file: ${response.status} ${response.statusText}`);
}
const data = await response.json();
setContent(data.content);
} catch (error) {
console.error('Error loading file:', error);
setContent(`// Error loading file: ${error.message}\n// File: ${file.name}\n// Path: ${file.path}`);
} finally {
setLoading(false);
}
};
loadFileContent();
}, [file, projectPath]);
const handleSave = async () => {
setSaving(true);
try {
console.log('Saving file:', {
projectName: file.projectName,
path: file.path,
contentLength: content?.length
});
const response = await api.saveFile(file.projectName, file.path, content);
console.log('Save response:', {
status: response.status,
ok: response.ok,
contentType: response.headers.get('content-type')
});
if (!response.ok) {
const contentType = response.headers.get('content-type');
if (contentType && contentType.includes('application/json')) {
const errorData = await response.json();
throw new Error(errorData.error || `Save failed: ${response.status}`);
} else {
const textError = await response.text();
console.error('Non-JSON error response:', textError);
throw new Error(`Save failed: ${response.status} ${response.statusText}`);
}
}
const result = await response.json();
console.log('Save successful:', result);
setSaveSuccess(true);
setTimeout(() => setSaveSuccess(false), 2000);
} catch (error) {
console.error('Error saving file:', error);
alert(`Error saving file: ${error.message}`);
} finally {
setSaving(false);
}
};
const handleDownload = () => {
const blob = new Blob([content], { type: 'text/plain' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = file.name;
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
URL.revokeObjectURL(url);
};
const toggleFullscreen = () => {
setIsFullscreen(!isFullscreen);
};
// Save theme preference to localStorage
useEffect(() => {
localStorage.setItem('codeEditorTheme', isDarkMode ? 'dark' : 'light');
}, [isDarkMode]);
// Save word wrap preference to localStorage
useEffect(() => {
localStorage.setItem('codeEditorWordWrap', wordWrap.toString());
}, [wordWrap]);
// Listen for settings changes from the Settings modal
useEffect(() => {
const handleStorageChange = () => {
const newTheme = localStorage.getItem('codeEditorTheme');
if (newTheme) {
setIsDarkMode(newTheme === 'dark');
}
const newWordWrap = localStorage.getItem('codeEditorWordWrap');
if (newWordWrap !== null) {
setWordWrap(newWordWrap === 'true');
}
const newShowMinimap = localStorage.getItem('codeEditorShowMinimap');
if (newShowMinimap !== null) {
setMinimapEnabled(newShowMinimap !== 'false');
}
const newShowLineNumbers = localStorage.getItem('codeEditorLineNumbers');
if (newShowLineNumbers !== null) {
setShowLineNumbers(newShowLineNumbers !== 'false');
}
const newFontSize = localStorage.getItem('codeEditorFontSize');
if (newFontSize) {
setFontSize(newFontSize);
}
};
// Listen for storage events (changes from other tabs/windows)
window.addEventListener('storage', handleStorageChange);
// Custom event for same-window updates
window.addEventListener('codeEditorSettingsChanged', handleStorageChange);
return () => {
window.removeEventListener('storage', handleStorageChange);
window.removeEventListener('codeEditorSettingsChanged', handleStorageChange);
};
}, []);
// Handle keyboard shortcuts
useEffect(() => {
const handleKeyDown = (e) => {
if (e.ctrlKey || e.metaKey) {
if (e.key === 's') {
e.preventDefault();
handleSave();
} else if (e.key === 'Escape') {
e.preventDefault();
onClose();
}
}
};
document.addEventListener('keydown', handleKeyDown);
return () => document.removeEventListener('keydown', handleKeyDown);
}, [content]);
if (loading) {
return (
<>
<style>
{`
.code-editor-loading {
background-color: ${isDarkMode ? '#111827' : '#ffffff'} !important;
}
.code-editor-loading:hover {
background-color: ${isDarkMode ? '#111827' : '#ffffff'} !important;
}
`}
</style>
{isSidebar ? (
<div className="w-full h-full flex items-center justify-center bg-background">
<div className="flex items-center gap-3">
<div className="animate-spin rounded-full h-6 w-6 border-b-2 border-blue-600"></div>
<span className="text-gray-900 dark:text-white">Loading {file.name}...</span>
</div>
</div>
) : (
<div className="fixed inset-0 z-40 md:bg-black/50 md:flex md:items-center md:justify-center">
<div className="code-editor-loading w-full h-full md:rounded-lg md:w-auto md:h-auto p-8 flex items-center justify-center">
<div className="flex items-center gap-3">
<div className="animate-spin rounded-full h-6 w-6 border-b-2 border-blue-600"></div>
<span className="text-gray-900 dark:text-white">Loading {file.name}...</span>
</div>
</div>
</div>
)}
</>
);
}
return (
<>
<style>
{`
/* Light background for full line changes */
.cm-deletedChunk {
background-color: ${isDarkMode ? 'rgba(239, 68, 68, 0.15)' : 'rgba(255, 235, 235, 1)'} !important;
border-left: 3px solid ${isDarkMode ? 'rgba(239, 68, 68, 0.6)' : 'rgb(239, 68, 68)'} !important;
padding-left: 4px !important;
}
.cm-insertedChunk {
background-color: ${isDarkMode ? 'rgba(34, 197, 94, 0.15)' : 'rgba(230, 255, 237, 1)'} !important;
border-left: 3px solid ${isDarkMode ? 'rgba(34, 197, 94, 0.6)' : 'rgb(34, 197, 94)'} !important;
padding-left: 4px !important;
}
/* Override linear-gradient underline and use solid darker background for partial changes */
.cm-editor.cm-merge-b .cm-changedText {
background: ${isDarkMode ? 'rgba(34, 197, 94, 0.4)' : 'rgba(34, 197, 94, 0.3)'} !important;
padding-top: 2px !important;
padding-bottom: 2px !important;
margin-top: -2px !important;
margin-bottom: -2px !important;
}
.cm-editor .cm-deletedChunk .cm-changedText {
background: ${isDarkMode ? 'rgba(239, 68, 68, 0.4)' : 'rgba(239, 68, 68, 0.3)'} !important;
padding-top: 2px !important;
padding-bottom: 2px !important;
margin-top: -2px !important;
margin-bottom: -2px !important;
}
/* Minimap gutter styling */
.cm-gutter.cm-gutter-minimap {
background-color: ${isDarkMode ? '#1e1e1e' : '#f5f5f5'};
}
/* Editor toolbar panel styling */
.cm-editor-toolbar-panel {
padding: 8px 12px;
background-color: ${isDarkMode ? '#1f2937' : '#ffffff'};
border-bottom: 1px solid ${isDarkMode ? '#374151' : '#e5e7eb'};
color: ${isDarkMode ? '#d1d5db' : '#374151'};
font-size: 14px;
}
.cm-diff-nav-btn,
.cm-toolbar-btn {
padding: 4px;
background: transparent;
border: none;
cursor: pointer;
border-radius: 4px;
display: inline-flex;
align-items: center;
justify-content: center;
color: inherit;
transition: background-color 0.2s;
}
.cm-diff-nav-btn:hover,
.cm-toolbar-btn:hover {
background-color: ${isDarkMode ? '#374151' : '#f3f4f6'};
}
.cm-diff-nav-btn:disabled {
opacity: 0.5;
cursor: not-allowed;
}
`}
</style>
<div className={isSidebar ?
'w-full h-full flex flex-col' :
`fixed inset-0 z-40 ${
// Mobile: native fullscreen, Desktop: modal with backdrop
'md:bg-black/50 md:flex md:items-center md:justify-center md:p-4'
} ${isFullscreen ? 'md:p-0' : ''}`}>
<div className={isSidebar ?
'bg-background flex flex-col w-full h-full' :
`bg-background shadow-2xl flex flex-col ${
// Mobile: always fullscreen, Desktop: modal sizing
'w-full h-full md:rounded-lg md:shadow-2xl' +
(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 */}
<div className="flex items-center justify-between p-4 border-b border-border flex-shrink-0 min-w-0">
<div className="flex items-center gap-3 min-w-0 flex-1">
<div className="min-w-0 flex-1">
<div className="flex items-center gap-2 min-w-0">
<h3 className="font-medium text-gray-900 dark:text-white truncate">{file.name}</h3>
{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">
Showing changes
</span>
)}
</div>
<p className="text-sm text-gray-500 dark:text-gray-400 truncate">{file.path}</p>
</div>
</div>
<div className="flex items-center gap-1 md:gap-2 flex-shrink-0">
<button
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"
title="Download file"
>
<Download className="w-5 h-5 md:w-4 md:h-4" />
</button>
<button
onClick={handleSave}
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 ${
saveSuccess
? 'bg-green-600 hover:bg-green-700'
: 'bg-blue-600 hover:bg-blue-700'
}`}
>
{saveSuccess ? (
<>
<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" />
</svg>
<span className="hidden sm:inline">Saved!</span>
</>
) : (
<>
<Save className="w-5 h-5 md:w-4 md:h-4" />
<span className="hidden sm:inline">{saving ? 'Saving...' : 'Save'}</span>
</>
)}
</button>
{!isSidebar && (
<button
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"
title={isFullscreen ? 'Exit fullscreen' : 'Fullscreen'}
>
{isFullscreen ? <Minimize2 className="w-4 h-4" /> : <Maximize2 className="w-4 h-4" />}
</button>
)}
<button
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"
title="Close"
>
<X className="w-6 h-6 md:w-4 md:h-4" />
</button>
</div>
</div>
{/* Editor */}
<div className="flex-1 overflow-hidden">
<CodeMirror
ref={editorRef}
value={content}
onChange={setContent}
extensions={[
...getLanguageExtension(file.name),
// Always show the toolbar
...editorToolbarPanel,
// Only show diff-related extensions when diff is enabled
...(file.diffInfo && showDiff && file.diffInfo.old_string !== undefined
? [
unifiedMergeView({
original: file.diffInfo.old_string,
mergeControls: false,
highlightChanges: true,
syntaxHighlightDeletions: false,
gutter: true
// NOTE: NO collapseUnchanged - this shows the full file!
}),
...minimapExtension,
...scrollToFirstChunkExtension
]
: []),
...(wordWrap ? [EditorView.lineWrapping] : [])
]}
theme={isDarkMode ? oneDark : undefined}
height="100%"
style={{
fontSize: `${fontSize}px`,
height: '100%',
}}
basicSetup={{
lineNumbers: showLineNumbers,
foldGutter: true,
dropCursor: false,
allowMultipleSelections: false,
indentOnInput: true,
bracketMatching: true,
closeBrackets: true,
autocompletion: true,
highlightSelectionMatches: true,
searchKeymap: true,
}}
/>
</div>
{/* Footer */}
<div className="flex items-center justify-between p-3 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">
<span>Lines: {content.split('\n').length}</span>
<span>Characters: {content.length}</span>
</div>
<div className="text-sm text-gray-500 dark:text-gray-400">
Press Ctrl+S to save Esc to close
</div>
</div>
</div>
</div>
</>
);
}
export default CodeEditor;