Release v1.5.0: Enhanced Features and Open Source Compatibility

Major improvements including optional advanced export options, removed proprietary dependencies for better open-source compatibility, and comprehensive new features for enhanced user experience.

🔧 Export Functions & Advanced Options:
• Fixed export function issues after advanced options integration
• Added optional advanced export checkbox (unchecked by default)
• Clean UI separation between simple and advanced export workflows

🏗️ Open Source Compatibility:
• Removed bundled Pandoc binaries - requires system installation
• Replaced proprietary XLSX with open-source CSV export
• Eliminated licensing concerns with bundled dependencies

 Advanced User Experience Features:
• Auto-save system with visual indicators every 30 seconds
• Enhanced document statistics (lines, paragraphs, sentences, reading time)
• Recent files menu with persistent storage (last 10 files)
• Mathematical expression support with KaTeX rendering
• Improved export dialog with better user experience

📚 Updated documentation with new dependency requirements and features

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
2025-09-15 16:36:23 +05:30
co-authored by Claude
parent 5376bddc59
commit f50ffe8ed2
11 changed files with 680 additions and 79 deletions
+24 -2
View File
@@ -122,8 +122,20 @@
<button id="export-dialog-close" title="Close">×</button>
</div>
<div class="export-dialog-body">
<div class="export-section">
<label for="export-template">Template:</label>
<!-- Simple/Advanced Export Toggle -->
<div class="export-section export-mode-toggle">
<label class="checkbox-label">
<input type="checkbox" id="advanced-export-toggle">
<span class="checkmark"></span>
Show Advanced Options
</label>
<small class="export-help">Enable for templates, metadata, bibliography, and advanced PDF settings</small>
</div>
<!-- Advanced Export Options (hidden by default) -->
<div id="advanced-export-options" class="advanced-options hidden">
<div class="export-section">
<label for="export-template">Template:</label>
<select id="export-template">
<option value="default">Default</option>
<option value="custom">Custom (Browse...)</option>
@@ -203,6 +215,16 @@
<button id="browse-csl">Browse</button>
</div>
</div>
</div> <!-- End advanced-export-options -->
<!-- Basic Export Options (always visible) -->
<div class="export-section basic-options">
<label>Quick Export Settings:</label>
<div class="checkbox-group">
<label><input type="checkbox" id="basic-toc"> Include Table of Contents</label>
<label><input type="checkbox" id="basic-number-sections"> Number Sections</label>
</div>
</div>
</div>
<div class="export-dialog-footer">
<button id="export-cancel">Cancel</button>
+116 -19
View File
@@ -2,7 +2,21 @@ const { app, BrowserWindow, Menu, dialog, ipcMain, shell } = require('electron')
const path = require('path');
const fs = require('fs');
const { exec } = require('child_process');
const XLSX = require('xlsx');
// Get the system Pandoc path
function getPandocPath() {
// Always use system pandoc - no bundled binaries
return 'pandoc';
}
// Check if Pandoc is available
function checkPandocAvailable() {
return new Promise((resolve) => {
exec('pandoc --version', (error) => {
resolve(!error);
});
});
}
// Simple storage implementation to replace electron-store
const settingsPath = path.join(app.getPath('userData'), 'settings.json');
@@ -52,6 +66,53 @@ function createWindow() {
// Handle pending file from file association will be handled by renderer-ready event
}
function buildRecentFilesMenu() {
const recentFiles = getRecentFiles();
if (recentFiles.length === 0) {
return [
{
label: 'No recent files',
enabled: false
}
];
}
const recentFileItems = recentFiles.map(filePath => ({
label: filePath.split(/[\\/]/).pop(), // Get filename only
click: () => {
if (fs.existsSync(filePath)) {
currentFile = filePath;
const content = fs.readFileSync(filePath, 'utf-8');
mainWindow.webContents.send('file-opened', { path: filePath, content });
} else {
dialog.showErrorBox('File Not Found', `The file "${filePath}" could not be found.`);
}
},
toolTip: filePath // Show full path in tooltip
}));
return [
...recentFileItems,
{ type: 'separator' },
{
label: 'Clear Recent Files',
click: () => {
mainWindow.webContents.send('clear-recent-files');
}
}
];
}
function getRecentFiles() {
try {
const recentFiles = JSON.parse(fs.readFileSync(path.join(app.getPath('userData'), 'recent-files.json'), 'utf-8'));
return recentFiles.filter(file => fs.existsSync(file));
} catch (e) {
return [];
}
}
function createMenu() {
const template = [
{
@@ -78,6 +139,11 @@ function createMenu() {
click: saveAsFile
},
{ type: 'separator' },
{
label: 'Recent Files',
submenu: buildRecentFilesMenu()
},
{ type: 'separator' },
{
label: 'Import Document...',
accelerator: 'CmdOrCtrl+I',
@@ -97,9 +163,7 @@ function createMenu() {
{ label: 'PowerPoint (PPTX)', click: () => exportFile('pptx') },
{ label: 'OpenDocument Presentation (ODP)', click: () => exportFile('odp') },
{ type: 'separator' },
{ label: 'Excel (XLSX)', click: () => exportSpreadsheet('xlsx') },
{ label: 'Excel Legacy (XLS)', click: () => exportSpreadsheet('xls') },
{ label: 'OpenDocument Spreadsheet (ODS)', click: () => exportSpreadsheet('ods') }
{ label: 'CSV (Tables)', click: () => exportSpreadsheet('csv') }
]
},
{ type: 'separator' },
@@ -195,7 +259,7 @@ function createMenu() {
type: 'info',
title: 'About PanConverter',
message: 'PanConverter',
detail: 'A cross-platform Markdown editor and converter using Pandoc.\n\nVersion: 1.4.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• File association support for .md files (Fixed in v1.4.0)\n• Advanced export options with templates and metadata\n• Batch file conversion with progress tracking\n• Improved preview typography and spacing\n• Adjustable font sizes via menu (Ctrl+Shift+Plus/Minus)\n• Complete theme support including Monokai fixes\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.5.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• Optional advanced export options\n• Advanced export options with templates and metadata\n• Batch file conversion with progress tracking\n• Improved preview typography and spacing\n• Adjustable font sizes via menu (Ctrl+Shift+Plus/Minus)\n• Complete theme support including Monokai fixes\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 CSV format\n• Document import & conversion\n• Table creation helper\n• Multiple themes support\n• Undo/redo functionality\n• Live word count and statistics',
buttons: ['OK']
});
}
@@ -270,7 +334,7 @@ function performExportWithOptions(format, options) {
});
if (outputFile) {
let pandocCmd = `pandoc "${currentFile}" -o "${outputFile}"`;
let pandocCmd = `${getPandocPath()} "${currentFile}" -o "${outputFile}"`;
// Add template if specified
if (options.template && options.template !== 'default') {
@@ -345,7 +409,7 @@ function tryPdfFallback(inputFile, outputFile, engines, index, options, lastErro
const engine = engines[index];
const geometry = options.geometry || 'margin=1in';
let pandocCmd = `pandoc "${inputFile}" --pdf-engine=${engine} -V geometry:${geometry} -o "${outputFile}"`;
let pandocCmd = `${getPandocPath()} "${inputFile}" --pdf-engine=${engine} -V geometry:${geometry} -o "${outputFile}"`;
// Add all other options
if (options.template && options.template !== 'default') {
@@ -403,7 +467,7 @@ function importDocument() {
const outputFile = inputFile.replace(/\.[^/.]+$/, '.md');
// Convert to markdown using pandoc
const pandocCmd = `pandoc "${inputFile}" -t markdown -o "${outputFile}"`;
const pandocCmd = `${getPandocPath()} "${inputFile}" -t markdown -o "${outputFile}"`;
exec(pandocCmd, (error, stdout, stderr) => {
if (error) {
@@ -496,27 +560,36 @@ ipcMain.on('export-spreadsheet', (event, { content, format }) => {
try {
// Parse markdown content to extract tables
const tables = extractTablesFromMarkdown(content);
if (tables.length === 0) {
dialog.showErrorBox('Export Error', 'No tables found in the markdown content');
return;
}
// Create workbook
const wb = XLSX.utils.book_new();
// Convert tables to CSV format
let csvContent = '';
tables.forEach((table, index) => {
const ws = XLSX.utils.aoa_to_sheet(table);
XLSX.utils.book_append_sheet(wb, ws, `Table ${index + 1}`);
if (index > 0) csvContent += '\n\n'; // Separate multiple tables
if (tables.length > 1) csvContent += `"Table ${index + 1}"\n`;
table.forEach(row => {
const csvRow = row.map(cell => {
// Escape quotes and wrap in quotes if necessary
const cleanCell = cell.replace(/"/g, '""');
return cleanCell.includes(',') || cleanCell.includes('"') || cleanCell.includes('\n')
? `"${cleanCell}"` : cleanCell;
}).join(',');
csvContent += csvRow + '\n';
});
});
// Write file
XLSX.writeFile(wb, outputFile);
// Write CSV file
fs.writeFileSync(outputFile, csvContent, 'utf-8');
dialog.showMessageBox(mainWindow, {
type: 'info',
title: 'Export Complete',
message: `Spreadsheet exported successfully to ${outputFile}`,
message: `CSV exported successfully to ${outputFile}`,
buttons: ['OK']
});
} catch (error) {
@@ -637,7 +710,7 @@ function performBatchConversion(inputFolder, outputFolder, format, options) {
}
// Build pandoc command
let pandocCmd = `pandoc "${inputFile}" -o "${outputFile}"`;
let pandocCmd = `${getPandocPath()} "${inputFile}" -o "${outputFile}"`;
// Add template if specified
if (options.template && options.template !== 'default') {
@@ -727,6 +800,30 @@ app.on('activate', () => {
}
});
// IPC handlers for recent files
ipcMain.on('save-recent-files', (event, recentFiles) => {
try {
const userDataPath = app.getPath('userData');
const recentFilesPath = path.join(userDataPath, 'recent-files.json');
fs.writeFileSync(recentFilesPath, JSON.stringify(recentFiles, null, 2));
} catch (error) {
console.error('Error saving recent files:', error);
}
});
ipcMain.on('clear-recent-files', (event) => {
try {
const userDataPath = app.getPath('userData');
const recentFilesPath = path.join(userDataPath, 'recent-files.json');
fs.writeFileSync(recentFilesPath, JSON.stringify([], null, 2));
// Rebuild menu to reflect changes
createMenu();
event.reply('recent-files-cleared');
} catch (error) {
console.error('Error clearing recent files:', error);
}
});
// Handle file opening on macOS
app.on('open-file', (event, filePath) => {
event.preventDefault();
+242 -40
View File
@@ -25,6 +25,9 @@ class TabManager {
this.nextTabId = 2;
this.isPreviewVisible = true;
this.showLineNumbers = false;
this.autoSaveInterval = null;
this.autoSaveDelay = 30000; // 30 seconds
this.recentFiles = JSON.parse(localStorage.getItem('recentFiles') || '[]');
// Initialize first tab
this.tabs.set(1, {
@@ -112,6 +115,7 @@ class TabManager {
this.tabs.set(newTabId, tab);
this.createTabElements(tab);
this.switchToTab(newTabId);
this.startAutoSave();
this.updateTabBar();
}
@@ -284,6 +288,22 @@ class TabManager {
const html = marked.parse(tab.content);
const sanitizedHtml = DOMPurify.sanitize(html);
preview.innerHTML = sanitizedHtml;
// Render math expressions if KaTeX is available
if (window.katex && window.renderMathInElement) {
try {
window.renderMathInElement(preview, {
delimiters: [
{left: '$$', right: '$$', display: true},
{left: '$', right: '$', display: false},
{left: '\\[', right: '\\]', display: true},
{left: '\\(', right: '\\)', display: false}
]
});
} catch (mathError) {
console.warn('Math rendering error:', mathError);
}
}
} catch (error) {
preview.innerHTML = '<p>Error rendering preview</p>';
}
@@ -324,10 +344,33 @@ class TabManager {
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}`;
const content = tab.content;
const words = content.trim() ? content.trim().split(/\s+/).filter(word => word.length > 0).length : 0;
const chars = content.length;
const charsNoSpaces = content.replace(/\s/g, '').length;
// Enhanced statistics
const lines = content.split('\n').length;
const paragraphs = content.split(/\n\s*\n/).filter(p => p.trim()).length;
const readingTime = Math.ceil(words / 200); // Average reading speed: 200 words/minute
const sentences = content.split(/[.!?]+/).filter(s => s.trim()).length;
// Update the word count display with enhanced stats
const basicStats = `Words: ${words} | Characters: ${chars} (${charsNoSpaces} no spaces)`;
const enhancedStats = `Lines: ${lines} | Paragraphs: ${paragraphs} | Sentences: ${sentences} | Reading time: ${readingTime} min`;
document.getElementById('word-count').textContent = basicStats;
// Add enhanced stats to a separate element
let enhancedEl = document.getElementById('enhanced-stats');
if (!enhancedEl) {
enhancedEl = document.createElement('div');
enhancedEl.id = 'enhanced-stats';
enhancedEl.className = 'enhanced-stats';
document.querySelector('.status-bar').appendChild(enhancedEl);
}
enhancedEl.textContent = enhancedStats;
}
setupEditorEvents() {
@@ -408,7 +451,84 @@ class TabManager {
this.updateWordCount();
}
}
// Auto-save functionality
startAutoSave() {
if (this.autoSaveInterval) {
clearInterval(this.autoSaveInterval);
}
this.autoSaveInterval = setInterval(() => {
this.performAutoSave();
}, this.autoSaveDelay);
}
stopAutoSave() {
if (this.autoSaveInterval) {
clearInterval(this.autoSaveInterval);
this.autoSaveInterval = null;
}
}
performAutoSave() {
const tab = this.tabs.get(this.activeTabId);
if (!tab || !tab.filePath || !tab.content) return;
// Only auto-save if content has changed since last save
if (tab.lastSavedContent !== tab.content) {
ipcRenderer.send('save-file', { path: tab.filePath, content: tab.content });
tab.lastSavedContent = tab.content;
// Show brief auto-save indicator
this.showAutoSaveIndicator();
}
}
showAutoSaveIndicator() {
const indicator = document.createElement('div');
indicator.textContent = 'Auto-saved';
indicator.className = 'auto-save-indicator';
document.body.appendChild(indicator);
setTimeout(() => {
indicator.classList.add('fade-out');
setTimeout(() => {
if (indicator.parentNode) {
indicator.parentNode.removeChild(indicator);
}
}, 300);
}, 1500);
}
// Recent files functionality
addToRecentFiles(filePath) {
if (!filePath) return;
// Remove if already exists
this.recentFiles = this.recentFiles.filter(f => f !== filePath);
// Add to beginning
this.recentFiles.unshift(filePath);
// Keep only last 10 files
this.recentFiles = this.recentFiles.slice(0, 10);
// Save to localStorage and sync with main process
localStorage.setItem('recentFiles', JSON.stringify(this.recentFiles));
window.electronAPI.send('save-recent-files', this.recentFiles);
}
getRecentFiles() {
return this.recentFiles.filter(file => {
// Check if file still exists (basic check by trying to access it)
try {
return file && file.length > 0;
} catch (e) {
return false;
}
});
}
setupToolbarEvents() {
// Existing toolbar setup...
document.getElementById('btn-preview-toggle').addEventListener('click', () => {
@@ -459,6 +579,8 @@ class TabManager {
}
this.restoreTabState(this.activeTabId);
this.startAutoSave();
this.addToRecentFiles(filePath);
this.updateTabBar();
}
@@ -601,6 +723,13 @@ function hideExportDialog() {
}
function initializeExportForm(format) {
// Reset advanced export toggle to unchecked
const advancedToggle = document.getElementById('advanced-export-toggle');
const advancedOptions = document.getElementById('advanced-export-options');
advancedToggle.checked = false;
advancedOptions.classList.add('hidden');
// Reset form to defaults
document.getElementById('export-template').value = 'default';
document.getElementById('custom-template-path').style.display = 'none';
@@ -635,48 +764,69 @@ function initializeExportForm(format) {
}
function collectExportOptions() {
const options = {
template: document.getElementById('export-template').value,
metadata: {},
variables: {},
toc: document.getElementById('export-toc').checked,
tocDepth: document.getElementById('export-toc-depth').value,
numberSections: document.getElementById('export-number-sections').checked,
citeproc: document.getElementById('export-citeproc').checked
};
const advancedMode = document.getElementById('advanced-export-toggle').checked;
const options = {};
// Collect custom template path
if (options.template === 'custom') {
options.template = document.getElementById('custom-template-path').value.trim();
if (advancedMode) {
// Collect advanced options
options.template = document.getElementById('export-template').value;
options.metadata = {};
options.variables = {};
options.toc = document.getElementById('export-toc').checked;
options.tocDepth = document.getElementById('export-toc-depth').value;
options.numberSections = document.getElementById('export-number-sections').checked;
options.citeproc = document.getElementById('export-citeproc').checked;
} else {
// Collect basic options only
options.template = 'default';
options.metadata = {};
options.variables = {};
options.toc = document.getElementById('basic-toc').checked;
options.tocDepth = 3;
options.numberSections = document.getElementById('basic-number-sections').checked;
options.citeproc = false;
}
// Collect metadata
const metadataFields = document.querySelectorAll('.metadata-field');
metadataFields.forEach(field => {
const key = field.querySelector('.metadata-key').value.trim();
const value = field.querySelector('.metadata-value').value.trim();
if (key && value) {
options.metadata[key] = value;
if (advancedMode) {
// Collect custom template path
if (options.template === 'custom') {
options.template = document.getElementById('custom-template-path').value.trim();
}
});
// PDF-specific options
if (currentExportFormat === 'pdf') {
options.pdfEngine = document.getElementById('pdf-engine').value;
const geometrySelect = document.getElementById('pdf-geometry');
if (geometrySelect.value === 'custom') {
options.geometry = document.getElementById('custom-geometry').value.trim() || 'margin=1in';
} else {
options.geometry = geometrySelect.value;
// Collect metadata
const metadataFields = document.querySelectorAll('.metadata-field');
metadataFields.forEach(field => {
const key = field.querySelector('.metadata-key').value.trim();
const value = field.querySelector('.metadata-value').value.trim();
if (key && value) {
options.metadata[key] = value;
}
});
// PDF-specific options
if (currentExportFormat === 'pdf') {
options.pdfEngine = document.getElementById('pdf-engine').value;
const geometrySelect = document.getElementById('pdf-geometry');
if (geometrySelect.value === 'custom') {
options.geometry = document.getElementById('custom-geometry').value.trim() || 'margin=1in';
} else {
options.geometry = geometrySelect.value;
}
}
// Bibliography
const bibFile = document.getElementById('bibliography-file').value.trim();
const cslFile = document.getElementById('csl-file').value.trim();
if (bibFile) options.bibliography = bibFile;
if (cslFile) options.csl = cslFile;
} else {
// Basic mode - set default PDF options if needed
if (currentExportFormat === 'pdf') {
options.pdfEngine = 'xelatex';
options.geometry = 'margin=1in';
}
}
// Bibliography
const bibFile = document.getElementById('bibliography-file').value.trim();
const cslFile = document.getElementById('csl-file').value.trim();
if (bibFile) options.bibliography = bibFile;
if (cslFile) options.csl = cslFile;
return options;
}
@@ -697,6 +847,16 @@ document.addEventListener('DOMContentLoaded', () => {
}
});
// Advanced export toggle
document.getElementById('advanced-export-toggle').addEventListener('change', (e) => {
const advancedOptions = document.getElementById('advanced-export-options');
if (e.target.checked) {
advancedOptions.classList.remove('hidden');
} else {
advancedOptions.classList.add('hidden');
}
});
// Template file input
document.getElementById('template-file-input').addEventListener('change', (e) => {
const file = e.target.files[0];
@@ -941,4 +1101,46 @@ if (originalExportConfirm) {
currentBatchOptions = collectExportOptions();
}
});
}
}
// IPC event listeners for recent files functionality
if (window.electronAPI) {
window.electronAPI.on('recent-files-cleared', () => {
tabManager.recentFiles = [];
localStorage.setItem('recentFiles', JSON.stringify([]));
console.log('Recent files cleared');
});
}
// Add math rendering support using KaTeX for enhanced preview
function initMathSupport() {
// Add KaTeX CSS
const katexCSS = document.createElement('link');
katexCSS.rel = 'stylesheet';
katexCSS.href = 'https://cdn.jsdelivr.net/npm/katex@0.16.8/dist/katex.min.css';
katexCSS.crossOrigin = 'anonymous';
document.head.appendChild(katexCSS);
// Add KaTeX JS
const katexJS = document.createElement('script');
katexJS.src = 'https://cdn.jsdelivr.net/npm/katex@0.16.8/dist/katex.min.js';
katexJS.crossOrigin = 'anonymous';
katexJS.onload = () => {
// Add auto-render extension
const autoRenderJS = document.createElement('script');
autoRenderJS.src = 'https://cdn.jsdelivr.net/npm/katex@0.16.8/dist/contrib/auto-render.min.js';
autoRenderJS.crossOrigin = 'anonymous';
autoRenderJS.onload = () => {
console.log('Math support (KaTeX) initialized');
// Re-render current preview to include math
if (tabManager) {
tabManager.updatePreview();
}
};
document.head.appendChild(autoRenderJS);
};
document.head.appendChild(katexJS);
}
// Initialize math support on load
initMathSupport();
+177
View File
@@ -1406,4 +1406,181 @@ body.theme-github .line-numbers {
background: #fafbfc;
border-right-color: #e1e4e8;
color: #586069;
}
/* Advanced Export Toggle Styles */
.export-mode-toggle {
border-bottom: 1px solid #ddd;
padding-bottom: 15px;
margin-bottom: 20px;
}
.checkbox-label {
display: flex;
align-items: center;
cursor: pointer;
font-weight: 500;
margin-bottom: 5px;
}
.checkbox-label input[type="checkbox"] {
margin-right: 10px;
transform: scale(1.2);
}
.export-help {
color: #666;
font-size: 12px;
font-style: italic;
margin-left: 25px;
}
.advanced-options {
transition: all 0.3s ease;
overflow: hidden;
}
.advanced-options.hidden {
display: none;
}
.basic-options {
background: #f8f9fa;
border: 1px solid #e9ecef;
border-radius: 6px;
padding: 15px;
margin-top: 10px;
}
.basic-options label {
font-weight: normal;
margin-bottom: 8px;
}
/* Theme support for advanced export toggle */
body.theme-dark .export-mode-toggle {
border-color: #444;
}
body.theme-dark .export-help {
color: #999;
}
body.theme-dark .basic-options {
background: #1a1a1a;
border-color: #444;
}
body.theme-dark .checkbox-label {
color: #f0f0f0;
}
body.theme-solarized .export-mode-toggle {
border-color: #eee8d5;
}
body.theme-solarized .export-help {
color: #586e75;
}
body.theme-solarized .basic-options {
background: #fdf6e3;
border-color: #eee8d5;
}
body.theme-monokai .export-mode-toggle {
border-color: #49483e;
}
body.theme-monokai .export-help {
color: #75715e;
}
body.theme-monokai .basic-options {
background: #2f2f2f;
border-color: #49483e;
}
body.theme-github .export-mode-toggle {
border-color: #e1e4e8;
}
body.theme-github .export-help {
color: #586069;
}
body.theme-github .basic-options {
background: #f6f8fa;
border-color: #e1e4e8;
}
/* Enhanced Statistics Styles */
.enhanced-stats {
font-size: 11px;
color: #666;
padding: 2px 0;
border-top: 1px solid #ddd;
margin-top: 2px;
font-family: monospace;
}
/* Theme support for enhanced stats */
body.theme-dark .enhanced-stats {
color: #999;
border-color: #444;
}
body.theme-solarized .enhanced-stats {
color: #586e75;
border-color: #eee8d5;
}
body.theme-monokai .enhanced-stats {
color: #75715e;
border-color: #49483e;
}
body.theme-github .enhanced-stats {
color: #586069;
border-color: #e1e4e8;
}
/* Auto-save indicator */
.auto-save-indicator {
position: fixed;
top: 20px;
right: 20px;
background: #28a745;
color: white;
padding: 8px 16px;
border-radius: 4px;
font-size: 12px;
font-weight: 500;
z-index: 10000;
box-shadow: 0 2px 8px rgba(0,0,0,0.2);
animation: slideInRight 0.3s ease-out;
}
.auto-save-indicator.fade-out {
animation: fadeOut 0.3s ease-out forwards;
}
@keyframes slideInRight {
from {
transform: translateX(100%);
opacity: 0;
}
to {
transform: translateX(0);
opacity: 1;
}
}
@keyframes fadeOut {
from {
opacity: 1;
}
to {
opacity: 0;
}
}