feat: Implement CodeMirror 6 file editor with tab support
Implement Phase 1 of the file editor & chat UI redesign: - CodeMirror 6 integration with syntax highlighting - Multi-file tab support with dirty state tracking - Custom dark theme matching GitHub's color scheme - Keyboard shortcuts (Ctrl+S to save, Ctrl+W to close tab) - Mobile-responsive design with proper touch targets - Fallback to basic textarea if CodeMirror fails to load Technical details: - Import map for ESM modules from node_modules - Language support for JS, Python, HTML, CSS, JSON, Markdown - Auto-initialization on DOM ready - Global window.fileEditor instance for integration - Serve node_modules at /claude/node_modules for import map Files added: - public/claude-ide/components/file-editor.js (main component) - public/claude-ide/components/file-editor.css (responsive styles) Files modified: - public/claude-ide/index.html (import map, script tags) - public/claude-ide/ide.js (updated loadFile function) - server.js (serve node_modules for CodeMirror) Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
421
public/claude-ide/components/file-editor.css
Normal file
421
public/claude-ide/components/file-editor.css
Normal file
@@ -0,0 +1,421 @@
|
|||||||
|
/**
|
||||||
|
* File Editor Component Styles
|
||||||
|
* Mobile-first responsive design for CodeMirror 6 editor
|
||||||
|
*/
|
||||||
|
|
||||||
|
/* === File Editor Container === */
|
||||||
|
.file-editor-container {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
height: 100%;
|
||||||
|
background: #0d1117;
|
||||||
|
color: #c9d1d9;
|
||||||
|
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Helvetica, Arial, sans-serif;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* === Editor Header (Tabs + Actions) === */
|
||||||
|
.editor-header {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
background: #161b22;
|
||||||
|
border-bottom: 1px solid #30363d;
|
||||||
|
padding: 0;
|
||||||
|
min-height: 40px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.editor-tabs {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
flex: 1;
|
||||||
|
overflow-x: auto;
|
||||||
|
overflow-y: hidden;
|
||||||
|
scrollbar-width: thin;
|
||||||
|
scrollbar-color: #484f58 #161b22;
|
||||||
|
}
|
||||||
|
|
||||||
|
.editor-tabs::-webkit-scrollbar {
|
||||||
|
height: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.editor-tabs::-webkit-scrollbar-track {
|
||||||
|
background: #161b22;
|
||||||
|
}
|
||||||
|
|
||||||
|
.editor-tabs::-webkit-scrollbar-thumb {
|
||||||
|
background: #484f58;
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.editor-tabs::-webkit-scrollbar-thumb:hover {
|
||||||
|
background: #6e7681;
|
||||||
|
}
|
||||||
|
|
||||||
|
.editor-actions {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
padding: 0 8px;
|
||||||
|
gap: 4px;
|
||||||
|
border-left: 1px solid #30363d;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* === Editor Tabs === */
|
||||||
|
.editor-tab {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 6px;
|
||||||
|
padding: 8px 12px;
|
||||||
|
background: transparent;
|
||||||
|
border: none;
|
||||||
|
border-right: 1px solid #30363d;
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: 13px;
|
||||||
|
color: #8b949e;
|
||||||
|
transition: background 0.15s ease, color 0.15s ease;
|
||||||
|
white-space: nowrap;
|
||||||
|
user-select: none;
|
||||||
|
min-width: fit-content;
|
||||||
|
}
|
||||||
|
|
||||||
|
.editor-tab:hover {
|
||||||
|
background: #21262d;
|
||||||
|
color: #c9d1d9;
|
||||||
|
}
|
||||||
|
|
||||||
|
.editor-tab.active {
|
||||||
|
background: #0d1117;
|
||||||
|
color: #c9d1d9;
|
||||||
|
border-top: 2px solid #58a6ff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.editor-tab.dirty .tab-name {
|
||||||
|
color: #e3b341;
|
||||||
|
}
|
||||||
|
|
||||||
|
.editor-tab.dirty .tab-dirty-indicator {
|
||||||
|
color: #e3b341;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tab-name {
|
||||||
|
flex: 1;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
max-width: 200px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tab-dirty-indicator {
|
||||||
|
font-size: 10px;
|
||||||
|
animation: pulse 2s ease-in-out infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes pulse {
|
||||||
|
0%, 100% { opacity: 1; }
|
||||||
|
50% { opacity: 0.5; }
|
||||||
|
}
|
||||||
|
|
||||||
|
.tab-close {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
width: 18px;
|
||||||
|
height: 18px;
|
||||||
|
padding: 0;
|
||||||
|
background: transparent;
|
||||||
|
border: none;
|
||||||
|
color: #8b949e;
|
||||||
|
cursor: pointer;
|
||||||
|
border-radius: 3px;
|
||||||
|
font-size: 16px;
|
||||||
|
line-height: 1;
|
||||||
|
transition: all 0.15s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tab-close:hover {
|
||||||
|
background: #484f58;
|
||||||
|
color: #c9d1d9;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* === Editor Content Area === */
|
||||||
|
.editor-content {
|
||||||
|
flex: 1;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
overflow: hidden;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* === CodeMirror Editor Instance === */
|
||||||
|
.cm-editor-instance {
|
||||||
|
height: 100%;
|
||||||
|
width: 100%;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* === Editor Placeholder === */
|
||||||
|
.editor-placeholder {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
height: 100%;
|
||||||
|
color: #484f58;
|
||||||
|
text-align: center;
|
||||||
|
padding: 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.placeholder-icon {
|
||||||
|
font-size: 4rem;
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
opacity: 0.5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.editor-placeholder h2 {
|
||||||
|
font-size: 1.5rem;
|
||||||
|
font-weight: 600;
|
||||||
|
margin-bottom: 0.5rem;
|
||||||
|
color: #8b949e;
|
||||||
|
}
|
||||||
|
|
||||||
|
.editor-placeholder p {
|
||||||
|
font-size: 1rem;
|
||||||
|
color: #484f58;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* === Fallback Editor (when CodeMirror fails) === */
|
||||||
|
.fallback-editor {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
background: #0d1117;
|
||||||
|
color: #c9d1d9;
|
||||||
|
border: none;
|
||||||
|
outline: none;
|
||||||
|
resize: none;
|
||||||
|
font-family: 'Fira Code', 'JetBrains Mono', 'SF Mono', 'Menlo', 'Consolas', monospace;
|
||||||
|
font-size: 14px;
|
||||||
|
line-height: 1.6;
|
||||||
|
padding: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* === Action Buttons === */
|
||||||
|
.btn-icon {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
width: 32px;
|
||||||
|
height: 32px;
|
||||||
|
padding: 0;
|
||||||
|
background: transparent;
|
||||||
|
border: none;
|
||||||
|
color: #8b949e;
|
||||||
|
cursor: pointer;
|
||||||
|
border-radius: 4px;
|
||||||
|
font-size: 16px;
|
||||||
|
transition: all 0.15s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-icon:hover {
|
||||||
|
background: #21262d;
|
||||||
|
color: #c9d1d9;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-icon:active {
|
||||||
|
transform: scale(0.95);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* === CodeMirror Customization === */
|
||||||
|
.codemirror-editor {
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.codemirror-editor .cm-scroller {
|
||||||
|
font-family: 'Fira Code', 'JetBrains Mono', 'SF Mono', 'Menlo', 'Consolas', monospace;
|
||||||
|
font-size: 14px;
|
||||||
|
line-height: 1.6;
|
||||||
|
}
|
||||||
|
|
||||||
|
.codemirror-editor .cm-content {
|
||||||
|
padding: 12px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.codemirror-editor .cm-line {
|
||||||
|
padding: 0 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* === Mobile Responsive === */
|
||||||
|
@media (max-width: 640px) {
|
||||||
|
.editor-header {
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: stretch;
|
||||||
|
}
|
||||||
|
|
||||||
|
.editor-tabs {
|
||||||
|
border-right: none;
|
||||||
|
border-bottom: 1px solid #30363d;
|
||||||
|
}
|
||||||
|
|
||||||
|
.editor-actions {
|
||||||
|
border-left: none;
|
||||||
|
border-top: 1px solid #30363d;
|
||||||
|
padding: 4px;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.editor-tab {
|
||||||
|
padding: 10px 8px;
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tab-name {
|
||||||
|
max-width: 120px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tab-close {
|
||||||
|
width: 28px;
|
||||||
|
height: 28px;
|
||||||
|
font-size: 18px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-icon {
|
||||||
|
width: 36px;
|
||||||
|
height: 36px;
|
||||||
|
font-size: 18px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.editor-placeholder h2 {
|
||||||
|
font-size: 1.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.editor-placeholder p {
|
||||||
|
font-size: 0.875rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* === Tablet Responsive === */
|
||||||
|
@media (min-width: 641px) and (max-width: 1024px) {
|
||||||
|
.tab-name {
|
||||||
|
max-width: 150px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* === Touch Targets (Mobile) === */
|
||||||
|
@media (hover: none) and (pointer: coarse) {
|
||||||
|
.editor-tab {
|
||||||
|
padding: 12px;
|
||||||
|
min-height: 44px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tab-close {
|
||||||
|
width: 44px;
|
||||||
|
height: 44px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-icon {
|
||||||
|
width: 44px;
|
||||||
|
height: 44px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* === Dark Mode Scrollbar for Editor === */
|
||||||
|
.cm-editor-instance ::-webkit-scrollbar {
|
||||||
|
width: 14px;
|
||||||
|
height: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cm-editor-instance ::-webkit-scrollbar-track {
|
||||||
|
background: #0d1117;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cm-editor-instance ::-webkit-scrollbar-thumb {
|
||||||
|
background: #30363d;
|
||||||
|
border-radius: 7px;
|
||||||
|
border: 3px solid #0d1117;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cm-editor-instance ::-webkit-scrollbar-thumb:hover {
|
||||||
|
background: #484f58;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cm-editor-instance ::-webkit-scrollbar-corner {
|
||||||
|
background: #0d1117;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* === Status Messages === */
|
||||||
|
.status-message {
|
||||||
|
font-size: 12px;
|
||||||
|
padding: 4px 8px;
|
||||||
|
border-radius: 4px;
|
||||||
|
animation: fadeIn 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-success {
|
||||||
|
color: #3fb950;
|
||||||
|
background: rgba(63, 185, 80, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-error {
|
||||||
|
color: #f85149;
|
||||||
|
background: rgba(248, 81, 73, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-info {
|
||||||
|
color: #58a6ff;
|
||||||
|
background: rgba(88, 166, 255, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes fadeIn {
|
||||||
|
from {
|
||||||
|
opacity: 0;
|
||||||
|
transform: translateY(-2px);
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
opacity: 1;
|
||||||
|
transform: translateY(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* === Loading Spinner === */
|
||||||
|
.loading-spinner {
|
||||||
|
width: 40px;
|
||||||
|
height: 40px;
|
||||||
|
border: 3px solid #30363d;
|
||||||
|
border-top-color: #58a6ff;
|
||||||
|
border-radius: 50%;
|
||||||
|
animation: spin 0.8s linear infinite;
|
||||||
|
margin: 2rem auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes spin {
|
||||||
|
to {
|
||||||
|
transform: rotate(360deg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* === No Files State === */
|
||||||
|
.no-tabs {
|
||||||
|
padding: 8px 12px;
|
||||||
|
color: #484f58;
|
||||||
|
font-size: 13px;
|
||||||
|
font-style: italic;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* === Focus Styles for Accessibility === */
|
||||||
|
.editor-tab:focus-visible,
|
||||||
|
.tab-close:focus-visible,
|
||||||
|
.btn-icon:focus-visible {
|
||||||
|
outline: 2px solid #58a6ff;
|
||||||
|
outline-offset: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* === Print Styles === */
|
||||||
|
@media print {
|
||||||
|
.editor-header,
|
||||||
|
.editor-actions {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.editor-content {
|
||||||
|
height: auto;
|
||||||
|
overflow: visible;
|
||||||
|
}
|
||||||
|
}
|
||||||
663
public/claude-ide/components/file-editor.js
Normal file
663
public/claude-ide/components/file-editor.js
Normal file
@@ -0,0 +1,663 @@
|
|||||||
|
/**
|
||||||
|
* File Editor with CodeMirror 6
|
||||||
|
* Supports: Multi-file tabs, syntax highlighting, dirty state tracking
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { EditorState, Compartment } from '@codemirror/state';
|
||||||
|
import { EditorView, keymap, highlightSpecialChars, drawSelection, dropCursor, lineNumbers, rectangularSelection, crosshairCursor, highlightActiveLine, highlightSelectionMatches, EditorView as cmEditorView } from '@codemirror/view';
|
||||||
|
import { defaultKeymap, history, historyKeymap } from '@codemirror/commands';
|
||||||
|
import { searchKeymap, highlightSelectionMatches as searchHighlightMatches } from '@codemirror/search';
|
||||||
|
import { autocompletion, completionKeymap, closeBrackets, closeBracketsKeymap } from '@codemirror/autocomplete';
|
||||||
|
import { bracketMatching, codeFolding, foldGutter, syntaxHighlighting, defaultHighlightStyle } from '@codemirror/language';
|
||||||
|
import { tags } from '@lezer/highlight';
|
||||||
|
|
||||||
|
// Language support
|
||||||
|
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';
|
||||||
|
|
||||||
|
// Custom dark theme matching GitHub's dark theme
|
||||||
|
const customDarkTheme = cmEditorView.theme({
|
||||||
|
'&': {
|
||||||
|
backgroundColor: '#0d1117',
|
||||||
|
color: '#c9d1d9',
|
||||||
|
fontSize: '14px',
|
||||||
|
fontFamily: "'Fira Code', 'JetBrains Mono', 'SF Mono', 'Menlo', 'Consolas', monospace"
|
||||||
|
},
|
||||||
|
'.cm-scroller': {
|
||||||
|
fontFamily: 'inherit',
|
||||||
|
overflow: 'auto'
|
||||||
|
},
|
||||||
|
'.cm-content': {
|
||||||
|
padding: '12px 0',
|
||||||
|
minHeight: '100%'
|
||||||
|
},
|
||||||
|
'.cm-line': {
|
||||||
|
padding: '0 12px'
|
||||||
|
},
|
||||||
|
'.cm-gutters': {
|
||||||
|
backgroundColor: '#0d1117',
|
||||||
|
color: '#484f58',
|
||||||
|
border: 'none'
|
||||||
|
},
|
||||||
|
'.cm-activeLineGutter': {
|
||||||
|
backgroundColor: 'transparent',
|
||||||
|
color: '#c9d1d9'
|
||||||
|
},
|
||||||
|
'.cm-activeLine': {
|
||||||
|
backgroundColor: '#161b22'
|
||||||
|
},
|
||||||
|
'.cm-focused': {
|
||||||
|
outline: 'none'
|
||||||
|
},
|
||||||
|
'.cm-selectionBackground': {
|
||||||
|
backgroundColor: '#264f78'
|
||||||
|
},
|
||||||
|
'&.cm-focused .cm-selectionBackground': {
|
||||||
|
backgroundColor: '#264f78'
|
||||||
|
},
|
||||||
|
'.cm-selectionMatch': {
|
||||||
|
backgroundColor: '#264f7855'
|
||||||
|
},
|
||||||
|
'.cm-cursor': {
|
||||||
|
borderLeftColor: '#58a6ff'
|
||||||
|
},
|
||||||
|
'.cm-foldPlaceholder': {
|
||||||
|
backgroundColor: 'transparent',
|
||||||
|
border: 'none',
|
||||||
|
color: '#484f58'
|
||||||
|
},
|
||||||
|
'.cm-tooltip': {
|
||||||
|
border: '1px solid #30363d',
|
||||||
|
backgroundColor: '#161b22',
|
||||||
|
boxShadow: '0 4px 12px rgba(0, 0, 0, 0.5)'
|
||||||
|
},
|
||||||
|
'.cm-tooltip-autocomplete': {
|
||||||
|
'& > ul': {
|
||||||
|
maxHeight: '200px',
|
||||||
|
fontFamily: 'inherit',
|
||||||
|
'& > li': {
|
||||||
|
padding: '4px 8px',
|
||||||
|
'&[aria-selected]': {
|
||||||
|
backgroundColor: '#1f6feb',
|
||||||
|
color: '#ffffff'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, {
|
||||||
|
dark: true
|
||||||
|
});
|
||||||
|
|
||||||
|
// State compartments for dynamic reconfiguration
|
||||||
|
const languageCompartment = new Compartment();
|
||||||
|
const tabSizeCompartment = new Compartment();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get language extension based on file extension
|
||||||
|
*/
|
||||||
|
function getLanguageExtension(filePath) {
|
||||||
|
const ext = filePath.split('.').pop().toLowerCase();
|
||||||
|
|
||||||
|
const languageMap = {
|
||||||
|
'js': javascript(),
|
||||||
|
'jsx': javascript({ jsx: true }),
|
||||||
|
'ts': javascript({ typescript: true }),
|
||||||
|
'tsx': javascript({ typescript: true, jsx: true }),
|
||||||
|
'mjs': javascript(),
|
||||||
|
'cjs': javascript(),
|
||||||
|
'py': python(),
|
||||||
|
'html': html(),
|
||||||
|
'htm': html(),
|
||||||
|
'css': css(),
|
||||||
|
'scss': css(),
|
||||||
|
'sass': css(),
|
||||||
|
'json': json(),
|
||||||
|
'md': markdown(),
|
||||||
|
'markdown': markdown(),
|
||||||
|
'txt': null
|
||||||
|
};
|
||||||
|
|
||||||
|
return languageMap[ext] || javascript();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create CodeMirror editor state
|
||||||
|
*/
|
||||||
|
function createEditorState(filePath, content, onChange) {
|
||||||
|
const language = getLanguageExtension(filePath);
|
||||||
|
|
||||||
|
return EditorState.create({
|
||||||
|
doc: content || '',
|
||||||
|
extensions: [
|
||||||
|
lineNumbers(),
|
||||||
|
highlightSpecialChars(),
|
||||||
|
history(),
|
||||||
|
foldGutter(),
|
||||||
|
drawSelection(),
|
||||||
|
dropCursor(),
|
||||||
|
codeFolding(),
|
||||||
|
bracketMatching(),
|
||||||
|
closeBrackets(),
|
||||||
|
autocompletion(),
|
||||||
|
rectangularSelection(),
|
||||||
|
crosshairCursor(),
|
||||||
|
highlightActiveLine(),
|
||||||
|
highlightSelectionMatches(),
|
||||||
|
syntaxHighlighting(defaultHighlightStyle, { fallback: true }),
|
||||||
|
keymap.of([
|
||||||
|
...closeBracketsKeymap,
|
||||||
|
...defaultKeymap,
|
||||||
|
...searchKeymap,
|
||||||
|
...historyKeymap,
|
||||||
|
...completionKeymap,
|
||||||
|
{
|
||||||
|
key: 'Mod-s',
|
||||||
|
run: () => {
|
||||||
|
// Trigger save - will be handled by the component
|
||||||
|
const event = new CustomEvent('editor-save', { bubbles: true });
|
||||||
|
document.dispatchEvent(event);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'Mod-w',
|
||||||
|
run: (view) => {
|
||||||
|
// Trigger close tab - will be handled by the component
|
||||||
|
const event = new CustomEvent('editor-close-tab', { bubbles: true });
|
||||||
|
document.dispatchEvent(event);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]),
|
||||||
|
customDarkTheme,
|
||||||
|
languageCompartment.of(language || []),
|
||||||
|
tabSizeCompartment.of(EditorState.tabSize.of(4)),
|
||||||
|
cmEditorView.updateListener.of((update) => {
|
||||||
|
if (update.docChanged) {
|
||||||
|
// Notify that content changed
|
||||||
|
if (onChange) onChange();
|
||||||
|
}
|
||||||
|
})
|
||||||
|
]
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* FileEditor Class
|
||||||
|
*/
|
||||||
|
class FileEditor {
|
||||||
|
constructor(container) {
|
||||||
|
this.container = container;
|
||||||
|
this.editors = new Map(); // tabId -> EditorView
|
||||||
|
this.activeTab = null;
|
||||||
|
this.tabs = [];
|
||||||
|
this.nextTabId = 1;
|
||||||
|
|
||||||
|
this.initialize();
|
||||||
|
}
|
||||||
|
|
||||||
|
initialize() {
|
||||||
|
// Create editor container structure
|
||||||
|
this.container.innerHTML = `
|
||||||
|
<div class="file-editor-container">
|
||||||
|
<div class="editor-header">
|
||||||
|
<div class="editor-tabs" id="editor-tabs">
|
||||||
|
<!-- Tabs will be rendered here -->
|
||||||
|
</div>
|
||||||
|
<div class="editor-actions">
|
||||||
|
<button class="btn-icon" id="btn-save-all" title="Save All (Ctrl+S)">💾</button>
|
||||||
|
<button class="btn-icon" id="btn-close-all" title="Close All">✕</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="editor-content" id="editor-content">
|
||||||
|
<div class="editor-placeholder">
|
||||||
|
<div class="placeholder-icon">📄</div>
|
||||||
|
<h2>No file open</h2>
|
||||||
|
<p>Select a file from the sidebar to start editing</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
|
||||||
|
// Set up event listeners
|
||||||
|
this.setupEventListeners();
|
||||||
|
}
|
||||||
|
|
||||||
|
setupEventListeners() {
|
||||||
|
// Save all button
|
||||||
|
const saveAllBtn = this.container.querySelector('#btn-save-all');
|
||||||
|
if (saveAllBtn) {
|
||||||
|
saveAllBtn.addEventListener('click', () => this.saveAllFiles());
|
||||||
|
}
|
||||||
|
|
||||||
|
// Close all button
|
||||||
|
const closeAllBtn = this.container.querySelector('#btn-close-all');
|
||||||
|
if (closeAllBtn) {
|
||||||
|
closeAllBtn.addEventListener('click', () => this.closeAllTabs());
|
||||||
|
}
|
||||||
|
|
||||||
|
// Keyboard shortcuts
|
||||||
|
document.addEventListener('editor-save', () => this.saveCurrentFile());
|
||||||
|
document.addEventListener('editor-close-tab', () => this.closeCurrentTab());
|
||||||
|
|
||||||
|
// Handle window resize
|
||||||
|
window.addEventListener('resize', () => this.refreshActiveEditor());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Open a file in the editor
|
||||||
|
*/
|
||||||
|
async openFile(filePath, content) {
|
||||||
|
// Check if file is already open
|
||||||
|
const existingTab = this.tabs.find(tab => tab.path === filePath);
|
||||||
|
if (existingTab) {
|
||||||
|
this.activateTab(existingTab.id);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create new tab
|
||||||
|
const tabId = `tab-${this.nextTabId++}`;
|
||||||
|
const tab = {
|
||||||
|
id: tabId,
|
||||||
|
path: filePath,
|
||||||
|
name: filePath.split('/').pop(),
|
||||||
|
dirty: false,
|
||||||
|
originalContent: content || ''
|
||||||
|
};
|
||||||
|
|
||||||
|
this.tabs.push(tab);
|
||||||
|
|
||||||
|
// Create CodeMirror editor instance
|
||||||
|
const editorContainer = document.createElement('div');
|
||||||
|
editorContainer.className = 'cm-editor-instance';
|
||||||
|
editorContainer.style.display = 'none';
|
||||||
|
|
||||||
|
const contentArea = this.container.querySelector('#editor-content');
|
||||||
|
|
||||||
|
// Remove placeholder if it exists
|
||||||
|
const placeholder = contentArea.querySelector('.editor-placeholder');
|
||||||
|
if (placeholder) {
|
||||||
|
placeholder.remove();
|
||||||
|
}
|
||||||
|
|
||||||
|
contentArea.appendChild(editorContainer);
|
||||||
|
|
||||||
|
const state = createEditorState(filePath, content || '', () => {
|
||||||
|
this.markDirty(tabId);
|
||||||
|
});
|
||||||
|
|
||||||
|
const editor = new EditorView({
|
||||||
|
state: state,
|
||||||
|
parent: editorContainer
|
||||||
|
});
|
||||||
|
|
||||||
|
this.editors.set(tabId, editor);
|
||||||
|
|
||||||
|
// Activate the new tab
|
||||||
|
this.activateTab(tabId);
|
||||||
|
|
||||||
|
return tabId;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Activate a tab
|
||||||
|
*/
|
||||||
|
activateTab(tabId) {
|
||||||
|
if (!this.editors.has(tabId)) {
|
||||||
|
console.error('[FileEditor] Tab not found:', tabId);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Hide all editors
|
||||||
|
this.editors.forEach((editor, id) => {
|
||||||
|
const container = editor.dom.parentElement;
|
||||||
|
if (container) {
|
||||||
|
container.style.display = id === tabId ? 'block' : 'none';
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
this.activeTab = tabId;
|
||||||
|
this.renderTabs();
|
||||||
|
|
||||||
|
// Refresh the active editor to ensure proper rendering
|
||||||
|
setTimeout(() => this.refreshActiveEditor(), 10);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Close a tab
|
||||||
|
*/
|
||||||
|
async closeTab(tabId) {
|
||||||
|
const tab = this.tabs.find(t => t.id === tabId);
|
||||||
|
if (!tab) return;
|
||||||
|
|
||||||
|
// Check for unsaved changes
|
||||||
|
if (tab.dirty) {
|
||||||
|
const shouldSave = confirm(`Save changes to ${tab.name} before closing?`);
|
||||||
|
if (shouldSave) {
|
||||||
|
await this.saveFile(tabId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Destroy editor
|
||||||
|
const editor = this.editors.get(tabId);
|
||||||
|
if (editor) {
|
||||||
|
editor.destroy();
|
||||||
|
this.editors.delete(tabId);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove tab from list
|
||||||
|
this.tabs = this.tabs.filter(t => t.id !== tabId);
|
||||||
|
|
||||||
|
// If we closed the active tab, activate another one
|
||||||
|
if (this.activeTab === tabId) {
|
||||||
|
if (this.tabs.length > 0) {
|
||||||
|
this.activateTab(this.tabs[0].id);
|
||||||
|
} else {
|
||||||
|
this.activeTab = null;
|
||||||
|
this.showPlaceholder();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
this.renderTabs();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Close current tab
|
||||||
|
*/
|
||||||
|
closeCurrentTab() {
|
||||||
|
if (this.activeTab) {
|
||||||
|
this.closeTab(this.activeTab);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Close all tabs
|
||||||
|
*/
|
||||||
|
async closeAllTabs() {
|
||||||
|
if (this.tabs.length === 0) return;
|
||||||
|
|
||||||
|
const hasUnsaved = this.tabs.some(t => t.dirty);
|
||||||
|
if (hasUnsaved) {
|
||||||
|
const shouldSaveAll = confirm('Some files have unsaved changes. Save all before closing?');
|
||||||
|
if (shouldSaveAll) {
|
||||||
|
await this.saveAllFiles();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Destroy all editors
|
||||||
|
this.editors.forEach(editor => editor.destroy());
|
||||||
|
this.editors.clear();
|
||||||
|
|
||||||
|
this.tabs = [];
|
||||||
|
this.activeTab = null;
|
||||||
|
|
||||||
|
this.renderTabs();
|
||||||
|
this.showPlaceholder();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Save a file
|
||||||
|
*/
|
||||||
|
async saveFile(tabId) {
|
||||||
|
const tab = this.tabs.find(t => t.id === tabId);
|
||||||
|
if (!tab) return;
|
||||||
|
|
||||||
|
const editor = this.editors.get(tabId);
|
||||||
|
if (!editor) return;
|
||||||
|
|
||||||
|
const content = editor.state.doc.toString();
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch(`/claude/api/file/${encodeURIComponent(tab.path)}`, {
|
||||||
|
method: 'PUT',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({ content })
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const data = await response.json();
|
||||||
|
if (data.error) {
|
||||||
|
throw new Error(data.error);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update tab state
|
||||||
|
tab.dirty = false;
|
||||||
|
tab.originalContent = content;
|
||||||
|
|
||||||
|
this.renderTabs();
|
||||||
|
|
||||||
|
// Show success toast
|
||||||
|
if (typeof showToast === 'function') {
|
||||||
|
showToast(`✅ Saved ${tab.name}`, 'success', 2000);
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
} catch (error) {
|
||||||
|
console.error('[FileEditor] Error saving file:', error);
|
||||||
|
if (typeof showToast === 'function') {
|
||||||
|
showToast(`❌ Failed to save ${tab.name}: ${error.message}`, 'error', 3000);
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Save current file
|
||||||
|
*/
|
||||||
|
async saveCurrentFile() {
|
||||||
|
if (this.activeTab) {
|
||||||
|
await this.saveFile(this.activeTab);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Save all files
|
||||||
|
*/
|
||||||
|
async saveAllFiles() {
|
||||||
|
const dirtyTabs = this.tabs.filter(t => t.dirty);
|
||||||
|
|
||||||
|
if (dirtyTabs.length === 0) {
|
||||||
|
if (typeof showToast === 'function') {
|
||||||
|
showToast('No unsaved changes', 'info', 2000);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let saved = 0;
|
||||||
|
let failed = 0;
|
||||||
|
|
||||||
|
for (const tab of dirtyTabs) {
|
||||||
|
const result = await this.saveFile(tab.id);
|
||||||
|
if (result) {
|
||||||
|
saved++;
|
||||||
|
} else {
|
||||||
|
failed++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof showToast === 'function') {
|
||||||
|
if (failed === 0) {
|
||||||
|
showToast(`✅ Saved ${saved} file${saved > 1 ? 's' : ''}`, 'success', 2000);
|
||||||
|
} else {
|
||||||
|
showToast(`⚠️ Saved ${saved} file${saved > 1 ? 's' : ''}, ${failed} failed`, 'warning', 3000);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Mark tab as dirty (unsaved changes)
|
||||||
|
*/
|
||||||
|
markDirty(tabId) {
|
||||||
|
const tab = this.tabs.find(t => t.id === tabId);
|
||||||
|
if (tab && !tab.dirty) {
|
||||||
|
tab.dirty = true;
|
||||||
|
this.renderTabs();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if any tab is dirty
|
||||||
|
*/
|
||||||
|
hasUnsavedChanges() {
|
||||||
|
return this.tabs.some(t => t.dirty);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get current file content
|
||||||
|
*/
|
||||||
|
getCurrentContent() {
|
||||||
|
if (!this.activeTab) return null;
|
||||||
|
|
||||||
|
const editor = this.editors.get(this.activeTab);
|
||||||
|
return editor ? editor.state.doc.toString() : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get current file path
|
||||||
|
*/
|
||||||
|
getCurrentFilePath() {
|
||||||
|
if (!this.activeTab) return null;
|
||||||
|
|
||||||
|
const tab = this.tabs.find(t => t.id === this.activeTab);
|
||||||
|
return tab ? tab.path : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Refresh active editor
|
||||||
|
*/
|
||||||
|
refreshActiveEditor() {
|
||||||
|
if (!this.activeTab) return;
|
||||||
|
|
||||||
|
const editor = this.editors.get(this.activeTab);
|
||||||
|
if (editor) {
|
||||||
|
editor.requestMeasure();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Show placeholder when no files are open
|
||||||
|
*/
|
||||||
|
showPlaceholder() {
|
||||||
|
const contentArea = this.container.querySelector('#editor-content');
|
||||||
|
if (contentArea) {
|
||||||
|
contentArea.innerHTML = `
|
||||||
|
<div class="editor-placeholder">
|
||||||
|
<div class="placeholder-icon">📄</div>
|
||||||
|
<h2>No file open</h2>
|
||||||
|
<p>Select a file from the sidebar to start editing</p>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Render tabs
|
||||||
|
*/
|
||||||
|
renderTabs() {
|
||||||
|
const tabsContainer = this.container.querySelector('#editor-tabs');
|
||||||
|
if (!tabsContainer) return;
|
||||||
|
|
||||||
|
if (this.tabs.length === 0) {
|
||||||
|
tabsContainer.innerHTML = '';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
tabsContainer.innerHTML = this.tabs.map(tab => `
|
||||||
|
<div class="editor-tab ${tab.id === this.activeTab ? 'active' : ''} ${tab.dirty ? 'dirty' : ''}"
|
||||||
|
data-tab-id="${tab.id}">
|
||||||
|
<span class="tab-name">${this.escapeHtml(tab.name)}</span>
|
||||||
|
${tab.dirty ? '<span class="tab-dirty-indicator">●</span>' : ''}
|
||||||
|
<button class="tab-close" title="Close tab">×</button>
|
||||||
|
</div>
|
||||||
|
`).join('');
|
||||||
|
|
||||||
|
// Tab click handlers
|
||||||
|
tabsContainer.querySelectorAll('.editor-tab').forEach(tabEl => {
|
||||||
|
tabEl.addEventListener('click', (e) => {
|
||||||
|
if (!e.target.classList.contains('tab-close')) {
|
||||||
|
this.activateTab(tabEl.dataset.tabId);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const closeBtn = tabEl.querySelector('.tab-close');
|
||||||
|
if (closeBtn) {
|
||||||
|
closeBtn.addEventListener('click', (e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
this.closeTab(tabEl.dataset.tabId);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Escape HTML to prevent XSS
|
||||||
|
*/
|
||||||
|
escapeHtml(text) {
|
||||||
|
const div = document.createElement('div');
|
||||||
|
div.textContent = text;
|
||||||
|
return div.innerHTML;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get tab count
|
||||||
|
*/
|
||||||
|
getTabCount() {
|
||||||
|
return this.tabs.length;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get dirty tab count
|
||||||
|
*/
|
||||||
|
getDirtyTabCount() {
|
||||||
|
return this.tabs.filter(t => t.dirty).length;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Destroy editor and cleanup
|
||||||
|
*/
|
||||||
|
destroy() {
|
||||||
|
// Destroy all editors
|
||||||
|
this.editors.forEach(editor => editor.destroy());
|
||||||
|
this.editors.clear();
|
||||||
|
|
||||||
|
this.tabs = [];
|
||||||
|
this.activeTab = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Export for use in other scripts
|
||||||
|
if (typeof module !== 'undefined' && module.exports) {
|
||||||
|
module.exports = { FileEditor };
|
||||||
|
}
|
||||||
|
|
||||||
|
// Export to window for use from non-module scripts
|
||||||
|
if (typeof window !== 'undefined') {
|
||||||
|
window.FileEditor = FileEditor;
|
||||||
|
console.log('[FileEditor] Component loaded and available globally');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Auto-initialize when DOM is ready
|
||||||
|
if (typeof document !== 'undefined') {
|
||||||
|
if (document.readyState === 'loading') {
|
||||||
|
document.addEventListener('DOMContentLoaded', () => {
|
||||||
|
// Create global file editor instance
|
||||||
|
if (!window.fileEditor) {
|
||||||
|
window.fileEditor = new FileEditor(document.getElementById('file-editor'));
|
||||||
|
console.log('[FileEditor] Auto-initialized on DOMContentLoaded');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
// DOM is already ready
|
||||||
|
if (!window.fileEditor) {
|
||||||
|
window.fileEditor = new FileEditor(document.getElementById('file-editor'));
|
||||||
|
console.log('[FileEditor] Auto-initialized (DOM already ready)');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -855,10 +855,17 @@ async function loadFile(filePath) {
|
|||||||
const res = await fetch(`/claude/api/file/${encodeURIComponent(filePath)}`);
|
const res = await fetch(`/claude/api/file/${encodeURIComponent(filePath)}`);
|
||||||
const data = await res.json();
|
const data = await res.json();
|
||||||
|
|
||||||
const isHtmlFile = filePath.toLowerCase().endsWith('.html') || filePath.toLowerCase().endsWith('.htm');
|
// Check if FileEditor component is available
|
||||||
|
if (window.fileEditor) {
|
||||||
|
// Use the new CodeMirror-based editor
|
||||||
|
await window.fileEditor.openFile(filePath, data.content || '');
|
||||||
|
} else {
|
||||||
|
// Fallback to the old view if FileEditor is not loaded yet
|
||||||
|
console.warn('[loadFile] FileEditor not available, using fallback');
|
||||||
const editorEl = document.getElementById('file-editor');
|
const editorEl = document.getElementById('file-editor');
|
||||||
|
|
||||||
|
const isHtmlFile = filePath.toLowerCase().endsWith('.html') || filePath.toLowerCase().endsWith('.htm');
|
||||||
|
|
||||||
if (isHtmlFile) {
|
if (isHtmlFile) {
|
||||||
// HTML file - show with preview option
|
// HTML file - show with preview option
|
||||||
editorEl.innerHTML = `
|
editorEl.innerHTML = `
|
||||||
@@ -894,21 +901,28 @@ async function loadFile(filePath) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// Non-HTML file - show as before
|
// Non-HTML file - show content
|
||||||
editorEl.innerHTML = `
|
editorEl.innerHTML = `
|
||||||
<div class="file-header">
|
<div class="file-header">
|
||||||
<h2>${filePath}</h2>
|
<h2>${filePath}</h2>
|
||||||
<div class="file-actions">
|
|
||||||
<button class="btn-secondary btn-sm" onclick="editFile('${filePath}')">Edit</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="file-content">
|
<div class="file-content">
|
||||||
<div class="markdown-body">${data.html}</div>
|
<pre><code>${escapeHtml(data.content || '')}</code></pre>
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error loading file:', error);
|
console.error('Error loading file:', error);
|
||||||
|
const editorEl = document.getElementById('file-editor');
|
||||||
|
if (editorEl) {
|
||||||
|
editorEl.innerHTML = `
|
||||||
|
<div class="file-error">
|
||||||
|
<h2>Error loading file</h2>
|
||||||
|
<p>${error.message}</p>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -10,7 +10,41 @@
|
|||||||
<link rel="stylesheet" href="/claude/claude-ide/preview-manager.css">
|
<link rel="stylesheet" href="/claude/claude-ide/preview-manager.css">
|
||||||
<link rel="stylesheet" href="/claude/claude-ide/chat-enhanced.css">
|
<link rel="stylesheet" href="/claude/claude-ide/chat-enhanced.css">
|
||||||
<link rel="stylesheet" href="/claude/claude-ide/terminal.css">
|
<link rel="stylesheet" href="/claude/claude-ide/terminal.css">
|
||||||
|
<link rel="stylesheet" href="/claude/claude-ide/components/file-editor.css">
|
||||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/styles/github-dark.min.css">
|
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/styles/github-dark.min.css">
|
||||||
|
|
||||||
|
<!-- Import Map for CodeMirror 6 -->
|
||||||
|
<script type="importmap">
|
||||||
|
{
|
||||||
|
"imports": {
|
||||||
|
"@codemirror/state": "/claude/node_modules/@codemirror/state/dist/index.cjs",
|
||||||
|
"@codemirror/view": "/claude/node_modules/@codemirror/view/dist/index.cjs",
|
||||||
|
"@codemirror/commands": "/claude/node_modules/@codemirror/commands/dist/index.cjs",
|
||||||
|
"@codemirror/language": "/claude/node_modules/@codemirror/language/dist/index.cjs",
|
||||||
|
"@codemirror/autocomplete": "/claude/node_modules/@codemirror/autocomplete/dist/index.cjs",
|
||||||
|
"@codemirror/search": "/claude/node_modules/@codemirror/search/dist/index.cjs",
|
||||||
|
"@codemirror/lint": "/claude/node_modules/@codemirror/lint/dist/index.cjs",
|
||||||
|
"@codemirror/lang-javascript": "/claude/node_modules/@codemirror/lang-javascript/dist/index.cjs",
|
||||||
|
"@codemirror/lang-python": "/claude/node_modules/@codemirror/lang-python/dist/index.cjs",
|
||||||
|
"@codemirror/lang-html": "/claude/node_modules/@codemirror/lang-html/dist/index.cjs",
|
||||||
|
"@codemirror/lang-css": "/claude/node_modules/@codemirror/lang-css/dist/index.cjs",
|
||||||
|
"@codemirror/lang-json": "/claude/node_modules/@codemirror/lang-json/dist/index.cjs",
|
||||||
|
"@codemirror/lang-markdown": "/claude/node_modules/@codemirror/lang-markdown/dist/index.cjs",
|
||||||
|
"@codemirror/gutter": "/claude/node_modules/@codemirror/gutter/dist/index.cjs",
|
||||||
|
"@codemirror/fold": "/claude/node_modules/@codemirror/fold/dist/index.cjs",
|
||||||
|
"@codemirror/panel": "/claude/node_modules/@codemirror/panel/dist/index.cjs",
|
||||||
|
"@lezer/highlight": "/claude/node_modules/@lezer/highlight/dist/index.cjs",
|
||||||
|
"@lezer/common": "/claude/node_modules/@lezer/common/dist/index.cjs",
|
||||||
|
"@lezer/javascript": "/claude/node_modules/@lezer/javascript/dist/index.cjs",
|
||||||
|
"@lezer/python": "/claude/node_modules/@lezer/python/dist/index.cjs",
|
||||||
|
"@lezer/html": "/claude/node_modules/@lezer/html/dist/index.cjs",
|
||||||
|
"@lezer/css": "/claude/node_modules/@lezer/css/dist/index.cjs",
|
||||||
|
"@lezer/json": "/claude/node_modules/@lezer/json/dist/index.cjs",
|
||||||
|
"@lezer/markdown": "/claude/node_modules/@lezer/markdown/dist/index.cjs",
|
||||||
|
"@codemirror/basic-setup": "/claude/node_modules/@codemirror/basic-setup/dist/index.cjs"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div id="app">
|
<div id="app">
|
||||||
@@ -344,6 +378,7 @@
|
|||||||
<script src="/claude/claude-ide/preview-manager.js"></script>
|
<script src="/claude/claude-ide/preview-manager.js"></script>
|
||||||
<script src="/claude/claude-ide/chat-enhanced.js"></script>
|
<script src="/claude/claude-ide/chat-enhanced.js"></script>
|
||||||
<script src="/claude/claude-ide/terminal.js"></script>
|
<script src="/claude/claude-ide/terminal.js"></script>
|
||||||
|
<script type="module" src="/claude/claude-ide/components/file-editor.js"></script>
|
||||||
|
|
||||||
<!-- Debug Panel Toggle Script -->
|
<!-- Debug Panel Toggle Script -->
|
||||||
<script>
|
<script>
|
||||||
|
|||||||
@@ -189,6 +189,9 @@ app.get('/projects', (req, res) => {
|
|||||||
// Serve static files (must come after specific routes)
|
// Serve static files (must come after specific routes)
|
||||||
app.use('/claude', express.static(path.join(__dirname, 'public')));
|
app.use('/claude', express.static(path.join(__dirname, 'public')));
|
||||||
|
|
||||||
|
// Serve node_modules for CodeMirror 6 import map
|
||||||
|
app.use('/claude/node_modules', express.static(path.join(__dirname, 'node_modules')));
|
||||||
|
|
||||||
// Login route
|
// Login route
|
||||||
app.post('/claude/api/login', (req, res) => {
|
app.post('/claude/api/login', (req, res) => {
|
||||||
const { username, password } = req.body;
|
const { username, password } = req.body;
|
||||||
|
|||||||
Reference in New Issue
Block a user