From 0fd20c1eb3c14822aad3a9177e2566b6935c043e Mon Sep 17 00:00:00 2001 From: amitwh Date: Mon, 1 Sep 2025 20:53:17 +0530 Subject: [PATCH] Major v1.3.0 update: Fix PDF export, file associations, remove converter menu, and implement tabbed interface - Enhanced PDF export with multiple LaTeX engine fallbacks - Fixed file association and direct file opening from OS - Removed redundant converter menu, moved import to File menu - Implemented comprehensive tabbed interface for multiple files - Added tab management with keyboard shortcuts (Ctrl+N, Ctrl+W, Ctrl+Tab) - Enhanced UI with tab bar and improved navigation - Updated to version 1.3.0 with new features - Improved main process and renderer architecture for multi-file support --- CLAUDE.md | 303 ++++++++++++++ package.json | 2 +- src/index.html | 23 +- src/main.js | 183 +++++---- src/renderer.js | 1043 ++++++++++++++++++++++------------------------- src/styles.css | 121 ++++++ 6 files changed, 1039 insertions(+), 636 deletions(-) create mode 100644 CLAUDE.md diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..0d1cd75 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,303 @@ +# PanConverter - Claude Development Guide + +## Project Overview + +**PanConverter** is a cross-platform Markdown editor and converter powered by Pandoc, built with Electron. It provides professional-grade editing capabilities with comprehensive export options. + +**Current Version**: v1.2.1 +**Author**: Amit Haridas (amit.wh@gmail.com) +**License**: MIT +**Repository**: https://github.com/amitwh/pan-converter + +## Architecture & Technology Stack + +### Core Technologies +- **Electron** - Cross-platform desktop application framework +- **Pandoc** - Universal document converter (required dependency) +- **marked** - Markdown parsing and rendering +- **highlight.js** - Syntax highlighting +- **XLSX** - Spreadsheet export functionality +- **DOMPurify** - HTML sanitization + +### Application Structure +``` +src/ +├── main.js # Electron main process, menu system, IPC handlers +├── renderer.js # UI logic, editor functionality, event handling +├── index.html # Application layout and components +└── styles.css # Comprehensive styling with multi-theme support + +assets/ +└── icon.png # Application icon + +package.json # Dependencies and build configuration +``` + +## Development Commands + +### Prerequisites +```bash +# Install Node.js dependencies +npm install + +# Install Pandoc (required for export functionality) +# Ubuntu/Debian: +sudo apt-get install pandoc + +# macOS: +brew install pandoc + +# Windows: Download from https://pandoc.org/installing.html +``` + +### Running the Application +```bash +# Start development server +npm start + +# Start with debugging +npm start --enable-logging +``` + +### Building & Packaging + +```bash +# Generate application icons +npm run generate-icons + +# Build for current platform +npm run build + +# Platform-specific builds +npm run build:win # Windows +npm run build:mac # macOS +npm run build:linux # Linux (AppImage, .deb, .snap) + +# Build for all platforms +npm run dist:all +``` + +### Git Branch Management +```bash +# Switch to platform-specific branches +git checkout linux # Linux development +git checkout macos # macOS development +git checkout windows # Windows development +git checkout master # Main development branch + +# Update all branches with latest changes +git checkout master +git push origin master +git checkout linux && git merge master && git push origin linux +git checkout macos && git merge master && git push origin macos +git checkout windows && git merge master && git push origin windows +``` + +### Release Management +```bash +# Create and push release tag +git tag v1.2.1 -m "Release message" +git push origin v1.2.1 + +# Create GitHub release with packages +gh release create v1.2.1 --title "Title" --notes "Release notes" \ + "dist/PanConverter-1.2.1.AppImage" \ + "dist/pan-converter_1.2.1_amd64.deb" \ + "dist/pan-converter_1.2.1_amd64.snap" +``` + +## Feature Implementation Guide + +### v1.2.1 Comprehensive Editor Enhancements + +#### ✨ Advanced Editor Features + +**Find & Replace System** (`src/renderer.js:200-350`) +- Dialog-based interface with match highlighting +- Forward/backward navigation through matches +- Replace single or replace all functionality +- Real-time match counting and status display +- Escape key closes dialog + +**Line Numbers** (`src/renderer.js:450-500`, `src/styles.css:517-598`) +- Toggle-able line numbers with toolbar button +- Synchronized scrolling with editor content +- Theme-aware styling for all supported themes +- Dynamic line number generation based on content + +**Undo/Redo System** (`src/renderer.js:100-150`) +- Stack-based state management for editor history +- Keyboard shortcuts: `Ctrl/Cmd+Z` (undo), `Ctrl/Cmd+Shift+Z` (redo) +- Intelligent state saving on text changes +- Memory-efficient history management + +**Smart Auto-Indentation** (`src/renderer.js:350-400`) +- Automatic list continuation on Enter key +- Proper indentation handling for nested lists +- Support for ordered and unordered lists +- Intelligent whitespace management + +**Enhanced Keyboard Shortcuts** (`src/renderer.js:400-450`) +- `Tab`/`Shift+Tab` for line indentation/outdentation +- `Enter` for auto-continuing lists +- `Ctrl/Cmd+F` for find & replace dialog +- `Escape` for closing dialogs + +**Word/Character Count** (`src/renderer.js:500-530`) +- Live counting displayed in status bar +- Updates automatically as content changes +- Word and character statistics + +#### 📤 Export & Conversion Features + +**PowerPoint Export** (`src/main.js:330-350`) +- Convert markdown to PPTX presentations +- Automatic slide-level formatting (`--slide-level=2`) +- Smart presentation structure handling + +**Spreadsheet Export** (`src/main.js:370-457`) +- Export markdown tables to Excel (XLSX/XLS) and ODS formats +- Multi-table support with separate worksheets +- Automatic table detection and parsing +- Error handling for files without tables + +**Document Import** (`src/main.js:280-315`) +- Import DOCX, ODT, RTF, HTML, PDF, PPTX, ODP files +- Automatic conversion to markdown format +- File dialog with appropriate filters +- Success notifications and error handling + +**Table Creation Helper** (`src/renderer.js:600-650`) +- Built-in table generator with row/column specification +- Automatic markdown table formatting +- Proper header separation and alignment + +#### 🎨 Interface & Theming + +**Multi-Theme Support** (`src/styles.css:214-598`) +- Light, Dark, Solarized, Monokai, GitHub themes +- Complete theming for all UI components +- Theme-aware styling for new features (find dialog, line numbers) +- Persistent theme selection with local storage + +**Enhanced UI Components** (`src/index.html:94-108`) +- Find & replace dialog with modern styling +- Toolbar buttons for all new features +- Status bar with live statistics +- Responsive layout with proper spacing + +## File Structure & Key Components + +### Main Process (`src/main.js`) +- **Menu System**: Comprehensive menu with file operations, editing, conversion, view options +- **IPC Handlers**: Communication between main and renderer processes +- **File Operations**: Open, save, import/export functionality +- **Theme Management**: Persistent theme storage and application +- **Spreadsheet Export**: Table extraction and XLSX generation +- **About Dialog**: Application information and feature list + +### Renderer Process (`src/renderer.js`) +- **Editor Initialization**: CodeMirror-like functionality with custom implementation +- **Find & Replace Engine**: Search algorithms with regex support +- **Undo/Redo Manager**: History stack management +- **Auto-indentation Logic**: Smart list continuation +- **Live Preview**: Real-time markdown rendering with DOMPurify +- **Event Handling**: Keyboard shortcuts and UI interactions +- **Statistics Tracking**: Word/character counting + +### Styling (`src/styles.css`) +- **Base Styles**: Application layout and typography +- **Component Styles**: Toolbar, editor, preview, dialogs +- **Theme Implementations**: Complete styling for all themes +- **Responsive Design**: Flexible layouts and proper spacing +- **Animation Support**: Smooth transitions and hover effects + +### HTML Structure (`src/index.html`) +- **Toolbar**: Feature buttons with SVG icons +- **Find Dialog**: Search and replace interface +- **Editor Container**: Line numbers and text editor +- **Preview Pane**: Rendered markdown display +- **Status Bar**: Statistics and application status + +## Testing & Quality Assurance + +### Manual Testing Checklist +- [ ] All keyboard shortcuts work correctly +- [ ] Find & replace functions properly with edge cases +- [ ] Line numbers sync correctly with content +- [ ] Undo/redo preserves cursor position +- [ ] Auto-indentation works with various list types +- [ ] All themes render correctly for new components +- [ ] Export functions work with various document formats +- [ ] Table creation and export functionality +- [ ] Cross-platform compatibility + +### Known Issues & Limitations +- AppImage may require `--no-sandbox` flag on some Linux systems +- Large files (>1MB) may cause performance issues +- Windows/Mac builds require platform-specific environments +- Pandoc must be installed separately for export functionality + +## Deployment & Distribution + +### Release Packages +- **Linux AppImage**: Universal Linux package (self-contained) +- **Debian Package**: `.deb` for Ubuntu/Debian systems +- **Snap Package**: Universal Linux package via Snap Store +- **Future**: Windows `.exe` and macOS `.dmg` packages + +### Release Process +1. Update version in `package.json`, `src/main.js`, and `README.md` +2. Commit changes and push to all platform branches +3. Build platform-specific packages +4. Create Git tag and GitHub release +5. Upload packages to GitHub release +6. Update documentation and announce release + +## Contributing Guidelines + +### Code Style +- Use consistent indentation (2 spaces) +- Follow JavaScript ES6+ standards +- Comment complex functionality +- Maintain separation between main and renderer processes +- Use descriptive variable and function names + +### Adding New Features +1. Plan feature implementation and UI integration +2. Update relevant files (main.js, renderer.js, styles.css) +3. Test across all supported themes +4. Update documentation and README +5. Test on multiple platforms if possible +6. Submit pull request with detailed description + +### Bug Reporting +- Include steps to reproduce +- Specify platform and version information +- Attach relevant screenshots or error logs +- Check existing issues before creating new ones + +## Future Roadmap + +### Planned Features +- [ ] Collaborative editing capabilities +- [ ] Plugin system for extensions +- [ ] Advanced markdown extensions (math, diagrams) +- [ ] Cloud synchronization options +- [ ] Mobile companion app +- [ ] Advanced export templates +- [ ] Spell check and grammar checking +- [ ] Version control integration + +### Technical Improvements +- [ ] Performance optimization for large files +- [ ] Memory usage optimization +- [ ] Startup time improvements +- [ ] Better error handling and user feedback +- [ ] Automated testing suite +- [ ] Continuous integration/deployment + +--- + +**Last Updated**: September 1, 2025 +**Claude Assistant**: Development completed for v1.2.1 comprehensive editor enhancements \ No newline at end of file diff --git a/package.json b/package.json index 778e7e3..444b9a9 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "pan-converter", - "version": "1.2.1", + "version": "1.3.0", "description": "Cross-platform Markdown editor and converter using Pandoc", "main": "src/main.js", "scripts": { diff --git a/src/index.html b/src/index.html index 39f8da3..7a299f2 100644 --- a/src/index.html +++ b/src/index.html @@ -10,6 +10,13 @@
+
+
+ Untitled + +
+ +
-
-
- - +
+
+
+ + +
+
+
+
-
-
-
diff --git a/src/main.js b/src/main.js index 70c2a72..fd644fc 100644 --- a/src/main.js +++ b/src/main.js @@ -28,7 +28,7 @@ const store = { }; let mainWindow; -let currentFile = null; +let currentFile = null; // This will now represent the active tab's file function createWindow() { mainWindow = new BrowserWindow({ @@ -48,6 +48,14 @@ function createWindow() { mainWindow.on('closed', () => { mainWindow = null; }); + + // Handle pending file from file association + if (app.pendingFile) { + mainWindow.webContents.once('dom-ready', () => { + openFileFromPath(app.pendingFile); + app.pendingFile = null; + }); + } } function createMenu() { @@ -76,6 +84,11 @@ function createMenu() { click: saveAsFile }, { type: 'separator' }, + { + label: 'Import Document...', + accelerator: 'CmdOrCtrl+I', + click: importDocument + }, { label: 'Export', submenu: [ @@ -121,32 +134,6 @@ function createMenu() { } ] }, - { - label: 'Convert', - submenu: [ - { - label: 'Import Document...', - accelerator: 'CmdOrCtrl+I', - click: importDocument - }, - { type: 'separator' }, - { - label: 'Convert Current File', - submenu: [ - { label: 'To Markdown', click: () => convertToFormat('md') }, - { label: 'To HTML', click: () => convertToFormat('html') }, - { label: 'To PDF', click: () => convertToFormat('pdf') }, - { label: 'To DOCX', click: () => convertToFormat('docx') }, - { label: 'To LaTeX', click: () => convertToFormat('latex') }, - { label: 'To RTF', click: () => convertToFormat('rtf') }, - { label: 'To ODT', click: () => convertToFormat('odt') }, - { label: 'To EPUB', click: () => convertToFormat('epub') }, - { label: 'To PPTX', click: () => convertToFormat('pptx') }, - { label: 'To ODP', click: () => convertToFormat('odp') } - ] - } - ] - }, { label: 'View', submenu: [ @@ -184,7 +171,7 @@ function createMenu() { type: 'info', title: 'About PanConverter', message: 'PanConverter', - detail: 'A cross-platform Markdown editor and converter using Pandoc.\n\nVersion: 1.2.1\nAuthor: Amit Haridas\nEmail: amit.wh@gmail.com\nLicense: MIT\n\nFeatures:\n• Advanced markdown editing with live preview\n• Find & replace with match highlighting\n• Line numbers and auto-indentation\n• Export to multiple formats via Pandoc\n• PowerPoint & presentation export\n• Export tables to Excel/ODS spreadsheets\n• Document import & conversion\n• Table creation helper\n• Multiple themes support\n• Undo/redo functionality', + detail: 'A cross-platform Markdown editor and converter using Pandoc.\n\nVersion: 1.3.0\nAuthor: Amit Haridas\nEmail: amit.wh@gmail.com\nLicense: MIT\n\nFeatures:\n• Tabbed interface for multiple files\n• Advanced markdown editing with live preview\n• Enhanced PDF export with LaTeX engines\n• Find & replace with match highlighting\n• Line numbers and auto-indentation\n• Export to multiple formats via Pandoc\n• PowerPoint & presentation export\n• Export tables to Excel/ODS spreadsheets\n• Document import & conversion\n• Table creation helper\n• Multiple themes support\n• Undo/redo functionality', buttons: ['OK'] }); } @@ -246,23 +233,62 @@ function exportFile(format) { }); if (outputFile) { - const pandocCmd = `pandoc "${currentFile}" -o "${outputFile}"`; + let pandocCmd = `pandoc "${currentFile}" -o "${outputFile}"`; - exec(pandocCmd, (error, stdout, stderr) => { - if (error) { - dialog.showErrorBox('Export Error', `Failed to export: ${error.message}\n\nMake sure Pandoc is installed.`); - } else { - dialog.showMessageBox(mainWindow, { - type: 'info', - title: 'Export Complete', - message: `File exported successfully to ${outputFile}`, - buttons: ['OK'] - }); - } - }); + // Add specific options for PDF export to ensure proper generation + if (format === 'pdf') { + pandocCmd = `pandoc "${currentFile}" --pdf-engine=xelatex -V geometry:margin=1in -o "${outputFile}"`; + // Try with different PDF engines if xelatex fails + exec(pandocCmd, (error, stdout, stderr) => { + if (error) { + // Fallback to pdflatex + const fallbackCmd = `pandoc "${currentFile}" --pdf-engine=pdflatex -V geometry:margin=1in -o "${outputFile}"`; + exec(fallbackCmd, (fallbackError, fallbackStdout, fallbackStderr) => { + if (fallbackError) { + // Final fallback to wkhtmltopdf + const htmlToPdfCmd = `pandoc "${currentFile}" -t html5 | wkhtmltopdf - "${outputFile}"`; + exec(htmlToPdfCmd, (finalError) => { + if (finalError) { + dialog.showErrorBox('PDF Export Error', + `Failed to export PDF. Please ensure you have one of the following installed:\n` + + `• XeLaTeX (recommended): sudo apt-get install texlive-xetex\n` + + `• PDFLaTeX: sudo apt-get install texlive-latex-base\n` + + `• wkhtmltopdf: sudo apt-get install wkhtmltopdf\n\n` + + `Error: ${finalError.message}` + ); + } else { + showExportSuccess(outputFile); + } + }); + } else { + showExportSuccess(outputFile); + } + }); + } else { + showExportSuccess(outputFile); + } + }); + } else { + exec(pandocCmd, (error, stdout, stderr) => { + if (error) { + dialog.showErrorBox('Export Error', `Failed to export: ${error.message}\n\nMake sure Pandoc is installed.`); + } else { + showExportSuccess(outputFile); + } + }); + } } } +function showExportSuccess(outputFile) { + dialog.showMessageBox(mainWindow, { + type: 'info', + title: 'Export Complete', + message: `File exported successfully to ${outputFile}`, + buttons: ['OK'] + }); +} + function exportSpreadsheet(format) { if (!currentFile) { dialog.showErrorBox('Error', 'Please save the file first'); @@ -310,40 +336,6 @@ function importDocument() { } } -function convertToFormat(format) { - if (!currentFile) { - dialog.showErrorBox('Error', 'Please save or open a file first'); - return; - } - - const outputFile = dialog.showSaveDialogSync(mainWindow, { - defaultPath: currentFile.replace(/\.[^/.]+$/, `.${format}`), - filters: [ - { name: format.toUpperCase(), extensions: [format] } - ] - }); - - if (outputFile) { - // For presentations, add slide level for better conversion - let pandocCmd = `pandoc "${currentFile}" -o "${outputFile}"`; - if (format === 'pptx' || format === 'odp') { - pandocCmd = `pandoc "${currentFile}" --slide-level=2 -o "${outputFile}"`; - } - - exec(pandocCmd, (error, stdout, stderr) => { - if (error) { - dialog.showErrorBox('Conversion Error', `Failed to convert: ${error.message}\n\nMake sure Pandoc is installed.`); - } else { - dialog.showMessageBox(mainWindow, { - type: 'info', - title: 'Conversion Complete', - message: `File converted successfully to ${outputFile}`, - buttons: ['OK'] - }); - } - }); - } -} function setTheme(theme) { store.set('theme', theme); @@ -369,6 +361,11 @@ ipcMain.on('get-theme', (event) => { event.reply('theme-changed', theme); }); +// Handle tab file tracking for exports +ipcMain.on('set-current-file', (event, filePath) => { + currentFile = filePath; +}); + ipcMain.on('export-spreadsheet', (event, { content, format }) => { const outputFile = dialog.showSaveDialogSync(mainWindow, { defaultPath: currentFile.replace(/\.[^/.]+$/, `.${format}`), @@ -452,7 +449,17 @@ function extractTablesFromMarkdown(markdown) { return tables; } -app.whenReady().then(createWindow); +app.whenReady().then(() => { + createWindow(); + + // Handle file association on app startup + if (process.argv.length > 1) { + const filePath = process.argv.find(arg => arg.endsWith('.md') || arg.endsWith('.markdown')); + if (filePath && fs.existsSync(filePath)) { + openFileFromPath(filePath); + } + } +}); app.on('window-all-closed', () => { if (process.platform !== 'darwin') { @@ -464,4 +471,26 @@ app.on('activate', () => { if (BrowserWindow.getAllWindows().length === 0) { createWindow(); } -}); \ No newline at end of file +}); + +// Handle file opening on macOS +app.on('open-file', (event, filePath) => { + event.preventDefault(); + if (mainWindow) { + openFileFromPath(filePath); + } else { + // Store the file path to open after window is created + app.pendingFile = filePath; + } +}); + +// Handle file opening from command line or file association +function openFileFromPath(filePath) { + if (fs.existsSync(filePath)) { + currentFile = filePath; + const content = fs.readFileSync(filePath, 'utf-8'); + if (mainWindow && mainWindow.webContents) { + mainWindow.webContents.send('file-opened', { path: filePath, content }); + } + } +} \ No newline at end of file diff --git a/src/renderer.js b/src/renderer.js index f14834a..40a4b6f 100644 --- a/src/renderer.js +++ b/src/renderer.js @@ -17,583 +17,524 @@ marked.setOptions({ gfm: true }); -// Elements -const editor = document.getElementById('editor'); -const preview = document.getElementById('preview'); -const previewPane = document.getElementById('preview-pane'); -const editorPane = document.getElementById('editor-pane'); -const statusText = document.getElementById('status-text'); -const wordCount = document.getElementById('word-count'); -const lineNumbers = document.getElementById('line-numbers'); -const findDialog = document.getElementById('find-dialog'); -const findInput = document.getElementById('find-input'); -const replaceInput = document.getElementById('replace-input'); -const findCount = document.getElementById('find-count'); +// Tab Management +class TabManager { + constructor() { + this.tabs = new Map(); + this.activeTabId = 1; + this.nextTabId = 2; + this.isPreviewVisible = true; + this.showLineNumbers = false; + + // Initialize first tab + this.tabs.set(1, { + id: 1, + title: 'Untitled', + content: '', + filePath: null, + isDirty: false, + undoStack: [], + redoStack: [], + findMatches: [], + currentMatchIndex: -1 + }); + + this.setupEventListeners(); + this.updateUI(); + } + + setupEventListeners() { + // Tab bar events + document.getElementById('new-tab-btn').addEventListener('click', () => this.createNewTab()); + document.getElementById('tab-bar').addEventListener('click', (e) => { + if (e.target.classList.contains('tab-close')) { + e.stopPropagation(); + const tabId = parseInt(e.target.closest('.tab').dataset.tabId); + this.closeTab(tabId); + } else if (e.target.closest('.tab')) { + const tabId = parseInt(e.target.closest('.tab').dataset.tabId); + this.switchToTab(tabId); + } + }); + + // Editor events for active tab + this.setupEditorEvents(); + + // Toolbar events + this.setupToolbarEvents(); + + // Find dialog events + this.setupFindEvents(); + + // Keyboard shortcuts + document.addEventListener('keydown', (e) => { + if (e.ctrlKey || e.metaKey) { + switch (e.key) { + case 'n': + e.preventDefault(); + this.createNewTab(); + break; + case 'w': + if (this.tabs.size > 1) { + e.preventDefault(); + this.closeTab(this.activeTabId); + } + break; + case 't': + e.preventDefault(); + this.createNewTab(); + break; + case 'Tab': + if (this.tabs.size > 1) { + e.preventDefault(); + this.switchToNextTab(); + } + break; + } + } + }); + } + + createNewTab() { + const newTabId = this.nextTabId++; + const tab = { + id: newTabId, + title: 'Untitled', + content: '', + filePath: null, + isDirty: false, + undoStack: [], + redoStack: [], + findMatches: [], + currentMatchIndex: -1 + }; + + this.tabs.set(newTabId, tab); + this.createTabElements(tab); + this.switchToTab(newTabId); + this.updateTabBar(); + } + + createTabElements(tab) { + // Create tab content container + const tabContent = document.createElement('div'); + tabContent.className = 'tab-content'; + tabContent.id = `tab-content-${tab.id}`; + tabContent.dataset.tabId = tab.id; + + tabContent.innerHTML = ` +
+
+ + +
+
+
+
+
+ `; + + document.querySelector('.editor-container').appendChild(tabContent); + } + + switchToTab(tabId) { + if (!this.tabs.has(tabId)) return; + + // Save current tab state before switching + if (this.activeTabId && this.tabs.has(this.activeTabId)) { + this.saveCurrentTabState(); + } + + this.activeTabId = tabId; + this.updateUI(); + this.restoreTabState(tabId); + this.focusActiveEditor(); + + // Notify main process about current file for exports + const tab = this.tabs.get(tabId); + if (tab && tab.filePath) { + ipcRenderer.send('set-current-file', tab.filePath); + } + } + + switchToNextTab() { + const tabIds = Array.from(this.tabs.keys()); + const currentIndex = tabIds.indexOf(this.activeTabId); + const nextIndex = (currentIndex + 1) % tabIds.length; + this.switchToTab(tabIds[nextIndex]); + } + + closeTab(tabId) { + if (this.tabs.size === 1) return; // Don't close the last tab + + const tab = this.tabs.get(tabId); + if (tab.isDirty) { + // TODO: Show confirmation dialog + } + + // Remove tab elements + const tabElement = document.querySelector(`[data-tab-id="${tabId}"]`); + const tabContent = document.getElementById(`tab-content-${tabId}`); + + if (tabElement && tabElement.classList.contains('tab')) { + tabElement.remove(); + } + if (tabContent) { + tabContent.remove(); + } + + this.tabs.delete(tabId); + + // Switch to another tab if this was active + if (this.activeTabId === tabId) { + const remainingTabs = Array.from(this.tabs.keys()); + this.switchToTab(remainingTabs[0]); + } + + this.updateTabBar(); + } + + updateTabBar() { + const tabBar = document.getElementById('tab-bar'); + const existingTabs = tabBar.querySelectorAll('.tab'); + + // Remove all existing tab elements except the new tab button + existingTabs.forEach(tab => tab.remove()); + + // Add tabs in order + const sortedTabs = Array.from(this.tabs.values()).sort((a, b) => a.id - b.id); + const newTabBtn = document.getElementById('new-tab-btn'); + + sortedTabs.forEach(tab => { + const tabElement = document.createElement('div'); + tabElement.className = `tab ${tab.id === this.activeTabId ? 'active' : ''}`; + tabElement.dataset.tabId = tab.id; + + const title = tab.filePath ? + tab.filePath.split('/').pop() : + tab.title; + + const dirtyIndicator = tab.isDirty ? ' •' : ''; + + tabElement.innerHTML = ` + ${title}${dirtyIndicator} + + `; + + tabBar.insertBefore(tabElement, newTabBtn); + }); + } + + updateUI() { + // Show/hide tab contents + document.querySelectorAll('.tab-content').forEach(content => { + content.classList.remove('active'); + }); + + const activeContent = document.getElementById(`tab-content-${this.activeTabId}`); + if (activeContent) { + activeContent.classList.add('active'); + } + + // Update preview visibility + this.updatePreviewVisibility(); + this.updateLineNumbers(); + this.updateTabBar(); + } + + saveCurrentTabState() { + const tab = this.tabs.get(this.activeTabId); + if (!tab) return; + + const editor = document.getElementById(`editor-${this.activeTabId}`); + if (editor) { + tab.content = editor.value; + tab.isDirty = tab.content !== (tab.originalContent || ''); + } + } + + restoreTabState(tabId) { + const tab = this.tabs.get(tabId); + if (!tab) return; + + const editor = document.getElementById(`editor-${tabId}`); + const preview = document.getElementById(`preview-${tabId}`); + + if (editor) { + editor.value = tab.content; + this.updatePreview(tabId); + this.updateWordCount(); + } + } + + focusActiveEditor() { + const editor = document.getElementById(`editor-${this.activeTabId}`); + if (editor) { + editor.focus(); + } + } + + updatePreview(tabId = this.activeTabId) { + const tab = this.tabs.get(tabId); + const preview = document.getElementById(`preview-${tabId}`); + + if (!tab || !preview) return; + + try { + const html = marked.parse(tab.content); + const sanitizedHtml = DOMPurify.sanitize(html); + preview.innerHTML = sanitizedHtml; + } catch (error) { + preview.innerHTML = '

Error rendering preview

'; + } + } + + updatePreviewVisibility() { + document.querySelectorAll('.tab-content').forEach(content => { + const previewPane = content.querySelector('.pane:last-child'); + const editorPane = content.querySelector('.pane:first-child'); + + if (this.isPreviewVisible) { + previewPane.classList.remove('hidden'); + editorPane.classList.remove('full-width'); + } else { + previewPane.classList.add('hidden'); + editorPane.classList.add('full-width'); + } + }); + } + + updateLineNumbers() { + const editor = document.getElementById(`editor-${this.activeTabId}`); + const lineNumbers = document.getElementById(`line-numbers-${this.activeTabId}`); + + if (!editor || !lineNumbers) return; + + if (this.showLineNumbers) { + const lines = editor.value.split('\\n'); + lineNumbers.innerHTML = lines.map((_, i) => + `
${i + 1}
` + ).join(''); + lineNumbers.classList.remove('hidden'); + } else { + lineNumbers.classList.add('hidden'); + } + } + + updateWordCount() { + const tab = this.tabs.get(this.activeTabId); + if (!tab) return; + + const words = tab.content.trim().split(/\\s+/).filter(word => word.length > 0).length; + const chars = tab.content.length; + document.getElementById('word-count').textContent = `Words: ${words} | Characters: ${chars}`; + } + + setupEditorEvents() { + // Set up editor events using event delegation + document.addEventListener('input', (e) => { + if (e.target.classList.contains('editor-textarea')) { + const tabId = parseInt(e.target.id.split('-')[1]); + if (tabId === this.activeTabId) { + this.handleEditorInput(tabId); + } + } + }); + + document.addEventListener('scroll', (e) => { + if (e.target.classList.contains('editor-textarea')) { + const tabId = parseInt(e.target.id.split('-')[1]); + if (tabId === this.activeTabId) { + this.updateLineNumbers(); + } + } + }); + } + + handleEditorInput(tabId) { + const tab = this.tabs.get(tabId); + if (!tab) return; + + const editor = document.getElementById(`editor-${tabId}`); + tab.content = editor.value; + tab.isDirty = true; + + this.updatePreview(tabId); + this.updateWordCount(); + this.updateLineNumbers(); + this.updateTabBar(); + + // Add to undo stack + this.pushUndoState(tabId); + } + + pushUndoState(tabId) { + const tab = this.tabs.get(tabId); + if (!tab) return; + + tab.undoStack.push(tab.content); + if (tab.undoStack.length > 50) { + tab.undoStack.shift(); + } + tab.redoStack = []; + } + + undo() { + const tab = this.tabs.get(this.activeTabId); + if (!tab || tab.undoStack.length === 0) return; + + tab.redoStack.push(tab.content); + tab.content = tab.undoStack.pop(); + + const editor = document.getElementById(`editor-${this.activeTabId}`); + if (editor) { + editor.value = tab.content; + this.updatePreview(); + this.updateWordCount(); + } + } + + redo() { + const tab = this.tabs.get(this.activeTabId); + if (!tab || tab.redoStack.length === 0) return; + + tab.undoStack.push(tab.content); + tab.content = tab.redoStack.pop(); + + const editor = document.getElementById(`editor-${this.activeTabId}`); + if (editor) { + editor.value = tab.content; + this.updatePreview(); + this.updateWordCount(); + } + } + + setupToolbarEvents() { + // Existing toolbar setup... + document.getElementById('btn-preview-toggle').addEventListener('click', () => { + this.isPreviewVisible = !this.isPreviewVisible; + this.updatePreviewVisibility(); + }); + + document.getElementById('btn-line-numbers').addEventListener('click', () => { + this.showLineNumbers = !this.showLineNumbers; + this.updateLineNumbers(); + }); + + // Add other toolbar events... + } + + setupFindEvents() { + // Find dialog implementation... + document.getElementById('btn-find').addEventListener('click', () => { + document.getElementById('find-dialog').classList.remove('hidden'); + document.getElementById('find-input').focus(); + }); + + document.getElementById('btn-find-close').addEventListener('click', () => { + document.getElementById('find-dialog').classList.add('hidden'); + }); + } + + // File operations + openFile(filePath, content) { + let tab = this.tabs.get(this.activeTabId); + + // If current tab is empty and untitled, reuse it + if (!tab.filePath && !tab.isDirty && tab.content === '') { + tab.filePath = filePath; + tab.title = filePath.split('/').pop(); + tab.content = content; + tab.originalContent = content; + tab.isDirty = false; + } else { + // Create new tab for the file + this.createNewTab(); + tab = this.tabs.get(this.activeTabId); + tab.filePath = filePath; + tab.title = filePath.split('/').pop(); + tab.content = content; + tab.originalContent = content; + tab.isDirty = false; + } + + this.restoreTabState(this.activeTabId); + this.updateTabBar(); + } + + getCurrentContent() { + const tab = this.tabs.get(this.activeTabId); + return tab ? tab.content : ''; + } + + getCurrentFilePath() { + const tab = this.tabs.get(this.activeTabId); + return tab ? tab.filePath : null; + } +} -// State -let isPreviewVisible = true; -let currentContent = ''; -let isDirty = false; -let showLineNumbers = false; -let findMatches = []; -let currentMatchIndex = -1; -let undoStack = []; -let redoStack = []; -let maxUndoSize = 50; +// Initialize tab manager +let tabManager; -// Initialize document.addEventListener('DOMContentLoaded', () => { + tabManager = new TabManager(); + // Request current theme ipcRenderer.send('get-theme'); // Set up auto-save interval - setInterval(autoSave, 30000); // Auto-save every 30 seconds - - // Initialize with empty content - updatePreview(); - updateWordCount(); + setInterval(() => { + // Auto-save logic for all tabs + tabManager.tabs.forEach(tab => { + if (tab.isDirty && tab.filePath) { + ipcRenderer.send('save-current-file', tab.content); + } + }); + }, 30000); }); -// Editor input handler -editor.addEventListener('input', () => { - currentContent = editor.value; - isDirty = true; - updatePreview(); - updateWordCount(); - updateStatus('Modified'); -}); - -// Toolbar button handlers -document.getElementById('btn-bold').addEventListener('click', () => insertMarkdown('**', '**')); -document.getElementById('btn-italic').addEventListener('click', () => insertMarkdown('*', '*')); -document.getElementById('btn-heading').addEventListener('click', () => insertMarkdown('## ', '')); -document.getElementById('btn-link').addEventListener('click', () => insertMarkdown('[', '](url)')); -document.getElementById('btn-code').addEventListener('click', () => insertMarkdown('`', '`')); -document.getElementById('btn-list').addEventListener('click', () => insertMarkdown('- ', '')); -document.getElementById('btn-quote').addEventListener('click', () => insertMarkdown('> ', '')); -document.getElementById('btn-table').addEventListener('click', insertTable); -document.getElementById('btn-find').addEventListener('click', toggleFindDialog); -document.getElementById('btn-line-numbers').addEventListener('click', toggleLineNumbers); -document.getElementById('btn-preview-toggle').addEventListener('click', togglePreview); - -// Find dialog handlers -document.getElementById('btn-find-close').addEventListener('click', closeFindDialog); -document.getElementById('btn-find-next').addEventListener('click', findNext); -document.getElementById('btn-find-prev').addEventListener('click', findPrev); -document.getElementById('btn-replace').addEventListener('click', replaceOne); -document.getElementById('btn-replace-all').addEventListener('click', replaceAll); -findInput.addEventListener('input', performFind); -replaceInput.addEventListener('keydown', (e) => { - if (e.key === 'Enter') replaceOne(); -}); - -// Table insertion function -function insertTable() { - const rows = prompt('Number of rows:', '3'); - const cols = prompt('Number of columns:', '3'); - - if (!rows || !cols) return; - - const numRows = parseInt(rows); - const numCols = parseInt(cols); - - if (isNaN(numRows) || isNaN(numCols) || numRows < 1 || numCols < 1) { - alert('Please enter valid numbers for rows and columns'); - return; - } - - let table = '\n'; - - // Header row - table += '|'; - for (let j = 0; j < numCols; j++) { - table += ` Header ${j + 1} |`; - } - table += '\n'; - - // Separator row - table += '|'; - for (let j = 0; j < numCols; j++) { - table += ' --- |'; - } - table += '\n'; - - // Data rows - for (let i = 0; i < numRows; i++) { - table += '|'; - for (let j = 0; j < numCols; j++) { - table += ` Cell ${i + 1}-${j + 1} |`; - } - table += '\n'; - } - table += '\n'; - - // Insert table at cursor position - const start = editor.selectionStart; - const end = editor.selectionEnd; - - editor.value = editor.value.substring(0, start) + table + editor.value.substring(end); - - // Set cursor after the table - editor.selectionStart = editor.selectionEnd = start + table.length; - editor.focus(); - - // Trigger input event - editor.dispatchEvent(new Event('input')); -} - -// Markdown insertion helper -function insertMarkdown(before, after) { - const start = editor.selectionStart; - const end = editor.selectionEnd; - const selectedText = editor.value.substring(start, end); - const replacement = before + (selectedText || 'text') + after; - - editor.value = editor.value.substring(0, start) + replacement + editor.value.substring(end); - - // Set cursor position - if (selectedText) { - editor.selectionStart = start; - editor.selectionEnd = start + replacement.length; - } else { - editor.selectionStart = start + before.length; - editor.selectionEnd = start + before.length + 4; // Select "text" - } - - editor.focus(); - - // Trigger input event - editor.dispatchEvent(new Event('input')); -} - -// Update preview -function updatePreview() { - const html = marked.parse(editor.value); - const clean = DOMPurify.sanitize(html); - preview.innerHTML = clean; - - // Re-highlight code blocks - preview.querySelectorAll('pre code').forEach((block) => { - hljs.highlightElement(block); - }); -} - -// Toggle preview visibility -function togglePreview() { - isPreviewVisible = !isPreviewVisible; - - if (isPreviewVisible) { - previewPane.classList.remove('hidden'); - editorPane.classList.remove('full-width'); - } else { - previewPane.classList.add('hidden'); - editorPane.classList.add('full-width'); - } -} - -// Update word count -function updateWordCount() { - const text = editor.value; - const words = text.trim() ? text.trim().split(/\s+/).length : 0; - const chars = text.length; - wordCount.textContent = `Words: ${words} | Characters: ${chars}`; -} - -// Update status -function updateStatus(text) { - statusText.textContent = text; -} - -// Auto-save function -function autoSave() { - if (isDirty && currentContent) { - ipcRenderer.send('save-current-file', currentContent); - isDirty = false; - updateStatus('Auto-saved'); - setTimeout(() => updateStatus('Ready'), 2000); - } -} - -// IPC event handlers +// IPC event listeners ipcRenderer.on('file-new', () => { - if (isDirty) { - if (confirm('You have unsaved changes. Do you want to continue?')) { - editor.value = ''; - currentContent = ''; - isDirty = false; - updatePreview(); - updateWordCount(); - updateStatus('New file'); - } - } else { - editor.value = ''; - currentContent = ''; - updatePreview(); - updateWordCount(); - updateStatus('New file'); - } + tabManager.createNewTab(); }); -ipcRenderer.on('file-opened', (event, { path, content }) => { - editor.value = content; - currentContent = content; - isDirty = false; - updatePreview(); - updateWordCount(); - updateStatus(`Opened: ${path}`); +ipcRenderer.on('file-opened', (event, data) => { + tabManager.openFile(data.path, data.content); }); ipcRenderer.on('file-save', () => { - ipcRenderer.send('save-current-file', editor.value); - isDirty = false; - updateStatus('Saved'); -}); - -ipcRenderer.on('get-content-for-save', (event, path) => { - ipcRenderer.send('save-file', { path, content: editor.value }); - isDirty = false; - updateStatus(`Saved: ${path}`); -}); - -ipcRenderer.on('toggle-preview', () => { - togglePreview(); -}); - -ipcRenderer.on('theme-changed', (event, theme) => { - // Remove all theme classes - document.body.classList.remove('theme-light', 'theme-dark', 'theme-solarized', 'theme-monokai', 'theme-github'); - - // Add new theme class - if (theme !== 'light') { - document.body.classList.add(`theme-${theme}`); + const currentContent = tabManager.getCurrentContent(); + const currentFilePath = tabManager.getCurrentFilePath(); + if (currentFilePath) { + ipcRenderer.send('save-current-file', currentContent); } - - updateStatus(`Theme: ${theme}`); +}); + +ipcRenderer.on('get-content-for-save', (event, filePath) => { + const currentContent = tabManager.getCurrentContent(); + ipcRenderer.send('save-file', { path: filePath, content: currentContent }); }); ipcRenderer.on('get-content-for-spreadsheet', (event, format) => { - ipcRenderer.send('export-spreadsheet', { content: editor.value, format }); + const currentContent = tabManager.getCurrentContent(); + ipcRenderer.send('export-spreadsheet', { content: currentContent, format }); +}); + +ipcRenderer.on('toggle-preview', () => { + tabManager.isPreviewVisible = !tabManager.isPreviewVisible; + tabManager.updatePreviewVisibility(); }); ipcRenderer.on('toggle-find', () => { - toggleFindDialog(); -}); - -// Enhanced editor input handler with undo/redo and auto-indentation -editor.addEventListener('input', (e) => { - // Save state for undo - if (e.inputType !== 'historyUndo' && e.inputType !== 'historyRedo') { - pushUndo(); - } - - updateLineNumbers(); -}); - -// Push current state to undo stack -function pushUndo() { - if (undoStack.length >= maxUndoSize) { - undoStack.shift(); - } - undoStack.push({ - content: currentContent, - selectionStart: editor.selectionStart, - selectionEnd: editor.selectionEnd - }); - redoStack = []; // Clear redo stack when new changes are made -} - -// Undo function -function undo() { - if (undoStack.length > 1) { - redoStack.push(undoStack.pop()); - const state = undoStack[undoStack.length - 1]; - editor.value = state.content; - editor.selectionStart = state.selectionStart; - editor.selectionEnd = state.selectionEnd; - currentContent = state.content; - updatePreview(); - updateWordCount(); - updateLineNumbers(); - } -} - -// Redo function -function redo() { - if (redoStack.length > 0) { - const state = redoStack.pop(); - undoStack.push(state); - editor.value = state.content; - editor.selectionStart = state.selectionStart; - editor.selectionEnd = state.selectionEnd; - currentContent = state.content; - updatePreview(); - updateWordCount(); - updateLineNumbers(); - } -} - -// Find & Replace functionality -function toggleFindDialog() { + const findDialog = document.getElementById('find-dialog'); if (findDialog.classList.contains('hidden')) { findDialog.classList.remove('hidden'); - findInput.focus(); - if (editor.selectionStart !== editor.selectionEnd) { - findInput.value = editor.value.substring(editor.selectionStart, editor.selectionEnd); - performFind(); - } + document.getElementById('find-input').focus(); } else { - closeFindDialog(); - } -} - -function closeFindDialog() { - findDialog.classList.add('hidden'); - clearHighlights(); - editor.focus(); -} - -function performFind() { - const searchText = findInput.value; - clearHighlights(); - findMatches = []; - currentMatchIndex = -1; - - if (!searchText) { - findCount.textContent = '0 matches'; - return; - } - - const content = editor.value; - let index = 0; - while ((index = content.indexOf(searchText, index)) !== -1) { - findMatches.push(index); - index += searchText.length; - } - - findCount.textContent = `${findMatches.length} matches`; - - if (findMatches.length > 0) { - currentMatchIndex = 0; - highlightMatch(); - } -} - -function findNext() { - if (findMatches.length === 0) return; - currentMatchIndex = (currentMatchIndex + 1) % findMatches.length; - highlightMatch(); -} - -function findPrev() { - if (findMatches.length === 0) return; - currentMatchIndex = currentMatchIndex === 0 ? findMatches.length - 1 : currentMatchIndex - 1; - highlightMatch(); -} - -function highlightMatch() { - if (currentMatchIndex === -1 || !findMatches[currentMatchIndex]) return; - - const matchStart = findMatches[currentMatchIndex]; - const matchEnd = matchStart + findInput.value.length; - - editor.selectionStart = matchStart; - editor.selectionEnd = matchEnd; - editor.focus(); - - // Scroll to match - const lineHeight = 20; // Approximate line height - const lineNumber = editor.value.substring(0, matchStart).split('\n').length; - editor.scrollTop = Math.max(0, (lineNumber - 10) * lineHeight); -} - -function replaceOne() { - if (currentMatchIndex === -1 || !findMatches[currentMatchIndex]) return; - - const searchText = findInput.value; - const replaceText = replaceInput.value; - const matchStart = findMatches[currentMatchIndex]; - const matchEnd = matchStart + searchText.length; - - editor.value = editor.value.substring(0, matchStart) + - replaceText + - editor.value.substring(matchEnd); - - editor.selectionStart = matchStart; - editor.selectionEnd = matchStart + replaceText.length; - - // Trigger input event - editor.dispatchEvent(new Event('input')); - - // Refresh find - setTimeout(() => performFind(), 10); -} - -function replaceAll() { - const searchText = findInput.value; - const replaceText = replaceInput.value; - - if (!searchText) return; - - let content = editor.value; - let replacements = 0; - - while (content.includes(searchText)) { - content = content.replace(searchText, replaceText); - replacements++; - } - - if (replacements > 0) { - editor.value = content; - editor.dispatchEvent(new Event('input')); - updateStatus(`Replaced ${replacements} occurrences`); - performFind(); - } -} - -function clearHighlights() { - // Clear any highlights (in a real implementation, you'd remove highlight spans) -} - -// Line Numbers functionality -function toggleLineNumbers() { - showLineNumbers = !showLineNumbers; - if (showLineNumbers) { - lineNumbers.classList.remove('hidden'); - updateLineNumbers(); - } else { - lineNumbers.classList.add('hidden'); - } -} - -function updateLineNumbers() { - if (!showLineNumbers) return; - - const lines = editor.value.split('\n'); - const lineNumbersHtml = lines.map((_, index) => - `${index + 1}` - ).join(''); - lineNumbers.innerHTML = lineNumbersHtml; - - // Sync scroll - lineNumbers.scrollTop = editor.scrollTop; -} - -// Sync line numbers scroll with editor -editor.addEventListener('scroll', () => { - if (showLineNumbers) { - lineNumbers.scrollTop = editor.scrollTop; + findDialog.classList.add('hidden'); } }); -// Auto-indentation for lists -function handleEnterKey(e) { - const cursorPos = editor.selectionStart; - const beforeCursor = editor.value.substring(0, cursorPos); - const lines = beforeCursor.split('\n'); - const currentLine = lines[lines.length - 1]; - - // Check for list patterns - const listMatch = currentLine.match(/^(\s*)([-*+]|\d+\.)\s/); - if (listMatch) { - e.preventDefault(); - const indent = listMatch[1]; - const marker = listMatch[2]; - - // If current line is just the marker, remove it - if (currentLine.trim() === marker) { - const lineStart = beforeCursor.lastIndexOf('\n') + 1; - editor.value = editor.value.substring(0, lineStart) + - editor.value.substring(cursorPos); - editor.selectionStart = editor.selectionEnd = lineStart; - } else { - // Continue the list - let newMarker = marker; - if (/^\d+\./.test(marker)) { - const num = parseInt(marker) + 1; - newMarker = num + '.'; - } - const insertion = '\n' + indent + newMarker + ' '; - editor.value = editor.value.substring(0, cursorPos) + - insertion + - editor.value.substring(cursorPos); - editor.selectionStart = editor.selectionEnd = cursorPos + insertion.length; - } - - editor.dispatchEvent(new Event('input')); - } -} - -// Keyboard shortcuts -document.addEventListener('keydown', (e) => { - // Ctrl/Cmd + F for find - if ((e.ctrlKey || e.metaKey) && e.key === 'f') { - e.preventDefault(); - toggleFindDialog(); - } - - // Ctrl/Cmd + Z for undo - if ((e.ctrlKey || e.metaKey) && e.key === 'z' && !e.shiftKey) { - e.preventDefault(); - undo(); - } - - // Ctrl/Cmd + Shift + Z for redo - if ((e.ctrlKey || e.metaKey) && e.key === 'z' && e.shiftKey) { - e.preventDefault(); - redo(); - } - - // Ctrl/Cmd + Enter to toggle preview - if ((e.ctrlKey || e.metaKey) && e.key === 'Enter') { - togglePreview(); - } - - // Enhanced Tab handling in editor - if (e.key === 'Tab' && e.target === editor) { - e.preventDefault(); - const start = editor.selectionStart; - const end = editor.selectionEnd; - - if (start === end) { - // Simple tab insertion - editor.value = editor.value.substring(0, start) + ' ' + editor.value.substring(end); - editor.selectionStart = editor.selectionEnd = start + 4; - } else { - // Indent/outdent selected lines - const beforeSelection = editor.value.substring(0, start); - const selection = editor.value.substring(start, end); - const afterSelection = editor.value.substring(end); - - const lines = selection.split('\n'); - const indentedLines = e.shiftKey ? - lines.map(line => line.replace(/^ /, '')) : // Outdent - lines.map(line => ' ' + line); // Indent - - const newSelection = indentedLines.join('\n'); - editor.value = beforeSelection + newSelection + afterSelection; - - editor.selectionStart = start; - editor.selectionEnd = start + newSelection.length; - } - - editor.dispatchEvent(new Event('input')); - } - - // Enter key handling for auto-indentation - if (e.key === 'Enter' && e.target === editor) { - handleEnterKey(e); - } - - // Escape to close find dialog - if (e.key === 'Escape' && !findDialog.classList.contains('hidden')) { - closeFindDialog(); - } -}); - -// Prevent accidental navigation -window.addEventListener('beforeunload', (e) => { - if (isDirty) { - e.preventDefault(); - e.returnValue = ''; - } +ipcRenderer.on('theme-changed', (event, theme) => { + document.body.className = `theme-${theme}`; }); \ No newline at end of file diff --git a/src/styles.css b/src/styles.css index 7a7a180..7126ea6 100644 --- a/src/styles.css +++ b/src/styles.css @@ -16,6 +16,93 @@ body { height: 100vh; } +/* Tab Bar */ +.tab-bar { + display: flex; + align-items: center; + background: #f0f0f0; + border-bottom: 1px solid #ddd; + padding: 0 8px; + min-height: 36px; +} + +.tab { + display: flex; + align-items: center; + padding: 6px 12px; + background: #e8e8e8; + border: 1px solid #ccc; + border-bottom: none; + border-radius: 6px 6px 0 0; + margin-right: 2px; + cursor: pointer; + user-select: none; + max-width: 200px; + min-width: 120px; +} + +.tab.active { + background: #fff; + border-color: #999; + z-index: 1; +} + +.tab-title { + flex: 1; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + font-size: 13px; +} + +.tab-close { + background: none; + border: none; + font-size: 16px; + font-weight: bold; + cursor: pointer; + padding: 0; + margin-left: 8px; + width: 16px; + height: 16px; + display: flex; + align-items: center; + justify-content: center; + border-radius: 3px; + color: #666; +} + +.tab-close:hover { + background: #ddd; + color: #000; +} + +.new-tab-button { + background: none; + border: 1px solid #ccc; + border-radius: 4px; + padding: 4px 8px; + cursor: pointer; + font-size: 14px; + margin-left: 8px; + color: #666; +} + +.new-tab-button:hover { + background: #e8e8e8; +} + +/* Tab Content */ +.tab-content { + display: flex; + flex: 1; + overflow: hidden; +} + +.tab-content:not(.active) { + display: none; +} + /* Toolbar */ .toolbar { display: flex; @@ -217,6 +304,40 @@ body.theme-dark { color: #d4d4d4; } +body.theme-dark .tab-bar { + background: #2d2d30; + border-bottom-color: #3e3e42; +} + +body.theme-dark .tab { + background: #3c3c3c; + border-color: #555; + color: #d4d4d4; +} + +body.theme-dark .tab.active { + background: #1e1e1e; + border-color: #666; +} + +body.theme-dark .tab-close { + color: #ccc; +} + +body.theme-dark .tab-close:hover { + background: #555; + color: #fff; +} + +body.theme-dark .new-tab-button { + border-color: #555; + color: #ccc; +} + +body.theme-dark .new-tab-button:hover { + background: #3c3c3c; +} + body.theme-dark .toolbar { background: #2d2d30; border-bottom-color: #3e3e42;