diff --git a/public/claude-ide/components/file-editor.css b/public/claude-ide/components/file-editor.css new file mode 100644 index 00000000..c846e75b --- /dev/null +++ b/public/claude-ide/components/file-editor.css @@ -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; + } +} diff --git a/public/claude-ide/components/file-editor.js b/public/claude-ide/components/file-editor.js new file mode 100644 index 00000000..ab25e06e --- /dev/null +++ b/public/claude-ide/components/file-editor.js @@ -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 = ` +
Select a file from the sidebar to start editing
+Select a file from the sidebar to start editing
+${escapeHtml(data.content)}
- ${escapeHtml(data.content)}
+ ${escapeHtml(data.content || '')}
+ ${error.message}
+