diff --git a/src/main.js b/src/main.js
index d9a0b33..e55ae1c 100644
--- a/src/main.js
+++ b/src/main.js
@@ -1,7 +1,7 @@
const { app, BrowserWindow, Menu, dialog, ipcMain, shell } = require('electron');
const path = require('path');
const fs = require('fs');
-const { exec } = require('child_process');
+const { exec, execFile, spawn } = require('child_process');
const { PDFDocument, rgb, degrees, StandardFonts } = require('pdf-lib');
const WordTemplateExporter = require('./wordTemplateExporter');
@@ -25,12 +25,95 @@ function getPandocPath() {
function checkPandocAvailable() {
return new Promise((resolve) => {
const pandocPath = getPandocPath();
- exec(`${pandocPath} --version`, (error) => {
+ execFile(pandocPath, ['--version'], (error) => {
resolve(!error);
});
});
}
+/**
+ * Safe command execution using execFile (no shell injection risk)
+ * @param {string} command - The command to execute
+ * @param {string[]} args - Array of arguments
+ * @param {object} options - Options for execFile
+ * @returns {Promise<{stdout: string, stderr: string}>}
+ */
+function safeExecFile(command, args, options = {}) {
+ return new Promise((resolve, reject) => {
+ execFile(command, args, { maxBuffer: 10 * 1024 * 1024, ...options }, (error, stdout, stderr) => {
+ if (error) {
+ reject({ error, stdout, stderr });
+ } else {
+ resolve({ stdout, stderr });
+ }
+ });
+ });
+}
+
+/**
+ * Run Pandoc command safely with execFile
+ * @param {string[]} args - Pandoc arguments array
+ * @param {Function} callback - Callback function (error, stdout, stderr)
+ */
+function runPandoc(args, callback) {
+ const pandocPath = getPandocPath();
+ execFile(pandocPath, args, { maxBuffer: 10 * 1024 * 1024 }, callback);
+}
+
+/**
+ * Run a Pandoc command string safely using execFile
+ * Parses the command string and uses execFile to prevent shell injection
+ * @param {string} cmdString - Full Pandoc command string (e.g., 'pandoc "input.md" -o "output.pdf"')
+ * @param {Function} callback - Callback function (error, stdout, stderr)
+ */
+function runPandocCmd(cmdString, callback) {
+ const parsed = parseCommand(cmdString);
+ // Skip 'pandoc' if it's the first element (command itself)
+ const args = parsed.command === 'pandoc' ? parsed.args : [parsed.command, ...parsed.args];
+ const pandocPath = getPandocPath();
+ execFile(pandocPath, args, { maxBuffer: 10 * 1024 * 1024 }, callback);
+}
+
+/**
+ * Parse a command string into command and arguments array
+ * This helps transition from exec() to execFile() safely
+ * @param {string} cmdString - Full command string
+ * @returns {{command: string, args: string[]}}
+ */
+function parseCommand(cmdString) {
+ // Handle quoted strings properly
+ const parts = [];
+ let current = '';
+ let inQuotes = false;
+ let quoteChar = '';
+
+ for (let i = 0; i < cmdString.length; i++) {
+ const char = cmdString[i];
+ if ((char === '"' || char === "'") && !inQuotes) {
+ inQuotes = true;
+ quoteChar = char;
+ } else if (char === quoteChar && inQuotes) {
+ inQuotes = false;
+ quoteChar = '';
+ } else if (char === ' ' && !inQuotes) {
+ if (current) {
+ parts.push(current);
+ current = '';
+ }
+ } else {
+ current += char;
+ }
+ }
+ if (current) {
+ parts.push(current);
+ }
+
+ return {
+ command: parts[0],
+ args: parts.slice(1)
+ };
+}
+
// Simple storage implementation to replace electron-store
const settingsPath = path.join(app.getPath('userData'), 'settings.json');
const store = {
@@ -180,15 +263,15 @@ if (!gotTheLock) {
});
}
-// Check if pandoc is available
+// Check if pandoc is available (using execFile for consistency)
function checkPandocAvailability() {
return new Promise((resolve) => {
if (pandocAvailable !== null) {
resolve(pandocAvailable);
return;
}
-
- exec('pandoc --version', (error, stdout, stderr) => {
+
+ execFile('pandoc', ['--version'], (error, stdout, stderr) => {
pandocAvailable = !error;
resolve(pandocAvailable);
});
@@ -284,6 +367,11 @@ function createMenu() {
accelerator: 'CmdOrCtrl+O',
click: openFile
},
+ {
+ label: 'Open PDF',
+ accelerator: 'CmdOrCtrl+Shift+O',
+ click: openPdfFile
+ },
{
label: 'Save',
accelerator: 'CmdOrCtrl+S',
@@ -401,29 +489,33 @@ function createMenu() {
{
label: 'Theme',
submenu: [
+ // Light Themes (grouped first)
+ { label: 'Atom One Light (Default)', click: () => setTheme('atomonelight') },
+ { label: 'GitHub Light', click: () => setTheme('github') },
{ label: 'Light', click: () => setTheme('light') },
- { label: 'Dark', click: () => setTheme('dark') },
- { label: 'Solarized', click: () => setTheme('solarized') },
- { label: 'Monokai', click: () => setTheme('monokai') },
- { label: 'GitHub', click: () => setTheme('github') },
+ { label: 'Solarized Light', click: () => setTheme('solarized') },
+ { label: 'Gruvbox Light', click: () => setTheme('gruvbox-light') },
+ { label: 'Ayu Light', click: () => setTheme('ayu-light') },
+ { label: 'Sepia', click: () => setTheme('sepia') },
+ { label: 'Paper', click: () => setTheme('paper') },
+ { label: 'Rose Pine Dawn', click: () => setTheme('rosepine-dawn') },
+ { label: 'Concrete Light', click: () => setTheme('concrete-light') },
{ type: 'separator' },
+ // Dark Themes
+ { label: 'Dark', click: () => setTheme('dark') },
+ { label: 'One Dark', click: () => setTheme('onedark') },
{ label: 'Dracula', click: () => setTheme('dracula') },
{ label: 'Nord', click: () => setTheme('nord') },
- { label: 'One Dark', click: () => setTheme('onedark') },
- { label: 'Atom One Light', click: () => setTheme('atomonelight') },
+ { label: 'Monokai', click: () => setTheme('monokai') },
{ label: 'Material', click: () => setTheme('material') },
{ label: 'Gruvbox Dark', click: () => setTheme('gruvbox-dark') },
- { label: 'Gruvbox Light', click: () => setTheme('gruvbox-light') },
{ label: 'Tokyo Night', click: () => setTheme('tokyonight') },
{ label: 'Palenight', click: () => setTheme('palenight') },
{ label: 'Ayu Dark', click: () => setTheme('ayu-dark') },
- { label: 'Ayu Light', click: () => setTheme('ayu-light') },
{ label: 'Ayu Mirage', click: () => setTheme('ayu-mirage') },
{ label: 'Oceanic Next', click: () => setTheme('oceanic-next') },
{ label: 'Cobalt2', click: () => setTheme('cobalt2') },
- { type: 'separator' },
{ label: 'Concrete Dark', click: () => setTheme('concrete-dark') },
- { label: 'Concrete Light', click: () => setTheme('concrete-light') },
{ label: 'Concrete Warm', click: () => setTheme('concrete-warm') }
]
},
@@ -461,8 +553,25 @@ function createMenu() {
label: 'Batch',
submenu: [
{
- label: 'Convert Folder...',
+ label: 'Convert Markdown Folder...',
click: () => showBatchConversionDialog()
+ },
+ { type: 'separator' },
+ {
+ label: 'Batch Image Conversion...',
+ click: () => mainWindow.webContents.send('show-batch-converter', 'image')
+ },
+ {
+ label: 'Batch Audio Conversion...',
+ click: () => mainWindow.webContents.send('show-batch-converter', 'audio')
+ },
+ {
+ label: 'Batch Video Conversion...',
+ click: () => mainWindow.webContents.send('show-batch-converter', 'video')
+ },
+ {
+ label: 'Batch PDF Conversion...',
+ click: () => mainWindow.webContents.send('show-batch-converter', 'pdf')
}
]
},
@@ -473,27 +582,18 @@ function createMenu() {
label: 'Universal File Converter...',
accelerator: 'CmdOrCtrl+Shift+C',
click: () => showUniversalConverterDialog()
- },
- {
- type: 'separator'
- },
- {
- label: 'About Converter',
- click: () => {
- dialog.showMessageBox(mainWindow, {
- type: 'info',
- title: 'About Universal Converter',
- message: 'Open-Source File Converter',
- detail: 'PanConverter includes a powerful open-source file conversion system.\n\nSupported Converters:\n⢠LibreOffice - Document conversions (DOCX, PDF, ODT, etc.)\n⢠ImageMagick - Image format conversions\n⢠FFmpeg - Media file conversions\n⢠Pandoc - Document markup conversions\n\nFeatures:\n⢠100% open-source and free\n⢠No API keys required\n⢠Runs completely offline\n⢠Supports 100+ file formats\n⢠High-quality professional conversions',
- buttons: ['OK']
- });
- }
}
]
},
{
label: 'PDF Editor',
submenu: [
+ {
+ label: 'Open PDF File...',
+ accelerator: 'CmdOrCtrl+Shift+O',
+ click: () => openPDFFile()
+ },
+ { type: 'separator' },
{
label: 'Merge PDFs...',
click: () => showPDFEditorDialog('merge')
@@ -570,12 +670,15 @@ function createMenu() {
submenu: [
{
label: 'Table Generator',
- click: () => mainWindow.webContents.send('show-table-generator')
+ accelerator: 'CmdOrCtrl+Shift+T',
+ click: () => openTableGenerator()
},
{
label: 'ASCII Art Generator',
- click: () => mainWindow.webContents.send('show-ascii-generator')
+ accelerator: 'CmdOrCtrl+Shift+A',
+ click: () => openAsciiGenerator()
},
+ { type: 'separator' },
{
label: 'Document Compare',
click: () => mainWindow.webContents.send('show-document-compare')
@@ -586,20 +689,26 @@ function createMenu() {
label: 'Help',
submenu: [
{
- label: 'About',
- click: () => {
- dialog.showMessageBox(mainWindow, {
- type: 'info',
- title: 'About PanConverter',
- message: 'PanConverter',
- detail: 'A cross-platform Markdown editor and converter using Pandoc.\n\nVersion: 2.1.0\nAuthor: Amit Haridas\nEmail: amit.wh@gmail.com\nLicense: MIT\n\n⨠New in v2.1.0:\n⢠Subtle & Small Preview Popout Button - Cleaner, minimalist design\n⢠Simplified Table Headers - Light gray background with better readability\n⢠Comprehensive Format-to-Markdown Conversion - Support for 30+ formats (RST, Textile, MediaWiki, Org-mode, AsciiDoc, CSV, JSON, and more)\n⢠Exhaustive ASCII Art Generator - 5 text banner styles (Standard, Banner, Block, Bubble, Digital) and 19 professional templates\n⢠Enhanced Templates - Flowcharts, sequence diagrams, network diagrams, hierarchy trees, timelines, note boxes, warning boxes, and decorative elements\n\n⨠Features from v2.0.0:\n⢠Export Profiles - Save and load export configurations\n⢠Mermaid.js diagram support - Render flowcharts, sequence diagrams, and more\n⢠Command Palette (Ctrl+Shift+P) - Quick access to all commands\n⢠GitHub Light/Dark preview themes - Beautiful code preview styling\n⢠Table Generator - Interactive table creation tool\n⢠Resizable Preview Pane - Drag divider to adjust editor/preview sizes\n⢠Pop-out Preview Window - Open preview in separate window with live sync\n\nCore Features:\n⢠Configurable page sizes (A3, A4, A5, B4, B5, Letter, Legal, Tabloid, Custom) with orientation support\n⢠Page size support for all export formats (PDF, DOCX, ODT, PowerPoint) and batch conversion\n⢠Preserved ASCII art, charts, tables, flowcharts in exports with full-width background\n⢠Custom Headers & Footers for PDF, DOCX, ODT, and PowerPoint exports\n⢠Dynamic field support: $PAGE$, $TOTAL$, $DATE$, $TIME$, $TITLE$, $AUTHOR$, $FILENAME$\n⢠Logo/image embedding in headers and footers\n⢠Modern glassmorphism UI with gradient backgrounds\n⢠Enhanced PDF export via Word template with configurable start page\n⢠Streamlined PDF Editor UI (merge, split, compress, rotate, watermark, encrypt)\n⢠Universal File Converter (LibreOffice, ImageMagick, FFmpeg, Pandoc)\n⢠Windows Explorer context menu integration\n⢠Tabbed interface for multiple files\n⢠Advanced markdown editing with live preview\n⢠Real-time preview updates while typing\n⢠Full toolbar markdown editing functions\n⢠Enhanced PDF export with built-in Electron fallback\n⢠Enhanced Word export with template support (single file & batch)\n⢠File association support for .md files\n⢠Command-line interface for batch conversion\n⢠Advanced export options with templates and metadata\n⢠Batch file conversion with progress tracking\n⢠Adjustable font sizes via menu (Ctrl+Shift+Plus/Minus)\n⢠22 beautiful themes (including Dracula, Nord, Tokyo Night, Gruvbox, Ayu, Concrete, and more)\n⢠Find & replace with match highlighting\n⢠Line numbers and auto-indentation\n⢠Undo/redo functionality\n⢠Live word count and statistics',
- buttons: ['OK']
- });
- }
+ label: 'About MarkdownConverter',
+ click: () => showAboutDialog()
},
+ { type: 'separator' },
+ {
+ label: 'Dependencies & Requirements',
+ click: () => showDependenciesDialog()
+ },
+ { type: 'separator' },
{
label: 'Documentation',
- click: () => shell.openExternal('https://github.com/amitwh/pan-converter')
+ click: () => shell.openExternal('https://github.com/amitwh/markdown-converter')
+ },
+ {
+ label: 'Report Issue',
+ click: () => shell.openExternal('https://github.com/amitwh/markdown-converter/issues')
+ },
+ {
+ label: 'Check for Updates',
+ click: () => shell.openExternal('https://github.com/amitwh/markdown-converter/releases')
}
]
}
@@ -609,6 +718,237 @@ function createMenu() {
Menu.setApplicationMenu(menu);
}
+// Show About Dialog with logo
+function showAboutDialog() {
+ const aboutWindow = new BrowserWindow({
+ width: 500,
+ height: 600,
+ parent: mainWindow,
+ modal: true,
+ resizable: false,
+ minimizable: false,
+ maximizable: false,
+ webPreferences: {
+ nodeIntegration: false,
+ contextIsolation: true
+ },
+ icon: path.join(__dirname, '../assets/icon.png')
+ });
+
+ // Convert images to base64 for data URL compatibility
+ let logoBase64 = '';
+ let iconBase64 = '';
+ try {
+ const logoPath = path.join(__dirname, '../assets/logo.png');
+ const iconPath = path.join(__dirname, '../assets/icon.png');
+ if (fs.existsSync(logoPath)) {
+ logoBase64 = 'data:image/png;base64,' + fs.readFileSync(logoPath).toString('base64');
+ }
+ if (fs.existsSync(iconPath)) {
+ iconBase64 = 'data:image/png;base64,' + fs.readFileSync(iconPath).toString('base64');
+ }
+ } catch (e) {
+ console.error('Error loading about dialog images:', e);
+ }
+
+ const aboutHTML = `
+
+
+
+
+
About MarkdownConverter
+
+
+
+

+
MarkdownConverter
+
Version 3.0.0
+
+
+
by
+

+
+
+
+
Features
+
+ - Professional Markdown editing with syntax highlighting
+ - Universal file converter (Image, Audio, Video, PDF)
+ - Batch conversion for all media types
+ - Advanced PDF Editor (merge, split, watermark, encrypt)
+ - Export to PDF, DOCX, HTML, LaTeX, EPUB & more
+ - 23+ beautiful themes including ConcreteInfo theme
+ - ASCII Art & Table generators
+ - Code file syntax highlighting
+
+
+
+
+
+
+
+`;
+
+ aboutWindow.loadURL('data:text/html;charset=utf-8,' + encodeURIComponent(aboutHTML));
+ aboutWindow.setMenuBarVisibility(false);
+}
+
+// Show Dependencies Dialog
+function showDependenciesDialog() {
+ const depsWindow = new BrowserWindow({
+ width: 600,
+ height: 700,
+ parent: mainWindow,
+ modal: true,
+ resizable: true,
+ webPreferences: {
+ nodeIntegration: false,
+ contextIsolation: true
+ },
+ icon: path.join(__dirname, '../assets/icon.png')
+ });
+
+ const depsHTML = `
+
+
+
+
+
Dependencies & Requirements
+
+
+
+
Dependencies & Requirements
+
+
Required Dependencies
+
+
+
+
Optional Dependencies (for extended features)
+
+
+
+
+
+
+
+
+
+
Bundled Libraries
+
+
+
pdf-lib
+
PDF manipulation library for merge, split, watermark, and encryption features.
+
+
+
+
marked
+
Markdown parser for preview rendering.
+
+
+
+
highlight.js
+
Syntax highlighting for code blocks.
+
+
+
+
DOMPurify
+
HTML sanitization for security.
+
+
+`;
+
+ depsWindow.loadURL('data:text/html;charset=utf-8,' + encodeURIComponent(depsHTML));
+ depsWindow.setMenuBarVisibility(false);
+}
+
+// Open PDF File for viewing/editing
+function openPDFFile() {
+ const files = dialog.showOpenDialogSync(mainWindow, {
+ properties: ['openFile'],
+ filters: [
+ { name: 'PDF Files', extensions: ['pdf'] }
+ ]
+ });
+
+ if (files && files[0]) {
+ mainWindow.webContents.send('open-pdf-viewer', files[0]);
+ }
+}
+
function openFile() {
const files = dialog.showOpenDialogSync(mainWindow, {
properties: ['openFile'],
@@ -625,6 +965,20 @@ function openFile() {
}
}
+function openPdfFile() {
+ const files = dialog.showOpenDialogSync(mainWindow, {
+ properties: ['openFile'],
+ filters: [
+ { name: 'PDF Files', extensions: ['pdf'] },
+ { name: 'All Files', extensions: ['*'] }
+ ]
+ });
+
+ if (files && files[0]) {
+ mainWindow.webContents.send('open-pdf-file', files[0]);
+ }
+}
+
function saveAsFile() {
const file = dialog.showSaveDialogSync(mainWindow, {
defaultExt: '.md',
@@ -1133,15 +1487,15 @@ async function exportPDFViaWordTemplate() {
const exporter = new WordTemplateExporter(wordTemplatePath, templateStartPage);
await exporter.convert(content, tempDocxPath);
- // Step 2: Convert DOCX to PDF using LibreOffice
+ // Step 2: Convert DOCX to PDF using LibreOffice (using execFile for safety)
const soffice = process.platform === 'win32'
- ? '"C:\\Program Files\\LibreOffice\\program\\soffice.exe"'
+ ? 'C:\\Program Files\\LibreOffice\\program\\soffice.exe'
: 'soffice';
const outputDir = path.dirname(result.filePath);
- const convertCmd = `${soffice} --headless --convert-to pdf --outdir "${outputDir}" "${tempDocxPath}"`;
+ const sofficeArgs = ['--headless', '--convert-to', 'pdf', '--outdir', outputDir, tempDocxPath];
- exec(convertCmd, (error, stdout, stderr) => {
+ execFile(soffice, sofficeArgs, (error, stdout, stderr) => {
// Clean up temporary DOCX file
try {
fs.unlinkSync(tempDocxPath);
@@ -1190,26 +1544,34 @@ function showPDFEditorDialog(operation) {
mainWindow.webContents.send('show-pdf-editor-dialog', operation);
}
-// Check if conversion tool is available
+// Handle PDF editor from toolbar (with optional file path)
+ipcMain.on('show-pdf-editor-from-toolbar', (event, { operation, filePath }) => {
+ mainWindow.webContents.send('show-pdf-editor-dialog', operation, filePath);
+});
+
+// Check if conversion tool is available (using execFile for safety)
function checkConverterAvailable(tool) {
return new Promise((resolve) => {
- let command;
+ const isWin = process.platform === 'win32';
+ const locateCmd = isWin ? 'where' : 'which';
+ let toolName;
+
switch (tool) {
case 'libreoffice':
- command = process.platform === 'win32' ? 'where soffice' : 'which soffice';
+ toolName = 'soffice';
break;
case 'imagemagick':
- command = process.platform === 'win32' ? 'where magick' : 'which convert';
+ toolName = isWin ? 'magick' : 'convert';
break;
case 'ffmpeg':
- command = process.platform === 'win32' ? 'where ffmpeg' : 'which ffmpeg';
+ toolName = 'ffmpeg';
break;
default:
resolve(false);
return;
}
- exec(command, (error) => {
+ execFile(locateCmd, [toolName], (error) => {
resolve(!error);
});
});
@@ -1230,26 +1592,27 @@ ipcMain.on('universal-convert', async (event, { tool, fromFormat, toFormat, file
mainWindow.webContents.send('conversion-status', 'Converting file...');
const outputPath = filePath.replace(/\.[^/.]+$/, `.${toFormat}`);
- let conversionCmd;
+ let conversionInfo;
switch (tool) {
case 'libreoffice':
- conversionCmd = convertWithLibreOffice(filePath, toFormat, outputPath);
+ conversionInfo = convertWithLibreOffice(filePath, toFormat, outputPath);
break;
case 'imagemagick':
- conversionCmd = convertWithImageMagick(filePath, outputPath);
+ conversionInfo = convertWithImageMagick(filePath, outputPath);
break;
case 'ffmpeg':
- conversionCmd = convertWithFFmpeg(filePath, outputPath);
+ conversionInfo = convertWithFFmpeg(filePath, outputPath);
break;
case 'pandoc':
- conversionCmd = `pandoc "${filePath}" -o "${outputPath}"`;
+ conversionInfo = convertWithPandoc(filePath, outputPath);
break;
default:
throw new Error(`Unknown conversion tool: ${tool}`);
}
- exec(conversionCmd, (error, stdout, stderr) => {
+ // Use execFile for safety (prevents command injection)
+ execFile(conversionInfo.command, conversionInfo.args, (error, stdout, stderr) => {
if (error) {
mainWindow.webContents.send('conversion-complete', {
success: false,
@@ -1294,11 +1657,11 @@ ipcMain.on('universal-convert', async (event, { tool, fromFormat, toFormat, file
}
});
-// LibreOffice conversion command builder
+// LibreOffice conversion - returns {command, args} for execFile (safer than exec)
function convertWithLibreOffice(inputFile, outputFormat, outputPath) {
const outputDir = path.dirname(outputPath);
const soffice = process.platform === 'win32'
- ? '"C:\\Program Files\\LibreOffice\\program\\soffice.exe"'
+ ? 'C:\\Program Files\\LibreOffice\\program\\soffice.exe'
: 'soffice';
// LibreOffice conversion format mapping
@@ -1321,19 +1684,36 @@ function convertWithLibreOffice(inputFile, outputFormat, outputPath) {
const format = formatMap[outputFormat] || outputFormat;
- // Use headless mode for conversion
- return `${soffice} --headless --convert-to ${format} --outdir "${outputDir}" "${inputFile}"`;
+ // Return command and args for execFile
+ return {
+ command: soffice,
+ args: ['--headless', '--convert-to', format, '--outdir', outputDir, inputFile]
+ };
}
-// ImageMagick conversion command builder
+// ImageMagick conversion - returns {command, args} for execFile
function convertWithImageMagick(inputFile, outputPath) {
const magick = process.platform === 'win32' ? 'magick' : 'convert';
- return `${magick} "${inputFile}" "${outputPath}"`;
+ return {
+ command: magick,
+ args: [inputFile, outputPath]
+ };
}
-// FFmpeg conversion command builder
+// FFmpeg conversion - returns {command, args} for execFile
function convertWithFFmpeg(inputFile, outputPath) {
- return `ffmpeg -i "${inputFile}" "${outputPath}" -y`;
+ return {
+ command: 'ffmpeg',
+ args: ['-i', inputFile, outputPath, '-y']
+ };
+}
+
+// Pandoc conversion - returns {command, args} for execFile
+function convertWithPandoc(inputFile, outputPath) {
+ return {
+ command: 'pandoc',
+ args: [inputFile, '-o', outputPath]
+ };
}
function performExportWithOptions(format, options) {
@@ -1449,8 +1829,8 @@ function performExportWithOptions(format, options) {
pandocCmd += ' --variable header-includes="\\\\usepackage{lastpage}"';
}
- // Try with specified PDF engine
- exec(pandocCmd, (error) => {
+ // Try with specified PDF engine (using runPandocCmd for safety)
+ runPandocCmd(pandocCmd, (error) => {
if (error) {
// Try fallback engines if the specified one fails
const fallbackEngines = ['pdflatex', 'lualatex'];
@@ -1544,7 +1924,8 @@ function tryPdfFallback(inputFile, outputFile, engines, index, options, lastErro
}
}
- exec(pandocCmd, (error) => {
+ // Use runPandocCmd for safety (prevents command injection)
+ runPandocCmd(pandocCmd, (error) => {
if (error) {
tryPdfFallback(inputFile, outputFile, engines, index + 1, options, error);
} else {
@@ -1562,11 +1943,11 @@ function showExportSuccess(outputFile) {
});
}
-// Helper function to export with pandoc (general)
+// Helper function to export with pandoc (general) - uses runPandocCmd for safety
function exportWithPandoc(pandocCmd, outputFile, format) {
console.log(`Executing Pandoc command: ${pandocCmd}`);
- exec(pandocCmd, async (error, stdout, stderr) => {
+ runPandocCmd(pandocCmd, async (error, stdout, stderr) => {
if (error) {
console.error(`Pandoc error for ${format}:`, error);
console.error(`Pandoc stderr:`, stderr);
@@ -1642,14 +2023,14 @@ function exportWithPandoc(pandocCmd, outputFile, format) {
});
}
-// Helper function to export PDF with pandoc (with fallbacks)
+// Helper function to export PDF with pandoc (with fallbacks) - uses runPandocCmd for safety
function exportWithPandocPDF(pandocCmd, outputFile) {
- exec(pandocCmd, (error, stdout, stderr) => {
+ runPandocCmd(pandocCmd, (error, stdout, stderr) => {
if (error) {
console.log('XeLaTeX failed, trying PDFLaTeX...');
// Fallback to pdflatex
const fallbackCmd = pandocCmd.replace('--pdf-engine=xelatex', '--pdf-engine=pdflatex');
- exec(fallbackCmd, (fallbackError, fallbackStdout, fallbackStderr) => {
+ runPandocCmd(fallbackCmd, (fallbackError, fallbackStdout, fallbackStderr) => {
if (fallbackError) {
console.log('PDFLaTeX failed, trying Electron PDF...');
// Final fallback to Electron PDF
@@ -1970,10 +2351,10 @@ function importDocument() {
additionalOptions = '--from=json -t markdown';
}
- // Convert to markdown using pandoc
+ // Convert to markdown using pandoc (using runPandocCmd for safety)
const pandocCmd = `${getPandocPath()} "${inputFile}" -t markdown ${additionalOptions} -o "${outputFile}"`;
- exec(pandocCmd, (error, stdout, stderr) => {
+ runPandocCmd(pandocCmd, (error, stdout, stderr) => {
if (error) {
dialog.showErrorBox('Import Error', `Failed to import: ${error.message}\n\nMake sure Pandoc is installed.\n\nSupported formats: DOCX, ODT, RTF, HTML, LaTeX, EPUB, PDF, PPTX, ODP, RST, Textile, MediaWiki, Org-mode, AsciiDoc, CSV, and more.`);
} else {
@@ -2014,7 +2395,7 @@ ipcMain.on('save-current-file', (event, content) => {
});
ipcMain.on('get-theme', (event) => {
- const theme = store.get('theme', 'github');
+ const theme = store.get('theme', 'atomonelight');
event.reply('theme-changed', theme);
});
@@ -2288,15 +2669,15 @@ function performBatchConversion(inputFolder, outputFolder, format, options) {
const exporter = new WordTemplateExporter(wordTemplatePath, templateStartPage);
await exporter.convert(content, tempDocxPath);
- // Step 2: Convert DOCX to PDF using LibreOffice
+ // Step 2: Convert DOCX to PDF using LibreOffice (using execFile for safety)
const soffice = process.platform === 'win32'
- ? '"C:\\Program Files\\LibreOffice\\program\\soffice.exe"'
+ ? 'C:\\Program Files\\LibreOffice\\program\\soffice.exe'
: 'soffice';
const outputDir = path.dirname(outputFile);
- const convertCmd = `${soffice} --headless --convert-to pdf --outdir "${outputDir}" "${tempDocxPath}"`;
+ const sofficeArgs = ['--headless', '--convert-to', 'pdf', '--outdir', outputDir, tempDocxPath];
- exec(convertCmd, (error, stdout, stderr) => {
+ execFile(soffice, sofficeArgs, (error, stdout, stderr) => {
// Clean up temporary DOCX file
try {
fs.unlinkSync(tempDocxPath);
@@ -2447,8 +2828,8 @@ function performBatchConversion(inputFolder, outputFolder, format, options) {
pandocCmd += ` --variable footer="${footerText}"`;
}
- // Execute conversion
- exec(pandocCmd, async (error, stdout, stderr) => {
+ // Execute conversion (using runPandocCmd for safety)
+ runPandocCmd(pandocCmd, async (error, stdout, stderr) => {
if (!error) {
// Add headers/footers to DOCX if enabled
if (format === 'docx' && headerFooterSettings.enabled) {
@@ -2563,24 +2944,25 @@ function performCLIConversion(inputPath, format) {
console.log(`Converting "${path.basename(inputPath)}" to ${format.toUpperCase()}...`);
- // Use existing export functions but with CLI output
+ // Use existing export functions but with CLI output (using runPandocCmd for safety)
const pandocCommand = buildPandocCommand(content, format, outputPath);
-
- exec(pandocCommand, (error, stdout, stderr) => {
+
+ runPandocCmd(pandocCommand, (error, stdout, stderr) => {
if (error) {
console.error(`Conversion failed: ${error.message}`);
if (stderr) console.error(`Details: ${stderr}`);
app.quit();
return;
}
-
+
console.log(`Successfully converted to: ${outputPath}`);
-
- // Show Windows notification
+
+ // Show Windows notification (using exec for PowerShell is acceptable here - hardcoded command)
if (process.platform === 'win32') {
- exec(`powershell -Command "New-BurntToastNotification -Text 'PanConverter', 'File converted to ${format.toUpperCase()}' -AppLogo '${path.join(__dirname, '../assets/icon.png')}'"`, () => {});
+ const iconPath = path.join(__dirname, '../assets/icon.png');
+ execFile('powershell', ['-Command', `New-BurntToastNotification -Text 'PanConverter', 'File converted to ${format.toUpperCase()}' -AppLogo '${iconPath}'`], () => {});
}
-
+
app.quit();
});
} catch (error) {
@@ -3329,4 +3711,85 @@ ipcMain.on('select-pdf-folder', (event, inputId) => {
if (folder && folder[0]) {
event.reply('pdf-folder-selected', { inputId, path: folder[0] });
}
+});
+
+// ============================================
+// ASCII Art Generator Window
+// ============================================
+let asciiGeneratorWindow = null;
+
+function openAsciiGenerator() {
+ if (asciiGeneratorWindow) {
+ asciiGeneratorWindow.focus();
+ return;
+ }
+
+ asciiGeneratorWindow = new BrowserWindow({
+ width: 800,
+ height: 700,
+ parent: mainWindow,
+ modal: false,
+ title: 'ASCII Art Generator',
+ icon: path.join(__dirname, '../assets/icon.png'),
+ webPreferences: {
+ nodeIntegration: false,
+ contextIsolation: true,
+ preload: path.join(__dirname, 'preload.js')
+ }
+ });
+
+ asciiGeneratorWindow.loadFile(path.join(__dirname, 'ascii-generator.html'));
+ asciiGeneratorWindow.setMenuBarVisibility(false);
+
+ asciiGeneratorWindow.on('closed', () => {
+ asciiGeneratorWindow = null;
+ });
+}
+
+ipcMain.on('open-ascii-generator', () => {
+ openAsciiGenerator();
+});
+
+// ============================================
+// Table Generator Window
+// ============================================
+let tableGeneratorWindow = null;
+
+function openTableGenerator() {
+ if (tableGeneratorWindow) {
+ tableGeneratorWindow.focus();
+ return;
+ }
+
+ tableGeneratorWindow = new BrowserWindow({
+ width: 900,
+ height: 700,
+ parent: mainWindow,
+ modal: false,
+ title: 'Table Generator',
+ icon: path.join(__dirname, '../assets/icon.png'),
+ webPreferences: {
+ nodeIntegration: false,
+ contextIsolation: true,
+ preload: path.join(__dirname, 'preload.js')
+ }
+ });
+
+ tableGeneratorWindow.loadFile(path.join(__dirname, 'table-generator.html'));
+ tableGeneratorWindow.setMenuBarVisibility(false);
+
+ tableGeneratorWindow.on('closed', () => {
+ tableGeneratorWindow = null;
+ });
+}
+
+ipcMain.on('open-table-generator', () => {
+ openTableGenerator();
+});
+
+// IPC Handler to receive generated content from generator windows
+ipcMain.on('insert-generated-content', (event, content) => {
+ if (mainWindow && !mainWindow.isDestroyed()) {
+ mainWindow.webContents.send('insert-content', content);
+ }
});
\ No newline at end of file
diff --git a/src/preload.js b/src/preload.js
new file mode 100644
index 0000000..bd04aa4
--- /dev/null
+++ b/src/preload.js
@@ -0,0 +1,376 @@
+/**
+ * Preload Script for PanConverter
+ *
+ * This script creates a secure bridge between the main process and renderer process.
+ * It exposes only specific IPC channels, preventing direct Node.js access in the renderer.
+ *
+ * Security Benefits:
+ * - No direct access to Node.js APIs (fs, path, child_process, etc.)
+ * - All IPC channels are explicitly whitelisted
+ * - Prevents XSS from escalating to full system access
+ *
+ * @version 2.2.0
+ */
+
+const { contextBridge, ipcRenderer } = require('electron');
+
+// Define allowed IPC channels for security
+const ALLOWED_SEND_CHANNELS = [
+ // File operations
+ 'save-file',
+ 'save-current-file',
+ 'set-current-file',
+ 'save-recent-files',
+ 'clear-recent-files',
+ 'renderer-ready',
+
+ // Theme
+ 'get-theme',
+
+ // Print
+ 'do-print',
+
+ // Export
+ 'export-with-options',
+ 'export-spreadsheet',
+
+ // Batch conversion
+ 'batch-convert',
+ 'select-folder',
+
+ // Universal converter
+ 'universal-convert',
+ 'universal-convert-batch',
+
+ // Image converter
+ 'image-convert',
+ 'image-batch-convert',
+ 'image-resize',
+ 'image-compress',
+ 'image-rotate',
+
+ // Audio converter
+ 'audio-convert',
+ 'audio-batch-convert',
+ 'audio-extract',
+ 'audio-trim',
+ 'audio-merge',
+
+ // Video converter
+ 'video-convert',
+ 'video-batch-convert',
+ 'video-compress',
+ 'video-trim',
+ 'video-frames',
+ 'video-gif',
+
+ // Header/Footer
+ 'get-header-footer-settings',
+ 'save-header-footer-settings',
+ 'browse-header-footer-logo',
+ 'save-header-footer-logo',
+ 'clear-header-footer-logo',
+
+ // Page settings
+ 'get-page-settings',
+ 'update-page-settings',
+
+ // Template settings
+ 'set-custom-start-page',
+
+ // PDF operations
+ 'process-pdf-operation',
+ 'get-pdf-page-count',
+ 'select-pdf-folder',
+
+ // ASCII generator (separate window)
+ 'open-ascii-generator',
+
+ // Table generator (separate window)
+ 'open-table-generator',
+
+ // Insert generated content
+ 'insert-generated-content'
+];
+
+const ALLOWED_RECEIVE_CHANNELS = [
+ // File operations
+ 'file-new',
+ 'file-opened',
+ 'file-save',
+ 'get-content-for-save',
+ 'get-content-for-spreadsheet',
+ 'recent-files-cleared',
+
+ // UI toggles
+ 'toggle-preview',
+ 'toggle-find',
+
+ // Theme
+ 'theme-changed',
+ 'theme-data',
+
+ // Edit operations
+ 'undo',
+ 'redo',
+
+ // Font
+ 'adjust-font-size',
+
+ // Print
+ 'print-preview',
+ 'print-preview-styled',
+
+ // Export dialogs
+ 'show-export-dialog',
+ 'show-batch-dialog',
+ 'show-universal-converter-dialog',
+ 'show-table-generator',
+ 'show-pdf-editor-dialog',
+
+ // Converter dialogs
+ 'show-image-converter',
+ 'show-audio-converter',
+ 'show-video-converter',
+
+ // PDF viewer
+ 'open-pdf-viewer',
+
+ // Conversion status
+ 'conversion-status',
+ 'conversion-complete',
+ 'batch-progress',
+ 'image-conversion-complete',
+ 'audio-conversion-complete',
+ 'video-conversion-complete',
+
+ // Folder selection
+ 'folder-selected',
+ 'pdf-folder-selected',
+
+ // Header/Footer
+ 'header-footer-settings-data',
+ 'header-footer-logo-selected',
+ 'header-footer-logo-saved',
+
+ // Page settings
+ 'page-settings-data',
+
+ // PDF operations
+ 'pdf-page-count',
+ 'pdf-operation-complete',
+ 'pdf-operation-error',
+
+ // ASCII Art Generator
+ 'show-ascii-generator-window',
+ 'show-ascii-generator',
+
+ // Table Generator
+ 'show-table-generator-window',
+
+ // Header/Footer dialog
+ 'open-header-footer-dialog',
+ 'header-footer-logo-cleared',
+
+ // PDF operation progress
+ 'pdf-operation-progress',
+
+ // Insert content from generator windows
+ 'insert-content'
+];
+
+/**
+ * Secure API exposed to renderer process
+ * Access via window.electronAPI in renderer
+ */
+contextBridge.exposeInMainWorld('electronAPI', {
+ // ============================================
+ // SEND METHODS (Renderer -> Main)
+ // ============================================
+
+ /**
+ * Send a message to the main process
+ * @param {string} channel - IPC channel name
+ * @param {any} data - Data to send
+ */
+ send: (channel, data) => {
+ if (ALLOWED_SEND_CHANNELS.includes(channel)) {
+ ipcRenderer.send(channel, data);
+ } else {
+ console.warn(`[Preload] Blocked send to unauthorized channel: ${channel}`);
+ }
+ },
+
+ /**
+ * Invoke a main process handler and get a response
+ * @param {string} channel - IPC channel name
+ * @param {any} data - Data to send
+ * @returns {Promise
} Response from main process
+ */
+ invoke: async (channel, data) => {
+ if (ALLOWED_SEND_CHANNELS.includes(channel)) {
+ return await ipcRenderer.invoke(channel, data);
+ } else {
+ console.warn(`[Preload] Blocked invoke to unauthorized channel: ${channel}`);
+ return null;
+ }
+ },
+
+ // ============================================
+ // RECEIVE METHODS (Main -> Renderer)
+ // ============================================
+
+ /**
+ * Register a listener for messages from main process
+ * @param {string} channel - IPC channel name
+ * @param {Function} callback - Function to call with received data
+ * @returns {Function} Cleanup function to remove listener
+ */
+ on: (channel, callback) => {
+ if (ALLOWED_RECEIVE_CHANNELS.includes(channel)) {
+ const subscription = (event, ...args) => callback(...args);
+ ipcRenderer.on(channel, subscription);
+
+ // Return cleanup function
+ return () => {
+ ipcRenderer.removeListener(channel, subscription);
+ };
+ } else {
+ console.warn(`[Preload] Blocked listener for unauthorized channel: ${channel}`);
+ return () => {}; // No-op cleanup
+ }
+ },
+
+ /**
+ * Register a one-time listener for messages from main process
+ * @param {string} channel - IPC channel name
+ * @param {Function} callback - Function to call with received data
+ */
+ once: (channel, callback) => {
+ if (ALLOWED_RECEIVE_CHANNELS.includes(channel)) {
+ ipcRenderer.once(channel, (event, ...args) => callback(...args));
+ } else {
+ console.warn(`[Preload] Blocked once listener for unauthorized channel: ${channel}`);
+ }
+ },
+
+ /**
+ * Remove all listeners for a channel
+ * @param {string} channel - IPC channel name
+ */
+ removeAllListeners: (channel) => {
+ if (ALLOWED_RECEIVE_CHANNELS.includes(channel)) {
+ ipcRenderer.removeAllListeners(channel);
+ }
+ },
+
+ // ============================================
+ // CONVENIENCE METHODS
+ // ============================================
+
+ // File Operations
+ file: {
+ save: (filePath, content) => ipcRenderer.send('save-file', { path: filePath, content }),
+ saveCurrent: (content) => ipcRenderer.send('save-current-file', content),
+ setCurrent: (filePath) => ipcRenderer.send('set-current-file', filePath),
+ saveRecent: (recentFiles) => ipcRenderer.send('save-recent-files', recentFiles),
+ clearRecent: () => ipcRenderer.send('clear-recent-files'),
+ rendererReady: () => ipcRenderer.send('renderer-ready')
+ },
+
+ // Theme Operations
+ theme: {
+ get: () => ipcRenderer.send('get-theme')
+ },
+
+ // Print Operations
+ print: {
+ doPrint: (options) => ipcRenderer.send('do-print', options)
+ },
+
+ // Export Operations
+ export: {
+ withOptions: (format, options) => ipcRenderer.send('export-with-options', { format, options }),
+ spreadsheet: (content, format) => ipcRenderer.send('export-spreadsheet', { content, format })
+ },
+
+ // Batch Conversion
+ batch: {
+ convert: (inputFolder, outputFolder, format, options) => {
+ ipcRenderer.send('batch-convert', { inputFolder, outputFolder, format, options });
+ },
+ selectFolder: (type) => ipcRenderer.send('select-folder', type)
+ },
+
+ // Universal Converter
+ converter: {
+ convert: (tool, fromFormat, toFormat, filePath) => {
+ ipcRenderer.send('universal-convert', { tool, fromFormat, toFormat, filePath });
+ },
+ convertBatch: (tool, fromFormat, toFormat, inputFolder, outputFolder) => {
+ ipcRenderer.send('universal-convert-batch', { tool, fromFormat, toFormat, inputFolder, outputFolder });
+ }
+ },
+
+ // Header/Footer Operations
+ headerFooter: {
+ getSettings: () => ipcRenderer.send('get-header-footer-settings'),
+ saveSettings: (settings) => ipcRenderer.send('save-header-footer-settings', settings),
+ browseLogo: (position) => ipcRenderer.send('browse-header-footer-logo', position),
+ saveLogo: (position, filePath) => ipcRenderer.send('save-header-footer-logo', { position, filePath }),
+ clearLogo: (position) => ipcRenderer.send('clear-header-footer-logo', position)
+ },
+
+ // Page Settings
+ page: {
+ getSettings: () => ipcRenderer.send('get-page-settings'),
+ updateSettings: (settings) => ipcRenderer.send('update-page-settings', settings),
+ setCustomStartPage: (pageNumber) => ipcRenderer.send('set-custom-start-page', pageNumber)
+ },
+
+ // PDF Operations
+ pdf: {
+ processOperation: (data) => ipcRenderer.send('process-pdf-operation', data),
+ getPageCount: (filePath) => ipcRenderer.send('get-pdf-page-count', filePath),
+ selectFolder: (inputId) => ipcRenderer.send('select-pdf-folder', inputId)
+ },
+
+ // Image Converter Operations
+ image: {
+ convert: (data) => ipcRenderer.send('image-convert', data),
+ batchConvert: (data) => ipcRenderer.send('image-batch-convert', data),
+ resize: (data) => ipcRenderer.send('image-resize', data),
+ compress: (data) => ipcRenderer.send('image-compress', data),
+ rotate: (data) => ipcRenderer.send('image-rotate', data)
+ },
+
+ // Audio Converter Operations
+ audio: {
+ convert: (data) => ipcRenderer.send('audio-convert', data),
+ batchConvert: (data) => ipcRenderer.send('audio-batch-convert', data),
+ extract: (data) => ipcRenderer.send('audio-extract', data),
+ trim: (data) => ipcRenderer.send('audio-trim', data),
+ merge: (data) => ipcRenderer.send('audio-merge', data)
+ },
+
+ // Video Converter Operations
+ video: {
+ convert: (data) => ipcRenderer.send('video-convert', data),
+ batchConvert: (data) => ipcRenderer.send('video-batch-convert', data),
+ compress: (data) => ipcRenderer.send('video-compress', data),
+ trim: (data) => ipcRenderer.send('video-trim', data),
+ extractFrames: (data) => ipcRenderer.send('video-frames', data),
+ toGif: (data) => ipcRenderer.send('video-gif', data)
+ },
+
+ // Generator Windows
+ generators: {
+ openAscii: () => ipcRenderer.send('open-ascii-generator'),
+ openTable: () => ipcRenderer.send('open-table-generator')
+ }
+});
+
+// Log successful preload initialization
+console.log('[Preload] Secure IPC bridge initialized');
+console.log('[Preload] Allowed send channels:', ALLOWED_SEND_CHANNELS.length);
+console.log('[Preload] Allowed receive channels:', ALLOWED_RECEIVE_CHANNELS.length);
diff --git a/src/renderer.js b/src/renderer.js
index d8aca81..a128a64 100644
--- a/src/renderer.js
+++ b/src/renderer.js
@@ -1,3 +1,8 @@
+/**
+ * MarkdownConverter Renderer Process
+ * @version 3.0.0
+ */
+
const { ipcRenderer } = require('electron');
const marked = require('marked');
const DOMPurify = require('dompurify');
@@ -10,7 +15,6 @@ marked.setOptions({
try {
return hljs.highlight(code, { language: lang }).value;
} catch (err) {
- // Fallback to auto highlighting if language-specific highlighting fails
console.warn('Syntax highlighting failed for language:', lang, err.message);
}
}
@@ -308,6 +312,11 @@ class TabManager {
if (!tab || !preview) return;
try {
+ // Check if libraries are available
+ if (!marked || !DOMPurify) {
+ preview.innerHTML = 'Error: Required libraries (marked/DOMPurify) not loaded. Check internet connection.
';
+ return;
+ }
const html = marked.parse(tab.content);
const sanitizedHtml = DOMPurify.sanitize(html);
preview.innerHTML = sanitizedHtml;
@@ -1043,7 +1052,6 @@ class TabManager {
this.updateTabBar();
// Notify main process about current file for exports
- const { ipcRenderer } = require('electron');
ipcRenderer.send('set-current-file', filePath);
console.log('File opened successfully');
@@ -2308,12 +2316,12 @@ ipcRenderer.on('show-table-generator', () => {
});
// Show PDF Editor Dialog
-ipcRenderer.on('show-pdf-editor-dialog', (event, operation) => {
+ipcRenderer.on('show-pdf-editor-dialog', (event, operation, openedFilePath) => {
currentPDFOperation = operation;
- showPDFEditorDialog(operation);
+ showPDFEditorDialog(operation, openedFilePath);
});
-function showPDFEditorDialog(operation) {
+function showPDFEditorDialog(operation, openedFilePath = null) {
const dialog = document.getElementById('pdf-editor-dialog');
const title = document.getElementById('pdf-editor-title');
@@ -2329,43 +2337,82 @@ function showPDFEditorDialog(operation) {
sectionId = 'pdf-merge-section';
titleText = 'Merge PDFs';
mergeFilePaths = [];
+ // If we have an opened file, add it as the first file to merge
+ if (openedFilePath) {
+ mergeFilePaths.push(openedFilePath);
+ }
updateMergeFilesList();
break;
case 'split':
sectionId = 'pdf-split-section';
titleText = 'Split PDF';
+ // Pre-fill input path if we have an opened file
+ if (openedFilePath) {
+ document.getElementById('split-input-path').value = openedFilePath;
+ }
break;
case 'compress':
sectionId = 'pdf-compress-section';
titleText = 'Compress PDF';
+ if (openedFilePath) {
+ document.getElementById('compress-input-path').value = openedFilePath;
+ }
break;
case 'rotate':
sectionId = 'pdf-rotate-section';
titleText = 'Rotate Pages';
+ if (openedFilePath) {
+ const rotateInput = document.getElementById('rotate-input-path');
+ if (rotateInput) rotateInput.value = openedFilePath;
+ }
break;
case 'delete':
sectionId = 'pdf-delete-section';
titleText = 'Delete Pages';
+ if (openedFilePath) {
+ const deleteInput = document.getElementById('delete-input-path');
+ if (deleteInput) deleteInput.value = openedFilePath;
+ }
break;
case 'reorder':
sectionId = 'pdf-reorder-section';
titleText = 'Reorder Pages';
+ if (openedFilePath) {
+ const reorderInput = document.getElementById('reorder-input-path');
+ if (reorderInput) reorderInput.value = openedFilePath;
+ }
break;
case 'watermark':
sectionId = 'pdf-watermark-section';
titleText = 'Add Watermark';
+ if (openedFilePath) {
+ const watermarkInput = document.getElementById('watermark-input-path');
+ if (watermarkInput) watermarkInput.value = openedFilePath;
+ }
break;
case 'encrypt':
sectionId = 'pdf-encrypt-section';
titleText = 'Password Protection';
+ if (openedFilePath) {
+ const encryptInput = document.getElementById('encrypt-input-path');
+ if (encryptInput) encryptInput.value = openedFilePath;
+ }
break;
case 'decrypt':
sectionId = 'pdf-decrypt-section';
titleText = 'Remove Password';
+ if (openedFilePath) {
+ const decryptInput = document.getElementById('decrypt-input-path');
+ if (decryptInput) decryptInput.value = openedFilePath;
+ }
break;
case 'permissions':
sectionId = 'pdf-permissions-section';
titleText = 'Set Permissions';
+ if (openedFilePath) {
+ const permInput = document.getElementById('permissions-input-path');
+ if (permInput) permInput.value = openedFilePath;
+ }
break;
}
@@ -2545,6 +2592,32 @@ document.addEventListener('DOMContentLoaded', () => {
});
}
+ // Overwrite checkbox handlers - toggle Save As section visibility
+ const overwriteCheckboxes = [
+ { checkbox: 'compress-overwrite', section: 'compress-saveas-section' },
+ { checkbox: 'rotate-overwrite', section: 'rotate-saveas-section' },
+ { checkbox: 'delete-overwrite', section: 'delete-saveas-section' },
+ { checkbox: 'reorder-overwrite', section: 'reorder-saveas-section' },
+ { checkbox: 'watermark-overwrite', section: 'watermark-saveas-section' },
+ { checkbox: 'encrypt-overwrite', section: 'encrypt-saveas-section' },
+ { checkbox: 'decrypt-overwrite', section: 'decrypt-saveas-section' },
+ { checkbox: 'permissions-overwrite', section: 'permissions-saveas-section' }
+ ];
+
+ overwriteCheckboxes.forEach(item => {
+ const checkbox = document.getElementById(item.checkbox);
+ const section = document.getElementById(item.section);
+ if (checkbox && section) {
+ checkbox.addEventListener('change', (e) => {
+ if (e.target.checked) {
+ section.classList.add('hidden');
+ } else {
+ section.classList.remove('hidden');
+ }
+ });
+ }
+ });
+
// Load current page order button
const loadCurrentOrder = document.getElementById('load-current-order');
if (loadCurrentOrder) {
@@ -2619,33 +2692,36 @@ function processPDFOperation() {
case 'compress':
operationData.inputPath = document.getElementById('compress-input-path').value.trim();
- operationData.outputPath = document.getElementById('compress-output-path').value.trim();
+ operationData.overwrite = document.getElementById('compress-overwrite').checked;
+ operationData.outputPath = operationData.overwrite ? operationData.inputPath : document.getElementById('compress-output-path').value.trim();
operationData.compressionLevel = document.getElementById('compress-level').value;
operationData.compressImages = document.getElementById('compress-images').checked;
operationData.removeDuplicates = document.getElementById('compress-remove-duplicates').checked;
operationData.optimizeFonts = document.getElementById('compress-optimize-fonts').checked;
if (!operationData.inputPath || !operationData.outputPath) {
- alert('Please select input and output file paths');
+ alert('Please select input file' + (operationData.overwrite ? '' : ' and output file paths'));
return;
}
break;
case 'rotate':
operationData.inputPath = document.getElementById('rotate-input-path').value.trim();
- operationData.outputPath = document.getElementById('rotate-output-path').value.trim();
+ operationData.overwrite = document.getElementById('rotate-overwrite').checked;
+ operationData.outputPath = operationData.overwrite ? operationData.inputPath : document.getElementById('rotate-output-path').value.trim();
operationData.pages = document.getElementById('rotate-pages').value.trim();
operationData.angle = parseInt(document.getElementById('rotate-angle').value);
if (!operationData.inputPath || !operationData.outputPath) {
- alert('Please select input and output file paths');
+ alert('Please select input file' + (operationData.overwrite ? '' : ' and output file'));
return;
}
break;
case 'delete':
operationData.inputPath = document.getElementById('delete-input-path').value.trim();
- operationData.outputPath = document.getElementById('delete-output-path').value.trim();
+ operationData.overwrite = document.getElementById('delete-overwrite').checked;
+ operationData.outputPath = operationData.overwrite ? operationData.inputPath : document.getElementById('delete-output-path').value.trim();
operationData.pages = document.getElementById('delete-pages').value.trim();
if (!operationData.inputPath || !operationData.outputPath || !operationData.pages) {
@@ -2656,7 +2732,8 @@ function processPDFOperation() {
case 'reorder':
operationData.inputPath = document.getElementById('reorder-input-path').value.trim();
- operationData.outputPath = document.getElementById('reorder-output-path').value.trim();
+ operationData.overwrite = document.getElementById('reorder-overwrite').checked;
+ operationData.outputPath = operationData.overwrite ? operationData.inputPath : document.getElementById('reorder-output-path').value.trim();
operationData.newOrder = document.getElementById('reorder-pages').value.trim();
if (!operationData.inputPath || !operationData.outputPath || !operationData.newOrder) {
@@ -2667,7 +2744,8 @@ function processPDFOperation() {
case 'watermark':
operationData.inputPath = document.getElementById('watermark-input-path').value.trim();
- operationData.outputPath = document.getElementById('watermark-output-path').value.trim();
+ operationData.overwrite = document.getElementById('watermark-overwrite').checked;
+ operationData.outputPath = operationData.overwrite ? operationData.inputPath : document.getElementById('watermark-output-path').value.trim();
operationData.text = document.getElementById('watermark-text').value.trim();
operationData.fontSize = parseInt(document.getElementById('watermark-font-size').value);
operationData.opacity = parseInt(document.getElementById('watermark-opacity').value) / 100;
@@ -2687,7 +2765,8 @@ function processPDFOperation() {
case 'encrypt':
operationData.inputPath = document.getElementById('encrypt-input-path').value.trim();
- operationData.outputPath = document.getElementById('encrypt-output-path').value.trim();
+ operationData.overwrite = document.getElementById('encrypt-overwrite').checked;
+ operationData.outputPath = operationData.overwrite ? operationData.inputPath : document.getElementById('encrypt-output-path').value.trim();
operationData.userPassword = document.getElementById('encrypt-user-password').value;
operationData.ownerPassword = document.getElementById('encrypt-owner-password').value;
operationData.encryptionLevel = parseInt(document.getElementById('encrypt-level').value);
@@ -2704,14 +2783,15 @@ function processPDFOperation() {
};
if (!operationData.inputPath || !operationData.outputPath || !operationData.userPassword) {
- alert('Please select files and enter a user password');
+ alert('Please select file and enter a user password');
return;
}
break;
case 'decrypt':
operationData.inputPath = document.getElementById('decrypt-input-path').value.trim();
- operationData.outputPath = document.getElementById('decrypt-output-path').value.trim();
+ operationData.overwrite = document.getElementById('decrypt-overwrite').checked;
+ operationData.outputPath = operationData.overwrite ? operationData.inputPath : document.getElementById('decrypt-output-path').value.trim();
operationData.password = document.getElementById('decrypt-password').value;
if (!operationData.inputPath || !operationData.outputPath || !operationData.password) {
@@ -2722,7 +2802,8 @@ function processPDFOperation() {
case 'permissions':
operationData.inputPath = document.getElementById('permissions-input-path').value.trim();
- operationData.outputPath = document.getElementById('permissions-output-path').value.trim();
+ operationData.overwrite = document.getElementById('permissions-overwrite').checked;
+ operationData.outputPath = operationData.overwrite ? operationData.inputPath : document.getElementById('permissions-output-path').value.trim();
operationData.currentPassword = document.getElementById('permissions-current-password').value;
operationData.ownerPassword = document.getElementById('permissions-owner-password').value;
@@ -4057,3 +4138,377 @@ document.addEventListener('click', (e) => {
}
});
+// ============================================
+// Insert Content from Generator Windows
+// ============================================
+ipcRenderer.on('insert-content', (event, content) => {
+ if (tabManager) {
+ const activeTabId = tabManager.activeTabId;
+ const editor = document.getElementById(`editor-${activeTabId}`);
+ if (editor) {
+ const cursorPos = editor.selectionStart;
+ const textBefore = editor.value.substring(0, cursorPos);
+ const textAfter = editor.value.substring(cursorPos);
+
+ editor.value = textBefore + content + textAfter;
+ editor.focus();
+
+ const newPos = cursorPos + content.length;
+ editor.setSelectionRange(newPos, newPos);
+
+ // Update state and preview
+ tabManager.tabs.get(activeTabId).content = editor.value;
+ tabManager.tabs.get(activeTabId).modified = true;
+ tabManager.updatePreview(activeTabId);
+ tabManager.updateTabTitle(activeTabId);
+ }
+ }
+});
+
+// ============================================
+// PDF VIEWER FUNCTIONALITY
+// ============================================
+
+let pdfDoc = null;
+let pdfCurrentPage = 1;
+let pdfZoomLevel = 1.0;
+let pdfRotation = 0;
+let pdfFilePath = null;
+let isPdfViewerActive = false; // Track if PDF viewer is currently shown
+
+// Initialize PDF.js
+const pdfjsLib = require('pdfjs-dist');
+pdfjsLib.GlobalWorkerOptions.workerSrc = require.resolve('pdfjs-dist/build/pdf.worker.js');
+
+// Open PDF file
+async function openPdfFile(filePath) {
+ // Prevent multiple simultaneous PDF loads
+ if (isPdfViewerActive && pdfFilePath === filePath) {
+ console.log('PDF already open:', filePath);
+ return;
+ }
+
+ // Close any existing PDF first
+ if (pdfDoc) {
+ try {
+ await pdfDoc.destroy();
+ } catch (e) {
+ console.warn('Error destroying previous PDF:', e);
+ }
+ pdfDoc = null;
+ }
+
+ try {
+ // Show loading state
+ document.getElementById('status-text').textContent = 'Loading PDF...';
+
+ const loadingTask = pdfjsLib.getDocument(filePath);
+ pdfDoc = await loadingTask.promise;
+ pdfFilePath = filePath;
+ pdfCurrentPage = 1;
+ pdfZoomLevel = 1.0;
+ pdfRotation = 0;
+ isPdfViewerActive = true;
+
+ // Update UI
+ document.getElementById('pdf-total-pages').textContent = pdfDoc.numPages;
+ document.getElementById('pdf-page-input').value = 1;
+ document.getElementById('pdf-page-input').max = pdfDoc.numPages;
+ document.getElementById('pdf-filename').textContent = require('path').basename(filePath);
+ document.getElementById('pdf-zoom-level').textContent = '100%';
+
+ // Hide markdown toolbar, tabs, show PDF viewer
+ document.querySelector('.toolbar').classList.add('hidden');
+ document.getElementById('tab-bar').classList.add('hidden');
+ document.querySelectorAll('.tab-content').forEach(tc => tc.classList.add('hidden'));
+ document.getElementById('pdf-viewer-container').classList.remove('hidden');
+
+ // Render first page
+ await renderPdfPage(pdfCurrentPage);
+
+ // Update status
+ document.getElementById('status-text').textContent = `PDF: ${require('path').basename(filePath)} (${pdfDoc.numPages} pages)`;
+ } catch (error) {
+ console.error('Error loading PDF:', error);
+ isPdfViewerActive = false;
+ pdfDoc = null;
+ pdfFilePath = null;
+ document.getElementById('status-text').textContent = 'Error loading PDF';
+ alert('Error loading PDF: ' + error.message);
+ }
+}
+
+// Render PDF page
+async function renderPdfPage(pageNum) {
+ if (!pdfDoc) return;
+
+ try {
+ const page = await pdfDoc.getPage(pageNum);
+ const canvas = document.getElementById('pdf-canvas');
+ const ctx = canvas.getContext('2d');
+
+ // Calculate viewport with zoom and rotation
+ const viewport = page.getViewport({ scale: pdfZoomLevel, rotation: pdfRotation });
+
+ canvas.width = viewport.width;
+ canvas.height = viewport.height;
+
+ const renderContext = {
+ canvasContext: ctx,
+ viewport: viewport
+ };
+
+ await page.render(renderContext).promise;
+
+ // Update page input
+ document.getElementById('pdf-page-input').value = pageNum;
+ } catch (error) {
+ console.error('Error rendering PDF page:', error);
+ }
+}
+
+// PDF navigation handlers
+document.getElementById('pdf-prev-page')?.addEventListener('click', async () => {
+ if (pdfCurrentPage > 1) {
+ pdfCurrentPage--;
+ await renderPdfPage(pdfCurrentPage);
+ }
+});
+
+document.getElementById('pdf-next-page')?.addEventListener('click', async () => {
+ if (pdfDoc && pdfCurrentPage < pdfDoc.numPages) {
+ pdfCurrentPage++;
+ await renderPdfPage(pdfCurrentPage);
+ }
+});
+
+document.getElementById('pdf-page-input')?.addEventListener('change', async (e) => {
+ const pageNum = parseInt(e.target.value);
+ if (pdfDoc && pageNum >= 1 && pageNum <= pdfDoc.numPages) {
+ pdfCurrentPage = pageNum;
+ await renderPdfPage(pdfCurrentPage);
+ }
+});
+
+// PDF zoom handlers
+document.getElementById('pdf-zoom-out')?.addEventListener('click', async () => {
+ if (pdfZoomLevel > 0.25) {
+ pdfZoomLevel -= 0.25;
+ document.getElementById('pdf-zoom-level').textContent = Math.round(pdfZoomLevel * 100) + '%';
+ await renderPdfPage(pdfCurrentPage);
+ }
+});
+
+document.getElementById('pdf-zoom-in')?.addEventListener('click', async () => {
+ if (pdfZoomLevel < 4.0) {
+ pdfZoomLevel += 0.25;
+ document.getElementById('pdf-zoom-level').textContent = Math.round(pdfZoomLevel * 100) + '%';
+ await renderPdfPage(pdfCurrentPage);
+ }
+});
+
+document.getElementById('pdf-fit-width')?.addEventListener('click', async () => {
+ if (!pdfDoc) return;
+ const page = await pdfDoc.getPage(pdfCurrentPage);
+ const viewport = page.getViewport({ scale: 1, rotation: pdfRotation });
+ const containerWidth = document.getElementById('pdf-viewer').clientWidth - 40;
+ pdfZoomLevel = containerWidth / viewport.width;
+ document.getElementById('pdf-zoom-level').textContent = Math.round(pdfZoomLevel * 100) + '%';
+ await renderPdfPage(pdfCurrentPage);
+});
+
+document.getElementById('pdf-fit-page')?.addEventListener('click', async () => {
+ if (!pdfDoc) return;
+ const page = await pdfDoc.getPage(pdfCurrentPage);
+ const viewport = page.getViewport({ scale: 1, rotation: pdfRotation });
+ const container = document.getElementById('pdf-viewer');
+ const containerWidth = container.clientWidth - 40;
+ const containerHeight = container.clientHeight - 40;
+ const scaleX = containerWidth / viewport.width;
+ const scaleY = containerHeight / viewport.height;
+ pdfZoomLevel = Math.min(scaleX, scaleY);
+ document.getElementById('pdf-zoom-level').textContent = Math.round(pdfZoomLevel * 100) + '%';
+ await renderPdfPage(pdfCurrentPage);
+});
+
+// PDF rotation handlers
+document.getElementById('pdf-rotate-left')?.addEventListener('click', async () => {
+ pdfRotation = (pdfRotation - 90 + 360) % 360;
+ await renderPdfPage(pdfCurrentPage);
+});
+
+document.getElementById('pdf-rotate-right')?.addEventListener('click', async () => {
+ pdfRotation = (pdfRotation + 90) % 360;
+ await renderPdfPage(pdfCurrentPage);
+});
+
+// Close PDF viewer
+document.getElementById('pdf-close')?.addEventListener('click', () => {
+ closePdfViewer();
+});
+
+async function closePdfViewer() {
+ // Destroy PDF document to free memory
+ if (pdfDoc) {
+ try {
+ await pdfDoc.destroy();
+ } catch (e) {
+ console.warn('Error destroying PDF:', e);
+ }
+ }
+
+ pdfDoc = null;
+ pdfFilePath = null;
+ isPdfViewerActive = false;
+
+ // Hide PDF viewer
+ document.getElementById('pdf-viewer-container').classList.add('hidden');
+
+ // Show markdown tabs, tab bar, and toolbar
+ document.getElementById('tab-bar').classList.remove('hidden');
+ document.querySelectorAll('.tab-content').forEach(tc => tc.classList.remove('hidden'));
+ document.querySelector('.toolbar').classList.remove('hidden');
+
+ // Activate the correct markdown tab
+ if (tabManager) {
+ const activeTab = document.querySelector(`.tab-content[data-tab-id="${tabManager.activeTabId}"]`);
+ if (activeTab) activeTab.classList.add('active');
+ // Refresh the active tab
+ tabManager.updatePreview(tabManager.activeTabId);
+ }
+
+ document.getElementById('status-text').textContent = 'Ready';
+}
+
+// Handle PDF file open from main process
+ipcRenderer.on('open-pdf-file', (event, filePath) => {
+ openPdfFile(filePath);
+});
+
+// ============================================
+// PDF EDITOR TOOLBAR BUTTONS
+// ============================================
+
+// Helper function to trigger PDF editor dialog
+function openPdfEditorDialog(operation) {
+ // Use the opened PDF file if available, otherwise prompt
+ if (pdfFilePath) {
+ ipcRenderer.send('show-pdf-editor-from-toolbar', { operation, filePath: pdfFilePath });
+ } else {
+ ipcRenderer.send('show-pdf-editor-from-toolbar', { operation, filePath: null });
+ }
+}
+
+// PDF Editor toolbar button handlers
+document.getElementById('pdf-tb-merge')?.addEventListener('click', () => {
+ openPdfEditorDialog('merge');
+});
+
+document.getElementById('pdf-tb-split')?.addEventListener('click', () => {
+ openPdfEditorDialog('split');
+});
+
+document.getElementById('pdf-tb-compress')?.addEventListener('click', () => {
+ openPdfEditorDialog('compress');
+});
+
+document.getElementById('pdf-tb-rotate')?.addEventListener('click', () => {
+ openPdfEditorDialog('rotate');
+});
+
+document.getElementById('pdf-tb-delete')?.addEventListener('click', () => {
+ openPdfEditorDialog('delete');
+});
+
+document.getElementById('pdf-tb-reorder')?.addEventListener('click', () => {
+ openPdfEditorDialog('reorder');
+});
+
+document.getElementById('pdf-tb-watermark')?.addEventListener('click', () => {
+ openPdfEditorDialog('watermark');
+});
+
+document.getElementById('pdf-tb-encrypt')?.addEventListener('click', () => {
+ openPdfEditorDialog('encrypt');
+});
+
+document.getElementById('pdf-tb-decrypt')?.addEventListener('click', () => {
+ openPdfEditorDialog('decrypt');
+});
+
+// ============================================
+// DYNAMIC PANE RESIZER
+// ============================================
+
+function initPaneResizer(tabId) {
+ const resizer = document.getElementById(`pane-resizer-${tabId}`);
+ const editorPane = document.getElementById(`editor-pane-${tabId}`);
+ const previewPane = document.getElementById(`preview-pane-${tabId}`);
+ const tabContent = document.getElementById(`tab-content-${tabId}`);
+
+ if (!resizer || !editorPane || !previewPane) return;
+
+ let isResizing = false;
+ let startX = 0;
+ let startEditorWidth = 0;
+
+ resizer.addEventListener('mousedown', (e) => {
+ isResizing = true;
+ startX = e.clientX;
+ startEditorWidth = editorPane.offsetWidth;
+ document.body.style.cursor = 'col-resize';
+ document.body.style.userSelect = 'none';
+ e.preventDefault();
+ });
+
+ document.addEventListener('mousemove', (e) => {
+ if (!isResizing) return;
+
+ const containerWidth = tabContent.offsetWidth;
+ const deltaX = e.clientX - startX;
+ let newEditorWidth = startEditorWidth + deltaX;
+
+ // Minimum widths
+ const minWidth = 150;
+ const maxWidth = containerWidth - minWidth - 6; // 6px for resizer
+
+ newEditorWidth = Math.max(minWidth, Math.min(maxWidth, newEditorWidth));
+
+ const editorPercent = (newEditorWidth / containerWidth) * 100;
+ const previewPercent = 100 - editorPercent - 1; // 1% for resizer
+
+ editorPane.style.flex = `0 0 ${editorPercent}%`;
+ previewPane.style.flex = `0 0 ${previewPercent}%`;
+ });
+
+ document.addEventListener('mouseup', () => {
+ if (isResizing) {
+ isResizing = false;
+ document.body.style.cursor = '';
+ document.body.style.userSelect = '';
+ }
+ });
+}
+
+// Initialize resizer for first tab
+document.addEventListener('DOMContentLoaded', () => {
+ initPaneResizer(1);
+});
+
+// ============================================
+// IMAGE/AUDIO/VIDEO CONVERTER HANDLERS
+// These require external tools (ImageMagick, FFmpeg)
+// ============================================
+
+ipcRenderer.on('show-image-tool', (event, tool) => {
+ alert(`Image ${tool} tool requires ImageMagick to be installed.\n\nPlease install ImageMagick from: https://imagemagick.org/\n\nThis feature will be available in a future update with built-in support.`);
+});
+
+ipcRenderer.on('show-audio-tool', (event, tool) => {
+ alert(`Audio ${tool} tool requires FFmpeg to be installed.\n\nPlease install FFmpeg from: https://ffmpeg.org/\n\nThis feature will be available in a future update with built-in support.`);
+});
+
+ipcRenderer.on('show-video-tool', (event, tool) => {
+ alert(`Video ${tool} tool requires FFmpeg to be installed.\n\nPlease install FFmpeg from: https://ffmpeg.org/\n\nThis feature will be available in a future update with built-in support.`);
+});
+
diff --git a/src/styles-concreteinfo.css b/src/styles-concreteinfo.css
new file mode 100644
index 0000000..aa80553
--- /dev/null
+++ b/src/styles-concreteinfo.css
@@ -0,0 +1,969 @@
+/**
+ * ConcreteInfo Theme for MarkdownConverter
+ * Based on logo palette: #464646, #9a9696, #e5461f, #e3e3e3, #0d0b09
+ * Version: 3.0.0
+ */
+
+/* ============================================
+ CSS Variables - ConcreteInfo Theme
+ ============================================ */
+:root {
+ /* Primary Colors from Logo Palette */
+ --ci-dark-gray: #464646;
+ --ci-medium-gray: #9a9696;
+ --ci-accent: #e5461f;
+ --ci-light-gray: #e3e3e3;
+ --ci-black: #0d0b09;
+
+ /* Extended Palette */
+ --ci-accent-hover: #c93a18;
+ --ci-accent-light: rgba(229, 70, 31, 0.1);
+ --ci-white: #ffffff;
+ --ci-bg: #f5f5f5;
+ --ci-border: #d0d0d0;
+
+ /* Semantic Colors */
+ --ci-success: #28a745;
+ --ci-warning: #ffc107;
+ --ci-danger: #dc3545;
+ --ci-info: #17a2b8;
+
+ /* Typography */
+ --font-sans: 'Inter', 'Segoe UI', system-ui, -apple-system, sans-serif;
+ --font-mono: 'JetBrains Mono', 'Fira Code', 'Consolas', monospace;
+
+ /* Spacing */
+ --spacing-xs: 4px;
+ --spacing-sm: 8px;
+ --spacing-md: 16px;
+ --spacing-lg: 24px;
+ --spacing-xl: 32px;
+
+ /* Border Radius */
+ --radius-sm: 4px;
+ --radius-md: 8px;
+ --radius-lg: 12px;
+ --radius-full: 9999px;
+
+ /* Shadows */
+ --shadow-sm: 0 1px 2px rgba(0, 0, 0, 0.05);
+ --shadow-md: 0 4px 6px rgba(0, 0, 0, 0.1);
+ --shadow-lg: 0 10px 15px rgba(0, 0, 0, 0.1);
+ --shadow-xl: 0 20px 25px rgba(0, 0, 0, 0.15);
+
+ /* Transitions */
+ --transition-fast: 150ms ease;
+ --transition-normal: 250ms ease;
+ --transition-slow: 350ms ease;
+}
+
+/* ============================================
+ ConcreteInfo Theme Application
+ ============================================ */
+body.theme-concreteinfo {
+ background: var(--ci-bg);
+ color: var(--ci-dark-gray);
+}
+
+body.theme-concreteinfo .toolbar {
+ background: linear-gradient(135deg, var(--ci-dark-gray) 0%, var(--ci-black) 100%);
+ border-bottom: 3px solid var(--ci-accent);
+}
+
+body.theme-concreteinfo .toolbar button {
+ color: var(--ci-white);
+ background: transparent;
+ border: 1px solid transparent;
+ transition: all var(--transition-fast);
+}
+
+body.theme-concreteinfo .toolbar button:hover {
+ background: var(--ci-accent);
+ border-color: var(--ci-accent);
+ transform: translateY(-1px);
+}
+
+body.theme-concreteinfo .tab-bar {
+ background: var(--ci-dark-gray);
+ border-bottom: 2px solid var(--ci-accent);
+}
+
+body.theme-concreteinfo .tab {
+ background: var(--ci-medium-gray);
+ color: var(--ci-white);
+ border: none;
+ margin: 2px;
+ border-radius: var(--radius-sm) var(--radius-sm) 0 0;
+ transition: all var(--transition-fast);
+}
+
+body.theme-concreteinfo .tab:hover {
+ background: var(--ci-accent-light);
+}
+
+body.theme-concreteinfo .tab.active {
+ background: var(--ci-accent);
+ color: var(--ci-white);
+}
+
+body.theme-concreteinfo .editor-textarea {
+ background: var(--ci-white);
+ color: var(--ci-black);
+ border: 1px solid var(--ci-border);
+ font-family: var(--font-mono);
+}
+
+body.theme-concreteinfo .preview-content {
+ background: var(--ci-white);
+ color: var(--ci-dark-gray);
+ border-left: 3px solid var(--ci-accent);
+}
+
+body.theme-concreteinfo .status-bar {
+ background: var(--ci-dark-gray);
+ color: var(--ci-light-gray);
+ border-top: 2px solid var(--ci-accent);
+}
+
+/* ============================================
+ Modern Modal Styles
+ ============================================ */
+.modal-overlay {
+ position: fixed;
+ top: 0;
+ left: 0;
+ right: 0;
+ bottom: 0;
+ background: rgba(13, 11, 9, 0.7);
+ backdrop-filter: blur(4px);
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ z-index: 10000;
+ opacity: 0;
+ visibility: hidden;
+ transition: all var(--transition-normal);
+}
+
+.modal-overlay.active {
+ opacity: 1;
+ visibility: visible;
+}
+
+.modal-content {
+ background: var(--ci-white);
+ border-radius: var(--radius-lg);
+ box-shadow: var(--shadow-xl);
+ max-width: 600px;
+ width: 90%;
+ max-height: 85vh;
+ overflow: hidden;
+ transform: scale(0.9) translateY(20px);
+ transition: transform var(--transition-normal);
+}
+
+.modal-overlay.active .modal-content {
+ transform: scale(1) translateY(0);
+}
+
+.modal-header {
+ background: linear-gradient(135deg, var(--ci-dark-gray) 0%, var(--ci-black) 100%);
+ color: var(--ci-white);
+ padding: var(--spacing-md) var(--spacing-lg);
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ border-bottom: 3px solid var(--ci-accent);
+}
+
+.modal-header h3 {
+ margin: 0;
+ font-size: 1.25rem;
+ font-weight: 600;
+ font-family: var(--font-sans);
+}
+
+.modal-close {
+ background: transparent;
+ border: none;
+ color: var(--ci-white);
+ font-size: 1.5rem;
+ cursor: pointer;
+ width: 32px;
+ height: 32px;
+ border-radius: var(--radius-full);
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ transition: all var(--transition-fast);
+}
+
+.modal-close:hover {
+ background: var(--ci-accent);
+ transform: rotate(90deg);
+}
+
+.modal-body {
+ padding: var(--spacing-lg);
+ overflow-y: auto;
+ max-height: 60vh;
+}
+
+.modal-footer {
+ padding: var(--spacing-md) var(--spacing-lg);
+ background: var(--ci-light-gray);
+ display: flex;
+ justify-content: flex-end;
+ gap: var(--spacing-sm);
+ border-top: 1px solid var(--ci-border);
+}
+
+/* ============================================
+ Button Styles
+ ============================================ */
+.btn {
+ display: inline-flex;
+ align-items: center;
+ justify-content: center;
+ gap: var(--spacing-xs);
+ padding: var(--spacing-sm) var(--spacing-md);
+ font-family: var(--font-sans);
+ font-size: 0.875rem;
+ font-weight: 500;
+ border-radius: var(--radius-md);
+ border: none;
+ cursor: pointer;
+ transition: all var(--transition-fast);
+}
+
+.btn-primary {
+ background: var(--ci-accent);
+ color: var(--ci-white);
+}
+
+.btn-primary:hover {
+ background: var(--ci-accent-hover);
+ transform: translateY(-1px);
+ box-shadow: var(--shadow-md);
+}
+
+.btn-secondary {
+ background: var(--ci-medium-gray);
+ color: var(--ci-white);
+}
+
+.btn-secondary:hover {
+ background: var(--ci-dark-gray);
+}
+
+.btn-outline {
+ background: transparent;
+ border: 2px solid var(--ci-accent);
+ color: var(--ci-accent);
+}
+
+.btn-outline:hover {
+ background: var(--ci-accent);
+ color: var(--ci-white);
+}
+
+.btn-ghost {
+ background: transparent;
+ color: var(--ci-dark-gray);
+}
+
+.btn-ghost:hover {
+ background: var(--ci-light-gray);
+}
+
+/* ============================================
+ Form Controls
+ ============================================ */
+.form-group {
+ margin-bottom: var(--spacing-md);
+}
+
+.form-label {
+ display: block;
+ margin-bottom: var(--spacing-xs);
+ font-weight: 500;
+ color: var(--ci-dark-gray);
+ font-size: 0.875rem;
+}
+
+.form-input,
+.form-select {
+ width: 100%;
+ padding: var(--spacing-sm) var(--spacing-md);
+ font-family: var(--font-sans);
+ font-size: 0.875rem;
+ border: 2px solid var(--ci-border);
+ border-radius: var(--radius-md);
+ background: var(--ci-white);
+ color: var(--ci-dark-gray);
+ transition: all var(--transition-fast);
+}
+
+.form-input:focus,
+.form-select:focus {
+ outline: none;
+ border-color: var(--ci-accent);
+ box-shadow: 0 0 0 3px var(--ci-accent-light);
+}
+
+.form-input::placeholder {
+ color: var(--ci-medium-gray);
+}
+
+/* ============================================
+ App Header with Logo
+ ============================================ */
+.app-header {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ padding: 6px 16px;
+ background: #ffffff;
+ border-bottom: 2px solid #e1e4e8;
+ min-height: 36px;
+}
+
+.app-header-left {
+ display: flex;
+ align-items: center;
+ gap: 12px;
+}
+
+.app-header-right {
+ display: flex;
+ align-items: center;
+ gap: 12px;
+}
+
+.app-logo {
+ height: 22px;
+ width: auto;
+}
+
+.app-title {
+ color: #24292e;
+ font-size: 0.95rem;
+ font-weight: 600;
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Helvetica, Arial, sans-serif;
+}
+
+.app-version {
+ color: #6a737d;
+ font-size: 0.7rem;
+ font-family: 'SFMono-Regular', Consolas, monospace;
+}
+
+/* Dark theme header adjustments */
+body.theme-dark .app-header,
+body.theme-dracula .app-header,
+body.theme-onedark .app-header,
+body.theme-tokyonight .app-header,
+body.theme-monokai .app-header,
+body.theme-cobalt2 .app-header,
+body.theme-gruvbox-dark .app-header,
+body.theme-ayu-dark .app-header,
+body.theme-ayu-mirage .app-header,
+body.theme-palenight .app-header,
+body.theme-oceanic-next .app-header,
+body.theme-nord .app-header,
+body.theme-concrete-dark .app-header {
+ background: #1f2937;
+ border-bottom-color: #374151;
+}
+
+body.theme-dark .app-title,
+body.theme-dracula .app-title,
+body.theme-onedark .app-title,
+body.theme-tokyonight .app-title,
+body.theme-monokai .app-title,
+body.theme-cobalt2 .app-title,
+body.theme-gruvbox-dark .app-title,
+body.theme-ayu-dark .app-title,
+body.theme-ayu-mirage .app-title,
+body.theme-palenight .app-title,
+body.theme-oceanic-next .app-title,
+body.theme-nord .app-title,
+body.theme-concrete-dark .app-title {
+ color: #f3f4f6;
+}
+
+body.theme-dark .app-version,
+body.theme-dracula .app-version,
+body.theme-onedark .app-version,
+body.theme-tokyonight .app-version,
+body.theme-monokai .app-version,
+body.theme-cobalt2 .app-version,
+body.theme-gruvbox-dark .app-version,
+body.theme-ayu-dark .app-version,
+body.theme-ayu-mirage .app-version,
+body.theme-palenight .app-version,
+body.theme-oceanic-next .app-version,
+body.theme-nord .app-version,
+body.theme-concrete-dark .app-version {
+ color: #9ca3af;
+}
+
+body.theme-dark .app-logo,
+body.theme-dracula .app-logo,
+body.theme-onedark .app-logo,
+body.theme-tokyonight .app-logo,
+body.theme-monokai .app-logo,
+body.theme-cobalt2 .app-logo,
+body.theme-gruvbox-dark .app-logo,
+body.theme-ayu-dark .app-logo,
+body.theme-ayu-mirage .app-logo,
+body.theme-palenight .app-logo,
+body.theme-oceanic-next .app-logo,
+body.theme-nord .app-logo,
+body.theme-concrete-dark .app-logo {
+ filter: none;
+}
+
+/* ============================================
+ Converter Cards
+ ============================================ */
+.converter-grid {
+ display: grid;
+ grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
+ gap: var(--spacing-md);
+ padding: var(--spacing-md);
+}
+
+.converter-card {
+ background: var(--ci-white);
+ border-radius: var(--radius-lg);
+ padding: var(--spacing-lg);
+ text-align: center;
+ border: 2px solid var(--ci-border);
+ transition: all var(--transition-fast);
+ cursor: pointer;
+}
+
+.converter-card:hover {
+ border-color: var(--ci-accent);
+ transform: translateY(-4px);
+ box-shadow: var(--shadow-lg);
+}
+
+.converter-card-icon {
+ font-size: 2.5rem;
+ margin-bottom: var(--spacing-md);
+}
+
+.converter-card-title {
+ font-weight: 600;
+ color: var(--ci-dark-gray);
+ margin-bottom: var(--spacing-xs);
+}
+
+.converter-card-desc {
+ font-size: 0.875rem;
+ color: var(--ci-medium-gray);
+}
+
+/* ============================================
+ Progress Bar
+ ============================================ */
+.progress-bar {
+ height: 8px;
+ background: var(--ci-light-gray);
+ border-radius: var(--radius-full);
+ overflow: hidden;
+}
+
+.progress-fill {
+ height: 100%;
+ background: linear-gradient(90deg, var(--ci-accent) 0%, var(--ci-accent-hover) 100%);
+ border-radius: var(--radius-full);
+ transition: width var(--transition-normal);
+}
+
+/* ============================================
+ File Drop Zone
+ ============================================ */
+.drop-zone {
+ border: 2px dashed var(--ci-border);
+ border-radius: var(--radius-lg);
+ padding: var(--spacing-xl);
+ text-align: center;
+ transition: all var(--transition-fast);
+ cursor: pointer;
+}
+
+.drop-zone:hover,
+.drop-zone.drag-over {
+ border-color: var(--ci-accent);
+ background: var(--ci-accent-light);
+}
+
+.drop-zone-icon {
+ font-size: 3rem;
+ color: var(--ci-medium-gray);
+ margin-bottom: var(--spacing-md);
+}
+
+.drop-zone-text {
+ color: var(--ci-dark-gray);
+ font-weight: 500;
+}
+
+.drop-zone-hint {
+ color: var(--ci-medium-gray);
+ font-size: 0.875rem;
+ margin-top: var(--spacing-xs);
+}
+
+/* ============================================
+ Syntax Highlighting for Editor
+ ============================================ */
+.editor-syntax .md-heading {
+ color: var(--ci-accent);
+ font-weight: bold;
+}
+
+.editor-syntax .md-bold {
+ color: var(--ci-dark-gray);
+ font-weight: bold;
+}
+
+.editor-syntax .md-italic {
+ color: var(--ci-dark-gray);
+ font-style: italic;
+}
+
+.editor-syntax .md-code {
+ background: var(--ci-light-gray);
+ color: var(--ci-accent);
+ font-family: var(--font-mono);
+ padding: 2px 4px;
+ border-radius: var(--radius-sm);
+}
+
+.editor-syntax .md-link {
+ color: var(--ci-info);
+ text-decoration: underline;
+}
+
+.editor-syntax .md-list {
+ color: var(--ci-accent);
+}
+
+.editor-syntax .md-blockquote {
+ color: var(--ci-medium-gray);
+ border-left: 3px solid var(--ci-accent);
+ padding-left: var(--spacing-md);
+}
+
+/* ============================================
+ ASCII Art Generator Styles
+ ============================================ */
+.ascii-preview {
+ font-family: var(--font-mono);
+ font-size: 12px;
+ line-height: 1.2;
+ background: var(--ci-black);
+ color: #00ff00;
+ padding: var(--spacing-md);
+ border-radius: var(--radius-md);
+ overflow-x: auto;
+ white-space: pre;
+}
+
+.ascii-controls {
+ display: flex;
+ flex-wrap: wrap;
+ gap: var(--spacing-sm);
+ margin-bottom: var(--spacing-md);
+}
+
+/* ============================================
+ Table Generator Styles
+ ============================================ */
+.table-generator-grid {
+ display: grid;
+ gap: 1px;
+ background: var(--ci-border);
+ border: 1px solid var(--ci-border);
+ border-radius: var(--radius-md);
+ overflow: hidden;
+}
+
+.table-generator-cell {
+ background: var(--ci-white);
+ padding: var(--spacing-sm);
+ min-width: 80px;
+ min-height: 32px;
+}
+
+.table-generator-cell input {
+ width: 100%;
+ border: none;
+ background: transparent;
+ text-align: center;
+ font-family: var(--font-mono);
+}
+
+.table-generator-cell.header {
+ background: var(--ci-light-gray);
+ font-weight: 600;
+}
+
+/* ============================================
+ PDF Viewer/Editor Styles
+ ============================================ */
+.pdf-viewer {
+ background: var(--ci-dark-gray);
+ padding: var(--spacing-md);
+ border-radius: var(--radius-md);
+ min-height: 400px;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+}
+
+.pdf-page {
+ background: var(--ci-white);
+ box-shadow: var(--shadow-lg);
+ max-width: 100%;
+}
+
+.pdf-toolbar {
+ display: flex;
+ align-items: center;
+ gap: var(--spacing-sm);
+ padding: var(--spacing-sm);
+ background: var(--ci-light-gray);
+ border-radius: var(--radius-md);
+ margin-bottom: var(--spacing-md);
+}
+
+/* ============================================
+ Dark Mode for ConcreteInfo Theme
+ ============================================ */
+body.theme-concreteinfo-dark {
+ --ci-bg: #1a1a1a;
+ --ci-white: #2a2a2a;
+ --ci-light-gray: #3a3a3a;
+ --ci-border: #4a4a4a;
+}
+
+body.theme-concreteinfo-dark .editor-textarea,
+body.theme-concreteinfo-dark .preview-content {
+ background: #2a2a2a;
+ color: #e0e0e0;
+}
+
+body.theme-concreteinfo-dark .modal-content {
+ background: #2a2a2a;
+ color: #e0e0e0;
+}
+
+body.theme-concreteinfo-dark .modal-body {
+ background: #2a2a2a;
+}
+
+body.theme-concreteinfo-dark .modal-footer {
+ background: #1a1a1a;
+}
+
+/* ============================================
+ Animations
+ ============================================ */
+@keyframes fadeIn {
+ from { opacity: 0; }
+ to { opacity: 1; }
+}
+
+@keyframes slideUp {
+ from { transform: translateY(20px); opacity: 0; }
+ to { transform: translateY(0); opacity: 1; }
+}
+
+@keyframes pulse {
+ 0%, 100% { transform: scale(1); }
+ 50% { transform: scale(1.05); }
+}
+
+.animate-fade-in {
+ animation: fadeIn var(--transition-normal);
+}
+
+.animate-slide-up {
+ animation: slideUp var(--transition-normal);
+}
+
+.animate-pulse {
+ animation: pulse 2s infinite;
+}
+
+/* ============================================
+ Responsive Design
+ ============================================ */
+@media (max-width: 768px) {
+ .modal-content {
+ width: 95%;
+ max-height: 90vh;
+ }
+
+ .converter-grid {
+ grid-template-columns: 1fr;
+ }
+
+ .app-header {
+ flex-wrap: wrap;
+ }
+}
+
+/* ============================================
+ Concrete Dark Theme
+ ============================================ */
+body.theme-concrete-dark {
+ background: #1a1a1a;
+ color: #e3e3e3;
+}
+
+body.theme-concrete-dark .toolbar {
+ background: linear-gradient(135deg, #0d0b09 0%, #1a1a1a 100%);
+ border-bottom: 3px solid #e5461f;
+}
+
+body.theme-concrete-dark .toolbar button {
+ color: #e3e3e3;
+}
+
+body.theme-concrete-dark .toolbar button:hover {
+ background: #e5461f;
+}
+
+body.theme-concrete-dark .tab-bar {
+ background: #0d0b09;
+ border-bottom: 2px solid #e5461f;
+}
+
+body.theme-concrete-dark .tab {
+ background: #2a2a2a;
+ color: #e3e3e3;
+ border-radius: 4px 4px 0 0;
+}
+
+body.theme-concrete-dark .tab.active {
+ background: #464646;
+ border-bottom: 2px solid #e5461f;
+}
+
+body.theme-concrete-dark .editor-textarea,
+body.theme-concrete-dark .editor-pane {
+ background: #1a1a1a;
+ color: #e3e3e3;
+}
+
+body.theme-concrete-dark .preview-content {
+ background: #1a1a1a;
+ color: #e3e3e3;
+}
+
+body.theme-concrete-dark .status-bar {
+ background: #0d0b09;
+ color: #9a9696;
+ border-top: 1px solid #464646;
+}
+
+body.theme-concrete-dark .export-dialog-content,
+body.theme-concrete-dark .batch-dialog-content {
+ background: #2a2a2a;
+ color: #e3e3e3;
+}
+
+body.theme-concrete-dark .export-dialog-header,
+body.theme-concrete-dark .batch-dialog-header {
+ background: #1a1a1a;
+ border-bottom: 2px solid #e5461f;
+}
+
+body.theme-concrete-dark .export-dialog-footer,
+body.theme-concrete-dark .batch-dialog-footer {
+ background: #1a1a1a;
+}
+
+body.theme-concrete-dark input,
+body.theme-concrete-dark select,
+body.theme-concrete-dark textarea {
+ background: #1a1a1a;
+ color: #e3e3e3;
+ border-color: #464646;
+}
+
+body.theme-concrete-dark input:focus,
+body.theme-concrete-dark select:focus,
+body.theme-concrete-dark textarea:focus {
+ border-color: #e5461f;
+}
+
+body.theme-concrete-dark .line-numbers {
+ background: #0d0b09;
+ color: #9a9696;
+}
+
+/* ============================================
+ Concrete Light Theme
+ ============================================ */
+body.theme-concrete-light {
+ background: #f5f5f5;
+ color: #464646;
+}
+
+body.theme-concrete-light .toolbar {
+ background: #ffffff;
+ border-bottom: 3px solid #e5461f;
+ box-shadow: 0 2px 4px rgba(0, 0, 0, 0.05);
+}
+
+body.theme-concrete-light .toolbar button {
+ color: #464646;
+}
+
+body.theme-concrete-light .toolbar button:hover {
+ background: #e5461f;
+ color: #ffffff;
+}
+
+body.theme-concrete-light .tab-bar {
+ background: #ffffff;
+ border-bottom: 2px solid #e3e3e3;
+}
+
+body.theme-concrete-light .tab {
+ background: #f5f5f5;
+ color: #464646;
+ border-radius: 4px 4px 0 0;
+}
+
+body.theme-concrete-light .tab.active {
+ background: #ffffff;
+ border-bottom: 2px solid #e5461f;
+ color: #e5461f;
+}
+
+body.theme-concrete-light .editor-textarea,
+body.theme-concrete-light .editor-pane {
+ background: #ffffff;
+ color: #464646;
+}
+
+body.theme-concrete-light .preview-content {
+ background: #ffffff;
+ color: #464646;
+}
+
+body.theme-concrete-light .status-bar {
+ background: #ffffff;
+ color: #9a9696;
+ border-top: 1px solid #e3e3e3;
+}
+
+body.theme-concrete-light .export-dialog-content,
+body.theme-concrete-light .batch-dialog-content {
+ background: #ffffff;
+ color: #464646;
+}
+
+body.theme-concrete-light .export-dialog-header,
+body.theme-concrete-light .batch-dialog-header {
+ background: #f5f5f5;
+ border-bottom: 2px solid #e5461f;
+}
+
+body.theme-concrete-light .line-numbers {
+ background: #f5f5f5;
+ color: #9a9696;
+}
+
+/* ============================================
+ Concrete Warm Theme
+ ============================================ */
+body.theme-concrete-warm {
+ background: #faf8f5;
+ color: #464646;
+}
+
+body.theme-concrete-warm .toolbar {
+ background: linear-gradient(135deg, #464646 0%, #5a5a5a 100%);
+ border-bottom: 3px solid #e5461f;
+}
+
+body.theme-concrete-warm .toolbar button {
+ color: #faf8f5;
+}
+
+body.theme-concrete-warm .toolbar button:hover {
+ background: #e5461f;
+}
+
+body.theme-concrete-warm .tab-bar {
+ background: #464646;
+ border-bottom: 2px solid #e5461f;
+}
+
+body.theme-concrete-warm .tab {
+ background: #6a6a6a;
+ color: #faf8f5;
+ border-radius: 4px 4px 0 0;
+}
+
+body.theme-concrete-warm .tab.active {
+ background: #faf8f5;
+ color: #e5461f;
+ border-bottom: 2px solid #e5461f;
+}
+
+body.theme-concrete-warm .editor-textarea,
+body.theme-concrete-warm .editor-pane {
+ background: #faf8f5;
+ color: #464646;
+}
+
+body.theme-concrete-warm .preview-content {
+ background: #faf8f5;
+ color: #464646;
+}
+
+body.theme-concrete-warm .status-bar {
+ background: #f0ebe4;
+ color: #9a9696;
+ border-top: 1px solid #e3e3e3;
+}
+
+body.theme-concrete-warm .export-dialog-content,
+body.theme-concrete-warm .batch-dialog-content {
+ background: #faf8f5;
+ color: #464646;
+}
+
+body.theme-concrete-warm .export-dialog-header,
+body.theme-concrete-warm .batch-dialog-header {
+ background: #f0ebe4;
+ border-bottom: 2px solid #e5461f;
+}
+
+body.theme-concrete-warm h1,
+body.theme-concrete-warm h2,
+body.theme-concrete-warm h3,
+body.theme-concrete-warm h4,
+body.theme-concrete-warm h5,
+body.theme-concrete-warm h6 {
+ color: #e5461f;
+}
+
+body.theme-concrete-warm a {
+ color: #e5461f;
+}
+
+body.theme-concrete-warm .line-numbers {
+ background: #f0ebe4;
+ color: #9a9696;
+}
diff --git a/src/styles-modern.css b/src/styles-modern.css
index 8cd6c0d..52445b2 100644
--- a/src/styles-modern.css
+++ b/src/styles-modern.css
@@ -50,9 +50,7 @@ body {
font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
overflow: hidden;
height: 100vh;
- background: linear-gradient(135deg, #667eea 0%, #764ba2 50%, #f093fb 100%);
- background-size: 200% 200%;
- animation: gradientShift 15s ease infinite;
+ /* Background controlled by themes - do not set here */
color: var(--gray-900);
}
@@ -66,8 +64,8 @@ body {
display: flex;
flex-direction: column;
height: 100vh;
- padding: 12px;
- gap: 12px;
+ padding: 4px 8px;
+ gap: 2px;
}
/* Glass Container for main content */
@@ -90,10 +88,11 @@ body {
align-items: center;
background: rgba(255, 255, 255, 0.5);
backdrop-filter: blur(10px);
- border-bottom: 1px solid rgba(255, 255, 255, 0.2);
- padding: 8px 12px;
- min-height: 48px;
- gap: 6px;
+ border-bottom: 1px solid rgba(0, 0, 0, 0.08);
+ padding: 4px 8px;
+ min-height: 36px;
+ gap: 4px;
+ border-radius: 8px;
}
.tab {
@@ -114,17 +113,14 @@ body {
}
.tab:hover {
- background: rgba(255, 255, 255, 0.5);
- transform: translateY(-2px);
- box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
+ background: rgba(0, 0, 0, 0.05);
}
.tab.active {
- background: linear-gradient(135deg, rgba(102, 126, 234, 0.9), rgba(118, 75, 162, 0.9));
- color: white;
- border-color: rgba(255, 255, 255, 0.5);
- box-shadow: 0 6px 20px rgba(102, 126, 234, 0.4);
- transform: translateY(-2px);
+ background: #ffffff;
+ color: #24292e;
+ border-bottom: 2px solid #0366d6;
+ box-shadow: 0 1px 3px rgba(0, 0, 0, 0.08);
}
.tab-title {
@@ -185,38 +181,36 @@ body {
.toolbar {
display: flex;
align-items: center;
- padding: 10px 16px;
+ padding: 4px 8px;
background: rgba(255, 255, 255, 0.4);
backdrop-filter: blur(10px);
- border-bottom: 1px solid rgba(255, 255, 255, 0.2);
- gap: 6px;
+ border-bottom: 1px solid rgba(0, 0, 0, 0.06);
+ gap: 2px;
+ border-radius: 8px;
}
.toolbar button {
display: flex;
align-items: center;
justify-content: center;
- width: 36px;
- height: 36px;
- border: 1px solid rgba(255, 255, 255, 0.3);
- background: rgba(255, 255, 255, 0.3);
- backdrop-filter: blur(8px);
- border-radius: 10px;
+ width: 32px;
+ height: 32px;
+ border: 1px solid transparent;
+ background: transparent;
+ border-radius: 6px;
cursor: pointer;
- transition: all var(--transition-speed) var(--transition-ease);
- color: var(--gray-700);
+ transition: all 0.15s ease;
+ color: #586069;
}
.toolbar button:hover {
- background: var(--primary-gradient);
- border-color: transparent;
- color: white;
- transform: translateY(-2px) scale(1.05);
- box-shadow: 0 6px 16px rgba(102, 126, 234, 0.3);
+ background: rgba(0, 0, 0, 0.06);
+ color: #24292e;
}
.toolbar button:active {
- transform: translateY(0) scale(0.98);
+ background: rgba(0, 0, 0, 0.1);
+ transform: scale(0.95);
}
.toolbar-separator {
@@ -272,14 +266,14 @@ body {
.pane::-webkit-scrollbar-thumb,
.editor-textarea::-webkit-scrollbar-thumb {
- background: var(--primary-gradient);
+ background: rgba(0, 0, 0, 0.25);
border-radius: 10px;
border: 2px solid rgba(255, 255, 255, 0.2);
}
.pane::-webkit-scrollbar-thumb:hover,
.editor-textarea::-webkit-scrollbar-thumb:hover {
- background: linear-gradient(135deg, #8b9aff, #9d6fd8);
+ background: rgba(0, 0, 0, 0.4);
}
.editor-textarea {
@@ -293,13 +287,12 @@ body {
outline: none;
resize: none;
background: transparent;
- color: var(--gray-900);
+ /* Color controlled by theme */
}
.pane:last-child {
padding: 0;
- background: rgba(255, 255, 255, 0.5);
- backdrop-filter: blur(15px);
+ /* Background controlled by theme */
}
.preview-content {
@@ -308,30 +301,23 @@ body {
padding: 24px 32px;
line-height: 1.8;
font-size: 15px;
- color: var(--gray-900);
+ /* Color controlled by theme */
}
-/* Status Bar - Glassmorphism */
+/* Status Bar */
.status-bar {
display: flex;
justify-content: space-between;
align-items: center;
padding: 8px 20px;
- background: rgba(255, 255, 255, 0.5);
- backdrop-filter: blur(10px);
- border-top: 1px solid rgba(255, 255, 255, 0.2);
- border-radius: 0 0 20px 20px;
+ /* Background and color controlled by theme */
font-size: 12px;
- color: var(--gray-600);
font-weight: 500;
}
/* Preview Styles - Modern Typography */
.preview-content h1, .preview-content h2, .preview-content h3 {
- background: var(--primary-gradient);
- -webkit-background-clip: text;
- -webkit-text-fill-color: transparent;
- background-clip: text;
+ /* Color controlled by theme */
font-weight: 700;
margin-top: 28px;
margin-bottom: 16px;
@@ -340,14 +326,13 @@ body {
.preview-content h1 {
font-size: 2.5em;
- border-bottom: 3px solid;
- border-image: var(--primary-gradient) 1;
+ border-bottom: 3px solid #e1e4e8;
padding-bottom: 12px;
}
.preview-content h2 {
font-size: 2em;
- border-bottom: 2px solid rgba(102, 126, 234, 0.3);
+ border-bottom: 2px solid #e1e4e8;
padding-bottom: 10px;
}
@@ -360,8 +345,8 @@ body {
padding: 3px 8px;
margin: 0 2px;
font-size: 90%;
- background: linear-gradient(135deg, rgba(102, 126, 234, 0.1), rgba(118, 75, 162, 0.1));
- border: 1px solid rgba(102, 126, 234, 0.2);
+ background: rgba(175, 184, 193, 0.2);
+ border: 1px solid #e1e4e8;
border-radius: 6px;
font-family: 'JetBrains Mono', 'Fira Code', monospace;
font-weight: 500;
@@ -372,9 +357,8 @@ body {
overflow-x: auto;
font-size: 14px;
line-height: 1.6;
- background: rgba(255, 255, 255, 0.6);
- backdrop-filter: blur(10px);
- border: 1px solid rgba(102, 126, 234, 0.2);
+ background: #f6f8fa;
+ border: 1px solid #e1e4e8;
border-radius: 12px;
margin-bottom: 20px;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.05);
@@ -390,17 +374,15 @@ body {
.preview-content blockquote {
padding: 16px 20px;
color: var(--gray-700);
- border-left: 4px solid;
- border-image: var(--primary-gradient) 1;
+ border-left: 4px solid #e1e4e8;
margin-bottom: 20px;
- background: rgba(255, 255, 255, 0.4);
- backdrop-filter: blur(8px);
+ background: #f6f8fa;
border-radius: 0 8px 8px 0;
font-style: italic;
}
.preview-content a {
- color: #667eea;
+ color: #0366d6;
text-decoration: none;
font-weight: 600;
position: relative;
@@ -414,12 +396,12 @@ body {
bottom: -2px;
width: 0;
height: 2px;
- background: var(--primary-gradient);
+ background: #0366d6;
transition: width var(--transition-speed) var(--transition-ease);
}
.preview-content a:hover {
- color: #764ba2;
+ color: #0366d6;
}
.preview-content a:hover::after {
@@ -439,7 +421,7 @@ body {
.preview-content table th,
.preview-content table td {
padding: 12px 16px;
- border: 1px solid rgba(102, 126, 234, 0.2);
+ border: 1px solid #e1e4e8;
}
.preview-content table th {
@@ -515,8 +497,8 @@ body {
align-items: center;
justify-content: space-between;
padding: 20px 24px;
- border-bottom: 1px solid rgba(102, 126, 234, 0.2);
- background: var(--primary-gradient);
+ border-bottom: 1px solid #e1e4e8;
+ background: #24292e;
border-radius: 20px 20px 0 0;
}
@@ -553,7 +535,7 @@ body {
.batch-section input {
width: 100%;
padding: 10px 14px;
- border: 2px solid rgba(102, 126, 234, 0.2);
+ border: 2px solid #e1e4e8;
border-radius: 10px;
font-size: 14px;
background: rgba(255, 255, 255, 0.8);
@@ -566,8 +548,8 @@ body {
.batch-section select:focus,
.batch-section input:focus {
outline: none;
- border-color: #667eea;
- box-shadow: 0 0 0 4px rgba(102, 126, 234, 0.1);
+ border-color: #0366d6;
+ box-shadow: 0 0 0 4px rgba(3, 102, 214, 0.1);
background: white;
}
@@ -578,9 +560,8 @@ body {
justify-content: flex-end;
gap: 12px;
padding: 20px 24px;
- border-top: 1px solid rgba(102, 126, 234, 0.2);
- background: rgba(255, 255, 255, 0.5);
- backdrop-filter: blur(10px);
+ border-top: 1px solid #e1e4e8;
+ background: #f6f8fa;
border-radius: 0 0 20px 20px;
}
@@ -600,30 +581,31 @@ body {
.export-dialog-footer button.primary,
.batch-dialog-footer button.primary {
- background: var(--primary-gradient);
+ background: #2ea44f;
color: white;
- box-shadow: 0 4px 12px rgba(102, 126, 234, 0.4);
+ box-shadow: 0 4px 12px rgba(46, 164, 79, 0.3);
}
.export-dialog-footer button.primary:hover,
.batch-dialog-footer button.primary:hover {
transform: translateY(-2px);
- box-shadow: 0 6px 20px rgba(102, 126, 234, 0.5);
+ background: #22863a;
+ box-shadow: 0 6px 20px rgba(34, 134, 58, 0.4);
}
.export-dialog-footer button:not(.primary),
.batch-dialog-footer button:not(.primary),
.folder-input-group button {
- background: rgba(255, 255, 255, 0.8);
+ background: #fafbfc;
color: var(--gray-700);
- border: 2px solid rgba(102, 126, 234, 0.2);
+ border: 1px solid #e1e4e8;
}
.export-dialog-footer button:not(.primary):hover,
.batch-dialog-footer button:not(.primary):hover,
.folder-input-group button:hover {
- background: white;
- border-color: #667eea;
+ background: #f3f4f6;
+ border-color: #d0d7de;
transform: translateY(-2px);
}
@@ -662,7 +644,7 @@ body {
.progress-bar {
width: 100%;
height: 8px;
- background: rgba(102, 126, 234, 0.1);
+ background: #e1e4e8;
border-radius: 10px;
overflow: hidden;
margin-bottom: 12px;
@@ -670,7 +652,7 @@ body {
.progress-fill {
height: 100%;
- background: var(--primary-gradient);
+ background: #2ea44f;
transition: width 0.4s var(--transition-ease);
width: 0%;
position: relative;
@@ -706,14 +688,14 @@ body {
position: fixed;
top: 24px;
right: 24px;
- background: var(--primary-gradient);
+ background: #2ea44f;
color: white;
padding: 10px 20px;
border-radius: 12px;
font-size: 13px;
font-weight: 600;
z-index: 10000;
- box-shadow: 0 4px 20px rgba(102, 126, 234, 0.4);
+ box-shadow: 0 4px 20px rgba(46, 164, 79, 0.3);
animation: slideInRight 0.3s ease-out;
display: flex;
align-items: center;
@@ -726,7 +708,7 @@ body {
width: 18px;
height: 18px;
background: white;
- color: #667eea;
+ color: #2ea44f;
border-radius: 50%;
font-size: 12px;
display: flex;
@@ -751,7 +733,7 @@ body {
padding: 12px 8px;
background: rgba(255, 255, 255, 0.3);
backdrop-filter: blur(8px);
- border-right: 1px solid rgba(102, 126, 234, 0.2);
+ border-right: 1px solid #e1e4e8;
font-family: 'JetBrains Mono', monospace;
font-size: 13px;
line-height: 1.7;
@@ -823,3 +805,1309 @@ body {
.pane.full-width {
border-right: none;
}
+
+/* ========================================
+ Dynamic Pane Resizer
+ Draggable splitter between editor/preview
+ ======================================== */
+
+.pane-resizer {
+ width: 6px;
+ background: linear-gradient(to right, #e1e4e8, #d0d7de, #e1e4e8);
+ cursor: col-resize;
+ position: relative;
+ flex-shrink: 0;
+ transition: background 0.2s ease;
+ border-radius: 3px;
+}
+
+.pane-resizer:hover {
+ background: linear-gradient(to right, #0366d6, #0366d6, #0366d6);
+}
+
+.pane-resizer:active {
+ background: #0366d6;
+}
+
+.pane-resizer::before {
+ content: '';
+ position: absolute;
+ top: 50%;
+ left: 50%;
+ transform: translate(-50%, -50%);
+ width: 2px;
+ height: 30px;
+ background: rgba(0, 0, 0, 0.2);
+ border-radius: 1px;
+}
+
+.pane-resizer:hover::before {
+ background: rgba(255, 255, 255, 0.8);
+}
+
+/* Dark theme resizer */
+body.theme-dark .pane-resizer,
+body.theme-one-dark .pane-resizer,
+body.theme-dracula .pane-resizer,
+body.theme-nord .pane-resizer,
+body.theme-tokyo-night .pane-resizer,
+body.theme-palenight .pane-resizer,
+body.theme-ayu-dark .pane-resizer,
+body.theme-ayu-mirage .pane-resizer,
+body.theme-oceanic-next .pane-resizer,
+body.theme-gruvbox-dark .pane-resizer,
+body.theme-cobalt2 .pane-resizer {
+ background: linear-gradient(to right, #333, #444, #333);
+}
+
+body.theme-dark .pane-resizer:hover,
+body.theme-one-dark .pane-resizer:hover,
+body.theme-dracula .pane-resizer:hover {
+ background: #61afef;
+}
+
+/* ========================================
+ PDF Viewer Styles
+ Full-featured PDF reader
+ ======================================== */
+
+.pdf-viewer-container {
+ display: flex;
+ flex-direction: column;
+ width: 100%;
+ height: 100%;
+ background: #525659;
+}
+
+.pdf-viewer-container.hidden {
+ display: none;
+}
+
+.pdf-viewer-toolbar {
+ display: flex;
+ align-items: center;
+ gap: 8px;
+ padding: 8px 12px;
+ background: #323639;
+ border-bottom: 1px solid #1a1a1a;
+ flex-shrink: 0;
+}
+
+.pdf-viewer-toolbar button {
+ background: #474a4d;
+ border: 1px solid #5a5d60;
+ border-radius: 4px;
+ color: #fff;
+ padding: 6px 10px;
+ cursor: pointer;
+ font-size: 14px;
+ transition: all 0.15s ease;
+}
+
+.pdf-viewer-toolbar button:hover {
+ background: #5a5d60;
+}
+
+.pdf-viewer-toolbar button:active {
+ background: #0366d6;
+}
+
+.pdf-toolbar-separator {
+ width: 1px;
+ height: 24px;
+ background: #5a5d60;
+ margin: 0 4px;
+}
+
+#pdf-page-info {
+ color: #fff;
+ font-size: 13px;
+ display: flex;
+ align-items: center;
+ gap: 4px;
+}
+
+#pdf-page-input {
+ width: 50px;
+ padding: 4px 6px;
+ border: 1px solid #5a5d60;
+ border-radius: 4px;
+ background: #474a4d;
+ color: #fff;
+ text-align: center;
+ font-size: 13px;
+}
+
+#pdf-zoom-level {
+ color: #fff;
+ font-size: 13px;
+ min-width: 50px;
+ text-align: center;
+}
+
+.pdf-filename {
+ color: #aaa;
+ font-size: 12px;
+ flex: 1;
+ text-align: right;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+ margin-right: 8px;
+}
+
+#pdf-close {
+ background: #c0392b !important;
+ border-color: #e74c3c !important;
+}
+
+#pdf-close:hover {
+ background: #e74c3c !important;
+}
+
+/* PDF Editor Toolbar Buttons */
+.pdf-editor-btn {
+ display: flex;
+ align-items: center;
+ gap: 4px;
+ background: linear-gradient(135deg, #3498db, #2980b9) !important;
+ border: 1px solid #2980b9 !important;
+ color: #fff !important;
+ padding: 5px 8px !important;
+ font-size: 11px !important;
+ font-weight: 500;
+ border-radius: 4px;
+ cursor: pointer;
+ transition: all 0.2s ease;
+}
+
+.pdf-editor-btn:hover {
+ background: linear-gradient(135deg, #2980b9, #1a5276) !important;
+ transform: translateY(-1px);
+}
+
+.pdf-editor-btn:active {
+ transform: translateY(0);
+}
+
+.pdf-editor-btn svg {
+ flex-shrink: 0;
+}
+
+.pdf-editor-btn span {
+ white-space: nowrap;
+}
+
+/* PDF toolbar wrapping for smaller screens */
+.pdf-toolbar-main {
+ flex-wrap: wrap;
+ min-height: auto;
+ padding: 6px 10px !important;
+}
+
+/* Light theme PDF toolbar - refined styling */
+body.theme-light .pdf-viewer-toolbar,
+body.theme-atomonelight .pdf-viewer-toolbar,
+body.theme-github .pdf-viewer-toolbar,
+body.theme-solarized .pdf-viewer-toolbar,
+body.theme-gruvbox-light .pdf-viewer-toolbar,
+body.theme-ayu-light .pdf-viewer-toolbar,
+body.theme-sepia .pdf-viewer-toolbar,
+body.theme-paper .pdf-viewer-toolbar,
+body.theme-rosepine-dawn .pdf-viewer-toolbar {
+ background: linear-gradient(180deg, #fafafa 0%, #f0f0f0 100%) !important;
+ border-bottom: 1px solid #d0d0d0 !important;
+ box-shadow: 0 1px 3px rgba(0,0,0,0.08);
+}
+
+body.theme-light .pdf-viewer-toolbar button,
+body.theme-atomonelight .pdf-viewer-toolbar button,
+body.theme-github .pdf-viewer-toolbar button,
+body.theme-solarized .pdf-viewer-toolbar button,
+body.theme-gruvbox-light .pdf-viewer-toolbar button,
+body.theme-ayu-light .pdf-viewer-toolbar button,
+body.theme-sepia .pdf-viewer-toolbar button,
+body.theme-paper .pdf-viewer-toolbar button,
+body.theme-rosepine-dawn .pdf-viewer-toolbar button {
+ background: #fff !important;
+ border: 1px solid #ccc !important;
+ color: #333 !important;
+ box-shadow: 0 1px 2px rgba(0,0,0,0.05);
+}
+
+body.theme-light .pdf-viewer-toolbar button:hover,
+body.theme-atomonelight .pdf-viewer-toolbar button:hover,
+body.theme-github .pdf-viewer-toolbar button:hover,
+body.theme-solarized .pdf-viewer-toolbar button:hover,
+body.theme-gruvbox-light .pdf-viewer-toolbar button:hover,
+body.theme-ayu-light .pdf-viewer-toolbar button:hover,
+body.theme-sepia .pdf-viewer-toolbar button:hover,
+body.theme-paper .pdf-viewer-toolbar button:hover,
+body.theme-rosepine-dawn .pdf-viewer-toolbar button:hover {
+ background: #e8e8e8 !important;
+ border-color: #bbb !important;
+}
+
+/* Light theme PDF editor buttons */
+body.theme-light .pdf-editor-btn,
+body.theme-atomonelight .pdf-editor-btn,
+body.theme-github .pdf-editor-btn,
+body.theme-solarized .pdf-editor-btn,
+body.theme-gruvbox-light .pdf-editor-btn,
+body.theme-ayu-light .pdf-editor-btn,
+body.theme-sepia .pdf-editor-btn,
+body.theme-paper .pdf-editor-btn,
+body.theme-rosepine-dawn .pdf-editor-btn {
+ background: linear-gradient(135deg, #4a90d9, #357abd) !important;
+ border: 1px solid #357abd !important;
+}
+
+body.theme-light .pdf-editor-btn:hover,
+body.theme-atomonelight .pdf-editor-btn:hover,
+body.theme-github .pdf-editor-btn:hover,
+body.theme-solarized .pdf-editor-btn:hover,
+body.theme-gruvbox-light .pdf-editor-btn:hover,
+body.theme-ayu-light .pdf-editor-btn:hover,
+body.theme-sepia .pdf-editor-btn:hover,
+body.theme-paper .pdf-editor-btn:hover,
+body.theme-rosepine-dawn .pdf-editor-btn:hover {
+ background: linear-gradient(135deg, #357abd, #2868a0) !important;
+}
+
+/* Light theme page info */
+body.theme-light #pdf-page-info,
+body.theme-atomonelight #pdf-page-info,
+body.theme-github #pdf-page-info,
+body.theme-solarized #pdf-page-info,
+body.theme-gruvbox-light #pdf-page-info,
+body.theme-ayu-light #pdf-page-info,
+body.theme-sepia #pdf-page-info,
+body.theme-paper #pdf-page-info,
+body.theme-rosepine-dawn #pdf-page-info,
+body.theme-light #pdf-zoom-level,
+body.theme-atomonelight #pdf-zoom-level,
+body.theme-github #pdf-zoom-level,
+body.theme-solarized #pdf-zoom-level,
+body.theme-gruvbox-light #pdf-zoom-level,
+body.theme-ayu-light #pdf-zoom-level,
+body.theme-sepia #pdf-zoom-level,
+body.theme-paper #pdf-zoom-level,
+body.theme-rosepine-dawn #pdf-zoom-level {
+ color: #444 !important;
+ font-weight: 500;
+}
+
+body.theme-light #pdf-page-input,
+body.theme-atomonelight #pdf-page-input,
+body.theme-github #pdf-page-input,
+body.theme-solarized #pdf-page-input,
+body.theme-gruvbox-light #pdf-page-input,
+body.theme-ayu-light #pdf-page-input,
+body.theme-sepia #pdf-page-input,
+body.theme-paper #pdf-page-input,
+body.theme-rosepine-dawn #pdf-page-input {
+ background: #fff !important;
+ border: 1px solid #ccc !important;
+ color: #333 !important;
+}
+
+body.theme-light .pdf-toolbar-separator,
+body.theme-atomonelight .pdf-toolbar-separator,
+body.theme-github .pdf-toolbar-separator,
+body.theme-solarized .pdf-toolbar-separator,
+body.theme-gruvbox-light .pdf-toolbar-separator,
+body.theme-ayu-light .pdf-toolbar-separator,
+body.theme-sepia .pdf-toolbar-separator,
+body.theme-paper .pdf-toolbar-separator,
+body.theme-rosepine-dawn .pdf-toolbar-separator {
+ background: #d0d0d0 !important;
+}
+
+body.theme-light .pdf-filename,
+body.theme-atomonelight .pdf-filename,
+body.theme-github .pdf-filename,
+body.theme-solarized .pdf-filename,
+body.theme-gruvbox-light .pdf-filename,
+body.theme-ayu-light .pdf-filename,
+body.theme-sepia .pdf-filename,
+body.theme-paper .pdf-filename,
+body.theme-rosepine-dawn .pdf-filename {
+ color: #555 !important;
+}
+
+/* Sepia theme specific */
+body.theme-sepia .pdf-viewer-toolbar {
+ background: linear-gradient(180deg, #f4f0e8 0%, #e8e4dc 100%) !important;
+ border-bottom-color: #cdc8c0 !important;
+}
+
+body.theme-sepia .pdf-viewer {
+ background: #d8d4cc !important;
+}
+
+/* Rose Pine Dawn specific */
+body.theme-rosepine-dawn .pdf-viewer-toolbar {
+ background: linear-gradient(180deg, #faf4ed 0%, #f2e9e1 100%) !important;
+ border-bottom-color: #ddd8d1 !important;
+}
+
+body.theme-rosepine-dawn .pdf-viewer {
+ background: #e6e0d8 !important;
+}
+
+/* Dark theme PDF toolbar refinement */
+body.theme-dark .pdf-viewer-toolbar,
+body.theme-onedark .pdf-viewer-toolbar,
+body.theme-one-dark .pdf-viewer-toolbar,
+body.theme-dracula .pdf-viewer-toolbar,
+body.theme-nord .pdf-viewer-toolbar,
+body.theme-tokyo-night .pdf-viewer-toolbar,
+body.theme-tokyonight .pdf-viewer-toolbar,
+body.theme-palenight .pdf-viewer-toolbar,
+body.theme-monokai .pdf-viewer-toolbar,
+body.theme-material .pdf-viewer-toolbar,
+body.theme-gruvbox-dark .pdf-viewer-toolbar,
+body.theme-ayu-dark .pdf-viewer-toolbar,
+body.theme-ayu-mirage .pdf-viewer-toolbar,
+body.theme-oceanic-next .pdf-viewer-toolbar,
+body.theme-cobalt2 .pdf-viewer-toolbar,
+body.theme-concrete-dark .pdf-viewer-toolbar,
+body.theme-concrete-warm .pdf-viewer-toolbar {
+ background: linear-gradient(180deg, #2d2d2d 0%, #252525 100%) !important;
+ border-bottom: 1px solid #1a1a1a !important;
+}
+
+body.theme-dark .pdf-viewer-toolbar button,
+body.theme-onedark .pdf-viewer-toolbar button,
+body.theme-one-dark .pdf-viewer-toolbar button,
+body.theme-dracula .pdf-viewer-toolbar button,
+body.theme-nord .pdf-viewer-toolbar button,
+body.theme-tokyo-night .pdf-viewer-toolbar button,
+body.theme-tokyonight .pdf-viewer-toolbar button,
+body.theme-palenight .pdf-viewer-toolbar button,
+body.theme-monokai .pdf-viewer-toolbar button,
+body.theme-material .pdf-viewer-toolbar button,
+body.theme-gruvbox-dark .pdf-viewer-toolbar button,
+body.theme-ayu-dark .pdf-viewer-toolbar button,
+body.theme-ayu-mirage .pdf-viewer-toolbar button,
+body.theme-oceanic-next .pdf-viewer-toolbar button,
+body.theme-cobalt2 .pdf-viewer-toolbar button,
+body.theme-concrete-dark .pdf-viewer-toolbar button,
+body.theme-concrete-warm .pdf-viewer-toolbar button {
+ background: #3d3d3d !important;
+ border: 1px solid #4d4d4d !important;
+ color: #e0e0e0 !important;
+}
+
+body.theme-dark .pdf-viewer-toolbar button:hover,
+body.theme-onedark .pdf-viewer-toolbar button:hover,
+body.theme-one-dark .pdf-viewer-toolbar button:hover,
+body.theme-dracula .pdf-viewer-toolbar button:hover,
+body.theme-nord .pdf-viewer-toolbar button:hover,
+body.theme-tokyo-night .pdf-viewer-toolbar button:hover,
+body.theme-tokyonight .pdf-viewer-toolbar button:hover,
+body.theme-palenight .pdf-viewer-toolbar button:hover,
+body.theme-monokai .pdf-viewer-toolbar button:hover,
+body.theme-material .pdf-viewer-toolbar button:hover,
+body.theme-gruvbox-dark .pdf-viewer-toolbar button:hover,
+body.theme-ayu-dark .pdf-viewer-toolbar button:hover,
+body.theme-ayu-mirage .pdf-viewer-toolbar button:hover,
+body.theme-oceanic-next .pdf-viewer-toolbar button:hover,
+body.theme-cobalt2 .pdf-viewer-toolbar button:hover,
+body.theme-concrete-dark .pdf-viewer-toolbar button:hover,
+body.theme-concrete-warm .pdf-viewer-toolbar button:hover {
+ background: #4d4d4d !important;
+ border-color: #5d5d5d !important;
+}
+
+body.theme-dark .pdf-viewer,
+body.theme-onedark .pdf-viewer,
+body.theme-one-dark .pdf-viewer,
+body.theme-dracula .pdf-viewer,
+body.theme-nord .pdf-viewer,
+body.theme-tokyo-night .pdf-viewer,
+body.theme-tokyonight .pdf-viewer,
+body.theme-palenight .pdf-viewer,
+body.theme-monokai .pdf-viewer,
+body.theme-material .pdf-viewer,
+body.theme-gruvbox-dark .pdf-viewer,
+body.theme-ayu-dark .pdf-viewer,
+body.theme-ayu-mirage .pdf-viewer,
+body.theme-oceanic-next .pdf-viewer,
+body.theme-cobalt2 .pdf-viewer,
+body.theme-concrete-dark .pdf-viewer,
+body.theme-concrete-warm .pdf-viewer {
+ background: #1a1a1a !important;
+}
+
+body.theme-dark .pdf-viewer-container,
+body.theme-onedark .pdf-viewer-container,
+body.theme-one-dark .pdf-viewer-container,
+body.theme-dracula .pdf-viewer-container,
+body.theme-nord .pdf-viewer-container,
+body.theme-tokyo-night .pdf-viewer-container,
+body.theme-tokyonight .pdf-viewer-container,
+body.theme-palenight .pdf-viewer-container,
+body.theme-monokai .pdf-viewer-container,
+body.theme-material .pdf-viewer-container,
+body.theme-gruvbox-dark .pdf-viewer-container,
+body.theme-ayu-dark .pdf-viewer-container,
+body.theme-ayu-mirage .pdf-viewer-container,
+body.theme-oceanic-next .pdf-viewer-container,
+body.theme-cobalt2 .pdf-viewer-container,
+body.theme-concrete-dark .pdf-viewer-container,
+body.theme-concrete-warm .pdf-viewer-container {
+ background: #1a1a1a !important;
+}
+
+body.theme-dark #pdf-page-input,
+body.theme-onedark #pdf-page-input,
+body.theme-one-dark #pdf-page-input,
+body.theme-dracula #pdf-page-input,
+body.theme-nord #pdf-page-input,
+body.theme-tokyo-night #pdf-page-input,
+body.theme-tokyonight #pdf-page-input,
+body.theme-palenight #pdf-page-input,
+body.theme-monokai #pdf-page-input,
+body.theme-material #pdf-page-input,
+body.theme-gruvbox-dark #pdf-page-input,
+body.theme-ayu-dark #pdf-page-input,
+body.theme-ayu-mirage #pdf-page-input,
+body.theme-oceanic-next #pdf-page-input,
+body.theme-cobalt2 #pdf-page-input,
+body.theme-concrete-dark #pdf-page-input,
+body.theme-concrete-warm #pdf-page-input {
+ background: #3d3d3d !important;
+ border: 1px solid #4d4d4d !important;
+ color: #e0e0e0 !important;
+}
+
+body.theme-dark .pdf-toolbar-separator,
+body.theme-onedark .pdf-toolbar-separator,
+body.theme-one-dark .pdf-toolbar-separator,
+body.theme-dracula .pdf-toolbar-separator,
+body.theme-nord .pdf-toolbar-separator,
+body.theme-tokyo-night .pdf-toolbar-separator,
+body.theme-tokyonight .pdf-toolbar-separator,
+body.theme-palenight .pdf-toolbar-separator,
+body.theme-monokai .pdf-toolbar-separator,
+body.theme-material .pdf-toolbar-separator,
+body.theme-gruvbox-dark .pdf-toolbar-separator,
+body.theme-ayu-dark .pdf-toolbar-separator,
+body.theme-ayu-mirage .pdf-toolbar-separator,
+body.theme-oceanic-next .pdf-toolbar-separator,
+body.theme-cobalt2 .pdf-toolbar-separator,
+body.theme-concrete-dark .pdf-toolbar-separator,
+body.theme-concrete-warm .pdf-toolbar-separator {
+ background: #3d3d3d !important;
+}
+
+.pdf-viewer {
+ flex: 1;
+ overflow: auto;
+ display: flex;
+ justify-content: center;
+ align-items: flex-start;
+ padding: 20px;
+ background: #525659;
+}
+
+#pdf-canvas {
+ box-shadow: 0 4px 20px rgba(0, 0, 0, 0.4);
+ background: white;
+}
+
+/* Light theme PDF viewer */
+body.theme-light .pdf-viewer-container,
+body.theme-atomonelight .pdf-viewer-container,
+body.theme-github .pdf-viewer-container,
+body.theme-solarized .pdf-viewer-container,
+body.theme-gruvbox-light .pdf-viewer-container,
+body.theme-ayu-light .pdf-viewer-container,
+body.theme-sepia .pdf-viewer-container,
+body.theme-paper .pdf-viewer-container,
+body.theme-rosepine-dawn .pdf-viewer-container {
+ background: #e8e8e8;
+}
+
+body.theme-light .pdf-viewer-toolbar,
+body.theme-atomonelight .pdf-viewer-toolbar,
+body.theme-github .pdf-viewer-toolbar,
+body.theme-solarized .pdf-viewer-toolbar,
+body.theme-gruvbox-light .pdf-viewer-toolbar,
+body.theme-ayu-light .pdf-viewer-toolbar,
+body.theme-sepia .pdf-viewer-toolbar,
+body.theme-paper .pdf-viewer-toolbar,
+body.theme-rosepine-dawn .pdf-viewer-toolbar {
+ background: #f5f5f5;
+ border-bottom-color: #ddd;
+}
+
+body.theme-light .pdf-viewer-toolbar button,
+body.theme-atomonelight .pdf-viewer-toolbar button,
+body.theme-github .pdf-viewer-toolbar button {
+ background: #fff;
+ border-color: #ddd;
+ color: #333;
+}
+
+body.theme-light .pdf-viewer-toolbar button:hover,
+body.theme-atomonelight .pdf-viewer-toolbar button:hover,
+body.theme-github .pdf-viewer-toolbar button:hover {
+ background: #e8e8e8;
+}
+
+body.theme-light #pdf-page-info,
+body.theme-atomonelight #pdf-page-info,
+body.theme-github #pdf-page-info,
+body.theme-light #pdf-zoom-level,
+body.theme-atomonelight #pdf-zoom-level,
+body.theme-github #pdf-zoom-level {
+ color: #333;
+}
+
+body.theme-light #pdf-page-input,
+body.theme-atomonelight #pdf-page-input,
+body.theme-github #pdf-page-input {
+ background: #fff;
+ border-color: #ddd;
+ color: #333;
+}
+
+body.theme-light .pdf-filename,
+body.theme-atomonelight .pdf-filename,
+body.theme-github .pdf-filename {
+ color: #666;
+}
+
+body.theme-light .pdf-viewer,
+body.theme-atomonelight .pdf-viewer,
+body.theme-github .pdf-viewer,
+body.theme-solarized .pdf-viewer,
+body.theme-gruvbox-light .pdf-viewer,
+body.theme-ayu-light .pdf-viewer,
+body.theme-sepia .pdf-viewer,
+body.theme-paper .pdf-viewer,
+body.theme-rosepine-dawn .pdf-viewer {
+ background: #d0d0d0;
+}
+
+/* ========================================
+ Print Styles for styles-modern.css
+ Ensures proper print output
+ ======================================== */
+
+@media print {
+ /* Reset container for print */
+ .container {
+ padding: 0;
+ gap: 0;
+ display: block !important;
+ }
+
+ /* Hide all UI elements */
+ .tab-bar,
+ .toolbar,
+ .status-bar,
+ .pane-resizer,
+ .preview-header,
+ .find-dialog,
+ .export-dialog,
+ .batch-dialog,
+ .editor-pane,
+ [id^="editor-pane-"],
+ .new-tab-button,
+ .tab-close {
+ display: none !important;
+ }
+
+ /* Show preview content */
+ .preview-pane,
+ [id^="preview-pane-"],
+ .preview-content,
+ [id^="preview-"] {
+ display: block !important;
+ width: 100% !important;
+ padding: 20px !important;
+ position: static !important;
+ overflow: visible !important;
+ }
+
+ /* Reset main content glass */
+ .main-content-glass {
+ backdrop-filter: none !important;
+ border: none !important;
+ box-shadow: none !important;
+ }
+
+ /* Reset editor container */
+ .editor-container {
+ display: block !important;
+ }
+
+ /* Reset tab content */
+ .tab-content {
+ display: block !important;
+ }
+
+ /* Reset pane */
+ .pane {
+ display: block !important;
+ width: 100% !important;
+ }
+
+ /* NO STYLES MODE - Black text on white (ink-saving) */
+ body.printing-no-styles .container,
+ body.printing-no-styles .main-content-glass,
+ body.printing-no-styles .tab-content,
+ body.printing-no-styles .pane,
+ body.printing-no-styles .preview-pane,
+ body.printing-no-styles [id^="preview-pane-"],
+ body.printing-no-styles .preview-content,
+ body.printing-no-styles [id^="preview-"] {
+ background: white !important;
+ color: #000 !important;
+ }
+
+ body.printing-no-styles .preview-content h1,
+ body.printing-no-styles .preview-content h2,
+ body.printing-no-styles .preview-content h3,
+ body.printing-no-styles .preview-content h4,
+ body.printing-no-styles .preview-content h5,
+ body.printing-no-styles .preview-content h6,
+ body.printing-no-styles .preview-content p,
+ body.printing-no-styles .preview-content li,
+ body.printing-no-styles .preview-content td,
+ body.printing-no-styles .preview-content th {
+ color: #000 !important;
+ }
+
+ body.printing-no-styles .preview-content code,
+ body.printing-no-styles .preview-content pre {
+ background: #f5f5f5 !important;
+ color: #000 !important;
+ border-color: #ddd !important;
+ }
+
+ body.printing-no-styles .preview-content a {
+ color: #000 !important;
+ }
+
+ body.printing-no-styles .preview-content blockquote {
+ background: #f9f9f9 !important;
+ color: #333 !important;
+ border-left-color: #ccc !important;
+ }
+
+ /* WITH STYLES MODE - Preserve theme colors */
+ /* The theme colors are inherited from the body class */
+}
+
+/* ========================================
+ Dark Theme Overrides for styles-modern.css
+ These ensure dark themes work properly
+ ======================================== */
+
+/* Common dark theme styles for preview content */
+body.theme-dark .preview-content,
+body.theme-dark [id^="preview-"],
+body.theme-one-dark .preview-content,
+body.theme-one-dark [id^="preview-"],
+body.theme-dracula .preview-content,
+body.theme-dracula [id^="preview-"],
+body.theme-nord .preview-content,
+body.theme-nord [id^="preview-"],
+body.theme-tokyo-night .preview-content,
+body.theme-tokyo-night [id^="preview-"],
+body.theme-palenight .preview-content,
+body.theme-palenight [id^="preview-"],
+body.theme-ayu-dark .preview-content,
+body.theme-ayu-dark [id^="preview-"],
+body.theme-ayu-mirage .preview-content,
+body.theme-ayu-mirage [id^="preview-"],
+body.theme-oceanic-next .preview-content,
+body.theme-oceanic-next [id^="preview-"],
+body.theme-gruvbox-dark .preview-content,
+body.theme-gruvbox-dark [id^="preview-"],
+body.theme-cobalt2 .preview-content,
+body.theme-cobalt2 [id^="preview-"] {
+ color: #e6e6e6;
+ background: transparent;
+}
+
+/* Dark theme headings */
+body.theme-dark .preview-content h1,
+body.theme-dark .preview-content h2,
+body.theme-dark .preview-content h3,
+body.theme-dark .preview-content h4,
+body.theme-dark .preview-content h5,
+body.theme-dark .preview-content h6,
+body.theme-one-dark .preview-content h1,
+body.theme-one-dark .preview-content h2,
+body.theme-one-dark .preview-content h3,
+body.theme-one-dark .preview-content h4,
+body.theme-one-dark .preview-content h5,
+body.theme-one-dark .preview-content h6,
+body.theme-dracula .preview-content h1,
+body.theme-dracula .preview-content h2,
+body.theme-dracula .preview-content h3,
+body.theme-dracula .preview-content h4,
+body.theme-dracula .preview-content h5,
+body.theme-dracula .preview-content h6,
+body.theme-nord .preview-content h1,
+body.theme-nord .preview-content h2,
+body.theme-nord .preview-content h3,
+body.theme-nord .preview-content h4,
+body.theme-nord .preview-content h5,
+body.theme-nord .preview-content h6,
+body.theme-tokyo-night .preview-content h1,
+body.theme-tokyo-night .preview-content h2,
+body.theme-tokyo-night .preview-content h3,
+body.theme-tokyo-night .preview-content h4,
+body.theme-tokyo-night .preview-content h5,
+body.theme-tokyo-night .preview-content h6,
+body.theme-palenight .preview-content h1,
+body.theme-palenight .preview-content h2,
+body.theme-palenight .preview-content h3,
+body.theme-palenight .preview-content h4,
+body.theme-palenight .preview-content h5,
+body.theme-palenight .preview-content h6,
+body.theme-ayu-dark .preview-content h1,
+body.theme-ayu-dark .preview-content h2,
+body.theme-ayu-dark .preview-content h3,
+body.theme-ayu-dark .preview-content h4,
+body.theme-ayu-dark .preview-content h5,
+body.theme-ayu-dark .preview-content h6,
+body.theme-ayu-mirage .preview-content h1,
+body.theme-ayu-mirage .preview-content h2,
+body.theme-ayu-mirage .preview-content h3,
+body.theme-ayu-mirage .preview-content h4,
+body.theme-ayu-mirage .preview-content h5,
+body.theme-ayu-mirage .preview-content h6,
+body.theme-oceanic-next .preview-content h1,
+body.theme-oceanic-next .preview-content h2,
+body.theme-oceanic-next .preview-content h3,
+body.theme-oceanic-next .preview-content h4,
+body.theme-oceanic-next .preview-content h5,
+body.theme-oceanic-next .preview-content h6,
+body.theme-gruvbox-dark .preview-content h1,
+body.theme-gruvbox-dark .preview-content h2,
+body.theme-gruvbox-dark .preview-content h3,
+body.theme-gruvbox-dark .preview-content h4,
+body.theme-gruvbox-dark .preview-content h5,
+body.theme-gruvbox-dark .preview-content h6,
+body.theme-cobalt2 .preview-content h1,
+body.theme-cobalt2 .preview-content h2,
+body.theme-cobalt2 .preview-content h3,
+body.theme-cobalt2 .preview-content h4,
+body.theme-cobalt2 .preview-content h5,
+body.theme-cobalt2 .preview-content h6 {
+ color: #f0f0f0;
+ border-bottom-color: #444;
+}
+
+/* Dark theme paragraphs and text */
+body.theme-dark .preview-content p,
+body.theme-dark .preview-content li,
+body.theme-dark .preview-content td,
+body.theme-one-dark .preview-content p,
+body.theme-one-dark .preview-content li,
+body.theme-one-dark .preview-content td,
+body.theme-dracula .preview-content p,
+body.theme-dracula .preview-content li,
+body.theme-dracula .preview-content td,
+body.theme-nord .preview-content p,
+body.theme-nord .preview-content li,
+body.theme-nord .preview-content td,
+body.theme-tokyo-night .preview-content p,
+body.theme-tokyo-night .preview-content li,
+body.theme-tokyo-night .preview-content td,
+body.theme-palenight .preview-content p,
+body.theme-palenight .preview-content li,
+body.theme-palenight .preview-content td,
+body.theme-ayu-dark .preview-content p,
+body.theme-ayu-dark .preview-content li,
+body.theme-ayu-dark .preview-content td,
+body.theme-ayu-mirage .preview-content p,
+body.theme-ayu-mirage .preview-content li,
+body.theme-ayu-mirage .preview-content td,
+body.theme-oceanic-next .preview-content p,
+body.theme-oceanic-next .preview-content li,
+body.theme-oceanic-next .preview-content td,
+body.theme-gruvbox-dark .preview-content p,
+body.theme-gruvbox-dark .preview-content li,
+body.theme-gruvbox-dark .preview-content td,
+body.theme-cobalt2 .preview-content p,
+body.theme-cobalt2 .preview-content li,
+body.theme-cobalt2 .preview-content td {
+ color: #d4d4d4;
+}
+
+/* Dark theme code blocks */
+body.theme-dark .preview-content pre,
+body.theme-one-dark .preview-content pre,
+body.theme-dracula .preview-content pre,
+body.theme-nord .preview-content pre,
+body.theme-tokyo-night .preview-content pre,
+body.theme-palenight .preview-content pre,
+body.theme-ayu-dark .preview-content pre,
+body.theme-ayu-mirage .preview-content pre,
+body.theme-oceanic-next .preview-content pre,
+body.theme-gruvbox-dark .preview-content pre,
+body.theme-cobalt2 .preview-content pre {
+ background: #1e1e1e;
+ border-color: #333;
+ color: #d4d4d4;
+}
+
+/* Dark theme inline code */
+body.theme-dark .preview-content code,
+body.theme-one-dark .preview-content code,
+body.theme-dracula .preview-content code,
+body.theme-nord .preview-content code,
+body.theme-tokyo-night .preview-content code,
+body.theme-palenight .preview-content code,
+body.theme-ayu-dark .preview-content code,
+body.theme-ayu-mirage .preview-content code,
+body.theme-oceanic-next .preview-content code,
+body.theme-gruvbox-dark .preview-content code,
+body.theme-cobalt2 .preview-content code {
+ background: rgba(255, 255, 255, 0.1);
+ border-color: #444;
+ color: #e06c75;
+}
+
+/* Dark theme blockquotes */
+body.theme-dark .preview-content blockquote,
+body.theme-one-dark .preview-content blockquote,
+body.theme-dracula .preview-content blockquote,
+body.theme-nord .preview-content blockquote,
+body.theme-tokyo-night .preview-content blockquote,
+body.theme-palenight .preview-content blockquote,
+body.theme-ayu-dark .preview-content blockquote,
+body.theme-ayu-mirage .preview-content blockquote,
+body.theme-oceanic-next .preview-content blockquote,
+body.theme-gruvbox-dark .preview-content blockquote,
+body.theme-cobalt2 .preview-content blockquote {
+ background: rgba(255, 255, 255, 0.05);
+ border-left-color: #555;
+ color: #aaa;
+}
+
+/* Dark theme links */
+body.theme-dark .preview-content a,
+body.theme-one-dark .preview-content a,
+body.theme-dracula .preview-content a,
+body.theme-nord .preview-content a,
+body.theme-tokyo-night .preview-content a,
+body.theme-palenight .preview-content a,
+body.theme-ayu-dark .preview-content a,
+body.theme-ayu-mirage .preview-content a,
+body.theme-oceanic-next .preview-content a,
+body.theme-gruvbox-dark .preview-content a,
+body.theme-cobalt2 .preview-content a {
+ color: #61afef;
+}
+
+/* Dark theme tables */
+body.theme-dark .preview-content table,
+body.theme-one-dark .preview-content table,
+body.theme-dracula .preview-content table,
+body.theme-nord .preview-content table,
+body.theme-tokyo-night .preview-content table,
+body.theme-palenight .preview-content table,
+body.theme-ayu-dark .preview-content table,
+body.theme-ayu-mirage .preview-content table,
+body.theme-oceanic-next .preview-content table,
+body.theme-gruvbox-dark .preview-content table,
+body.theme-cobalt2 .preview-content table {
+ border-color: #444;
+}
+
+body.theme-dark .preview-content table th,
+body.theme-one-dark .preview-content table th,
+body.theme-dracula .preview-content table th,
+body.theme-nord .preview-content table th,
+body.theme-tokyo-night .preview-content table th,
+body.theme-palenight .preview-content table th,
+body.theme-ayu-dark .preview-content table th,
+body.theme-ayu-mirage .preview-content table th,
+body.theme-oceanic-next .preview-content table th,
+body.theme-gruvbox-dark .preview-content table th,
+body.theme-cobalt2 .preview-content table th {
+ background: #2d2d2d;
+ color: #e6e6e6;
+ border-color: #444;
+}
+
+body.theme-dark .preview-content table td,
+body.theme-one-dark .preview-content table td,
+body.theme-dracula .preview-content table td,
+body.theme-nord .preview-content table td,
+body.theme-tokyo-night .preview-content table td,
+body.theme-palenight .preview-content table td,
+body.theme-ayu-dark .preview-content table td,
+body.theme-ayu-mirage .preview-content table td,
+body.theme-oceanic-next .preview-content table td,
+body.theme-gruvbox-dark .preview-content table td,
+body.theme-cobalt2 .preview-content table td {
+ border-color: #444;
+}
+
+body.theme-dark .preview-content table tr:nth-child(even),
+body.theme-one-dark .preview-content table tr:nth-child(even),
+body.theme-dracula .preview-content table tr:nth-child(even),
+body.theme-nord .preview-content table tr:nth-child(even),
+body.theme-tokyo-night .preview-content table tr:nth-child(even),
+body.theme-palenight .preview-content table tr:nth-child(even),
+body.theme-ayu-dark .preview-content table tr:nth-child(even),
+body.theme-ayu-mirage .preview-content table tr:nth-child(even),
+body.theme-oceanic-next .preview-content table tr:nth-child(even),
+body.theme-gruvbox-dark .preview-content table tr:nth-child(even),
+body.theme-cobalt2 .preview-content table tr:nth-child(even) {
+ background: rgba(255, 255, 255, 0.03);
+}
+
+/* Dark theme horizontal rules */
+body.theme-dark .preview-content hr,
+body.theme-one-dark .preview-content hr,
+body.theme-dracula .preview-content hr,
+body.theme-nord .preview-content hr,
+body.theme-tokyo-night .preview-content hr,
+body.theme-palenight .preview-content hr,
+body.theme-ayu-dark .preview-content hr,
+body.theme-ayu-mirage .preview-content hr,
+body.theme-oceanic-next .preview-content hr,
+body.theme-gruvbox-dark .preview-content hr,
+body.theme-cobalt2 .preview-content hr {
+ border-color: #444;
+ background: #444;
+}
+
+/* ========================================
+ Modal/Dialog Theming
+ Dialogs inherit app theme colors
+ ======================================== */
+
+/* Dark theme modals */
+body.theme-dark .export-dialog-content,
+body.theme-dark .batch-dialog-content,
+body.theme-dark .pdf-editor-content,
+body.theme-one-dark .export-dialog-content,
+body.theme-one-dark .batch-dialog-content,
+body.theme-one-dark .pdf-editor-content,
+body.theme-onedark .export-dialog-content,
+body.theme-onedark .batch-dialog-content,
+body.theme-onedark .pdf-editor-content,
+body.theme-dracula .export-dialog-content,
+body.theme-dracula .batch-dialog-content,
+body.theme-dracula .pdf-editor-content,
+body.theme-nord .export-dialog-content,
+body.theme-nord .batch-dialog-content,
+body.theme-nord .pdf-editor-content,
+body.theme-tokyo-night .export-dialog-content,
+body.theme-tokyo-night .batch-dialog-content,
+body.theme-tokyo-night .pdf-editor-content,
+body.theme-tokyonight .export-dialog-content,
+body.theme-tokyonight .batch-dialog-content,
+body.theme-tokyonight .pdf-editor-content,
+body.theme-palenight .export-dialog-content,
+body.theme-palenight .batch-dialog-content,
+body.theme-palenight .pdf-editor-content,
+body.theme-ayu-dark .export-dialog-content,
+body.theme-ayu-dark .batch-dialog-content,
+body.theme-ayu-dark .pdf-editor-content,
+body.theme-ayu-mirage .export-dialog-content,
+body.theme-ayu-mirage .batch-dialog-content,
+body.theme-ayu-mirage .pdf-editor-content,
+body.theme-oceanic-next .export-dialog-content,
+body.theme-oceanic-next .batch-dialog-content,
+body.theme-oceanic-next .pdf-editor-content,
+body.theme-gruvbox-dark .export-dialog-content,
+body.theme-gruvbox-dark .batch-dialog-content,
+body.theme-gruvbox-dark .pdf-editor-content,
+body.theme-cobalt2 .export-dialog-content,
+body.theme-cobalt2 .batch-dialog-content,
+body.theme-cobalt2 .pdf-editor-content,
+body.theme-monokai .export-dialog-content,
+body.theme-monokai .batch-dialog-content,
+body.theme-monokai .pdf-editor-content,
+body.theme-material .export-dialog-content,
+body.theme-material .batch-dialog-content,
+body.theme-material .pdf-editor-content,
+body.theme-concrete-dark .export-dialog-content,
+body.theme-concrete-dark .batch-dialog-content,
+body.theme-concrete-dark .pdf-editor-content,
+body.theme-concrete-warm .export-dialog-content,
+body.theme-concrete-warm .batch-dialog-content,
+body.theme-concrete-warm .pdf-editor-content {
+ background: #2d2d2d !important;
+ border-color: #444 !important;
+ color: #e6e6e6 !important;
+}
+
+/* Dark theme dialog headers */
+body.theme-dark .export-dialog-header,
+body.theme-dark .batch-dialog-header,
+body.theme-one-dark .export-dialog-header,
+body.theme-one-dark .batch-dialog-header,
+body.theme-onedark .export-dialog-header,
+body.theme-onedark .batch-dialog-header,
+body.theme-dracula .export-dialog-header,
+body.theme-dracula .batch-dialog-header,
+body.theme-nord .export-dialog-header,
+body.theme-nord .batch-dialog-header,
+body.theme-tokyo-night .export-dialog-header,
+body.theme-tokyo-night .batch-dialog-header,
+body.theme-tokyonight .export-dialog-header,
+body.theme-tokyonight .batch-dialog-header,
+body.theme-palenight .export-dialog-header,
+body.theme-palenight .batch-dialog-header,
+body.theme-ayu-dark .export-dialog-header,
+body.theme-ayu-dark .batch-dialog-header,
+body.theme-ayu-mirage .export-dialog-header,
+body.theme-ayu-mirage .batch-dialog-header,
+body.theme-oceanic-next .export-dialog-header,
+body.theme-oceanic-next .batch-dialog-header,
+body.theme-gruvbox-dark .export-dialog-header,
+body.theme-gruvbox-dark .batch-dialog-header,
+body.theme-cobalt2 .export-dialog-header,
+body.theme-cobalt2 .batch-dialog-header,
+body.theme-monokai .export-dialog-header,
+body.theme-monokai .batch-dialog-header,
+body.theme-material .export-dialog-header,
+body.theme-material .batch-dialog-header,
+body.theme-concrete-dark .export-dialog-header,
+body.theme-concrete-dark .batch-dialog-header,
+body.theme-concrete-warm .export-dialog-header,
+body.theme-concrete-warm .batch-dialog-header {
+ background: #1a1a1a !important;
+ border-bottom-color: #333 !important;
+}
+
+/* Dark theme dialog header text */
+body.theme-dark .export-dialog-header h3,
+body.theme-dark .batch-dialog-header h3,
+body.theme-onedark .export-dialog-header h3,
+body.theme-onedark .batch-dialog-header h3,
+body.theme-dracula .export-dialog-header h3,
+body.theme-dracula .batch-dialog-header h3,
+body.theme-nord .export-dialog-header h3,
+body.theme-nord .batch-dialog-header h3,
+body.theme-tokyonight .export-dialog-header h3,
+body.theme-tokyonight .batch-dialog-header h3,
+body.theme-monokai .export-dialog-header h3,
+body.theme-monokai .batch-dialog-header h3,
+body.theme-material .export-dialog-header h3,
+body.theme-material .batch-dialog-header h3 {
+ color: #fff !important;
+}
+
+/* Dark theme dialog body */
+body.theme-dark .export-dialog-body,
+body.theme-dark .batch-dialog-body,
+body.theme-one-dark .export-dialog-body,
+body.theme-one-dark .batch-dialog-body,
+body.theme-dracula .export-dialog-body,
+body.theme-dracula .batch-dialog-body,
+body.theme-nord .export-dialog-body,
+body.theme-nord .batch-dialog-body,
+body.theme-tokyo-night .export-dialog-body,
+body.theme-tokyo-night .batch-dialog-body,
+body.theme-palenight .export-dialog-body,
+body.theme-palenight .batch-dialog-body,
+body.theme-ayu-dark .export-dialog-body,
+body.theme-ayu-dark .batch-dialog-body,
+body.theme-ayu-mirage .export-dialog-body,
+body.theme-ayu-mirage .batch-dialog-body,
+body.theme-oceanic-next .export-dialog-body,
+body.theme-oceanic-next .batch-dialog-body,
+body.theme-gruvbox-dark .export-dialog-body,
+body.theme-gruvbox-dark .batch-dialog-body,
+body.theme-cobalt2 .export-dialog-body,
+body.theme-cobalt2 .batch-dialog-body {
+ background: transparent;
+}
+
+/* Dark theme dialog labels */
+body.theme-dark .export-section label,
+body.theme-dark .batch-section label,
+body.theme-one-dark .export-section label,
+body.theme-one-dark .batch-section label,
+body.theme-dracula .export-section label,
+body.theme-dracula .batch-section label,
+body.theme-nord .export-section label,
+body.theme-nord .batch-section label,
+body.theme-tokyo-night .export-section label,
+body.theme-tokyo-night .batch-section label,
+body.theme-palenight .export-section label,
+body.theme-palenight .batch-section label,
+body.theme-ayu-dark .export-section label,
+body.theme-ayu-dark .batch-section label,
+body.theme-ayu-mirage .export-section label,
+body.theme-ayu-mirage .batch-section label,
+body.theme-oceanic-next .export-section label,
+body.theme-oceanic-next .batch-section label,
+body.theme-gruvbox-dark .export-section label,
+body.theme-gruvbox-dark .batch-section label,
+body.theme-cobalt2 .export-section label,
+body.theme-cobalt2 .batch-section label {
+ color: #ccc;
+}
+
+/* Dark theme dialog inputs and selects */
+body.theme-dark .export-section select,
+body.theme-dark .export-section input,
+body.theme-dark .batch-section select,
+body.theme-dark .batch-section input,
+body.theme-one-dark .export-section select,
+body.theme-one-dark .export-section input,
+body.theme-one-dark .batch-section select,
+body.theme-one-dark .batch-section input,
+body.theme-dracula .export-section select,
+body.theme-dracula .export-section input,
+body.theme-dracula .batch-section select,
+body.theme-dracula .batch-section input,
+body.theme-nord .export-section select,
+body.theme-nord .export-section input,
+body.theme-nord .batch-section select,
+body.theme-nord .batch-section input,
+body.theme-tokyo-night .export-section select,
+body.theme-tokyo-night .export-section input,
+body.theme-tokyo-night .batch-section select,
+body.theme-tokyo-night .batch-section input,
+body.theme-palenight .export-section select,
+body.theme-palenight .export-section input,
+body.theme-palenight .batch-section select,
+body.theme-palenight .batch-section input,
+body.theme-ayu-dark .export-section select,
+body.theme-ayu-dark .export-section input,
+body.theme-ayu-dark .batch-section select,
+body.theme-ayu-dark .batch-section input,
+body.theme-ayu-mirage .export-section select,
+body.theme-ayu-mirage .export-section input,
+body.theme-ayu-mirage .batch-section select,
+body.theme-ayu-mirage .batch-section input,
+body.theme-oceanic-next .export-section select,
+body.theme-oceanic-next .export-section input,
+body.theme-oceanic-next .batch-section select,
+body.theme-oceanic-next .batch-section input,
+body.theme-gruvbox-dark .export-section select,
+body.theme-gruvbox-dark .export-section input,
+body.theme-gruvbox-dark .batch-section select,
+body.theme-gruvbox-dark .batch-section input,
+body.theme-cobalt2 .export-section select,
+body.theme-cobalt2 .export-section input,
+body.theme-cobalt2 .batch-section select,
+body.theme-cobalt2 .batch-section input {
+ background: #2d2d2d;
+ border-color: #444;
+ color: #e6e6e6;
+}
+
+/* Dark theme dialog footer */
+body.theme-dark .export-dialog-footer,
+body.theme-dark .batch-dialog-footer,
+body.theme-one-dark .export-dialog-footer,
+body.theme-one-dark .batch-dialog-footer,
+body.theme-dracula .export-dialog-footer,
+body.theme-dracula .batch-dialog-footer,
+body.theme-nord .export-dialog-footer,
+body.theme-nord .batch-dialog-footer,
+body.theme-tokyo-night .export-dialog-footer,
+body.theme-tokyo-night .batch-dialog-footer,
+body.theme-palenight .export-dialog-footer,
+body.theme-palenight .batch-dialog-footer,
+body.theme-ayu-dark .export-dialog-footer,
+body.theme-ayu-dark .batch-dialog-footer,
+body.theme-ayu-mirage .export-dialog-footer,
+body.theme-ayu-mirage .batch-dialog-footer,
+body.theme-oceanic-next .export-dialog-footer,
+body.theme-oceanic-next .batch-dialog-footer,
+body.theme-gruvbox-dark .export-dialog-footer,
+body.theme-gruvbox-dark .batch-dialog-footer,
+body.theme-cobalt2 .export-dialog-footer,
+body.theme-cobalt2 .batch-dialog-footer {
+ background: #1a1a1a;
+ border-top-color: #333;
+}
+
+/* Sepia theme modals */
+body.theme-sepia .export-dialog-content,
+body.theme-sepia .batch-dialog-content,
+body.theme-sepia .pdf-editor-content {
+ background: rgba(244, 236, 216, 0.98);
+ border-color: #d4c4a8;
+ color: #5b4636;
+}
+
+body.theme-sepia .export-dialog-header,
+body.theme-sepia .batch-dialog-header {
+ background: #e8dcc8;
+ border-bottom-color: #d4c4a8;
+}
+
+body.theme-sepia .export-dialog-header h3,
+body.theme-sepia .batch-dialog-header h3 {
+ color: #3d2914;
+}
+
+body.theme-sepia .export-section label,
+body.theme-sepia .batch-section label {
+ color: #5b4636;
+}
+
+body.theme-sepia .export-section select,
+body.theme-sepia .export-section input,
+body.theme-sepia .batch-section select,
+body.theme-sepia .batch-section input {
+ background: #f4ecd8;
+ border-color: #d4c4a8;
+ color: #5b4636;
+}
+
+body.theme-sepia .export-dialog-footer,
+body.theme-sepia .batch-dialog-footer {
+ background: #e8dcc8;
+ border-top-color: #d4c4a8;
+}
+
+/* Rose Pine Dawn theme modals */
+body.theme-rosepine-dawn .export-dialog-content,
+body.theme-rosepine-dawn .batch-dialog-content,
+body.theme-rosepine-dawn .pdf-editor-content {
+ background: rgba(250, 244, 237, 0.98);
+ border-color: #dfdad9;
+ color: #575279;
+}
+
+body.theme-rosepine-dawn .export-dialog-header,
+body.theme-rosepine-dawn .batch-dialog-header {
+ background: #f2e9e1;
+ border-bottom-color: #dfdad9;
+}
+
+body.theme-rosepine-dawn .export-dialog-header h3,
+body.theme-rosepine-dawn .batch-dialog-header h3 {
+ color: #286983;
+}
+
+body.theme-rosepine-dawn .export-section label,
+body.theme-rosepine-dawn .batch-section label {
+ color: #575279;
+}
+
+body.theme-rosepine-dawn .export-section select,
+body.theme-rosepine-dawn .export-section input,
+body.theme-rosepine-dawn .batch-section select,
+body.theme-rosepine-dawn .batch-section input {
+ background: #faf4ed;
+ border-color: #dfdad9;
+ color: #575279;
+}
+
+body.theme-rosepine-dawn .export-dialog-footer,
+body.theme-rosepine-dawn .batch-dialog-footer {
+ background: #f2e9e1;
+ border-top-color: #dfdad9;
+}
diff --git a/src/styles.css b/src/styles.css
index 41258b8..c377601 100644
--- a/src/styles.css
+++ b/src/styles.css
@@ -1031,52 +1031,82 @@ body.theme-github .status-bar {
.export-dialog-content {
background: #fff;
- border-radius: 8px;
- box-shadow: 0 4px 20px rgba(0, 0, 0, 0.2);
- width: 90%;
- max-width: 600px;
- max-height: 90vh;
+ border-radius: 6px;
+ box-shadow: 0 4px 16px rgba(0, 0, 0, 0.15);
+ width: 85%;
+ max-width: 450px;
+ max-height: 80vh;
overflow-y: auto;
position: relative;
+ font-size: 12px;
}
.export-dialog-header {
display: flex;
align-items: center;
justify-content: space-between;
- padding: 16px 20px;
+ padding: 12px 16px;
border-bottom: 1px solid #eee;
}
.export-dialog-header h3 {
margin: 0;
- font-size: 18px;
+ font-size: 14px;
font-weight: 600;
}
.export-dialog-body {
- padding: 20px;
+ padding: 14px 16px;
}
.export-section {
- margin-bottom: 20px;
+ margin-bottom: 14px;
}
.export-section label {
display: block;
- font-weight: 600;
- margin-bottom: 8px;
+ font-weight: 500;
+ margin-bottom: 4px;
color: #333;
+ font-size: 12px;
+}
+
+.export-section label.checkbox-inline {
+ display: flex;
+ align-items: center;
+ gap: 6px;
+ cursor: pointer;
+ font-weight: 400;
+}
+
+.export-section label.checkbox-inline input[type="checkbox"] {
+ width: auto;
+ margin: 0;
+}
+
+/* PDF Editor buttons */
+#add-merge-file {
+ padding: 5px 10px;
+ font-size: 12px;
+ background: #4a90d9;
+ color: #fff;
+ border: none;
+ border-radius: 4px;
+ cursor: pointer;
+}
+
+#add-merge-file:hover {
+ background: #357abd;
}
.export-section select,
.export-section input[type="text"],
.export-section input[type="number"] {
width: 100%;
- padding: 8px 12px;
+ padding: 5px 8px;
border: 1px solid #ddd;
border-radius: 4px;
- font-size: 14px;
+ font-size: 12px;
}
.export-section select:focus,
@@ -1280,9 +1310,10 @@ body.theme-dark #add-metadata-field {
/* PDF Editor Specific Styles */
.pdf-editor-content {
- max-width: 600px !important;
- max-height: 85vh;
+ max-width: 450px !important;
+ max-height: 75vh;
overflow-y: auto;
+ font-size: 12px;
}
.pdf-editor-body {
@@ -1418,12 +1449,13 @@ body.theme-dark #export-dialog-close:hover {
.batch-dialog-content {
background: #fff;
- border-radius: 8px;
- box-shadow: 0 4px 20px rgba(0, 0, 0, 0.2);
- width: 90%;
- max-width: 500px;
- max-height: 80vh;
+ border-radius: 6px;
+ box-shadow: 0 4px 16px rgba(0, 0, 0, 0.15);
+ width: 85%;
+ max-width: 450px;
+ max-height: 75vh;
overflow-y: auto;
+ font-size: 12px;
}
.batch-dialog-header {
@@ -3152,6 +3184,250 @@ body.theme-concrete-warm .status-bar {
color: #e3e3e3;
}
+/* ========================================
+ Sepia Theme - Warm reading theme
+ ======================================== */
+body.theme-sepia {
+ background: #f4ecd8;
+ color: #5b4636;
+}
+
+body.theme-sepia .tab-bar {
+ background: #e8dcc8;
+ border-bottom-color: #d4c4a8;
+}
+
+body.theme-sepia .tab {
+ background: #e8dcc8;
+ border-color: #d4c4a8;
+ color: #5b4636;
+}
+
+body.theme-sepia .tab.active {
+ background: #f4ecd8;
+ border-bottom-color: #8b7355;
+}
+
+body.theme-sepia .toolbar {
+ background: #e8dcc8;
+ border-bottom-color: #d4c4a8;
+}
+
+body.theme-sepia .toolbar button {
+ color: #5b4636;
+}
+
+body.theme-sepia .toolbar button:hover {
+ background: #d4c4a8;
+}
+
+body.theme-sepia .editor-textarea {
+ background: #f4ecd8;
+ color: #5b4636;
+}
+
+body.theme-sepia .pane:last-child {
+ background: #f4ecd8;
+}
+
+body.theme-sepia .preview-content {
+ color: #5b4636;
+}
+
+body.theme-sepia .preview-content h1,
+body.theme-sepia .preview-content h2 {
+ color: #3d2914;
+ border-bottom-color: #d4c4a8;
+}
+
+body.theme-sepia .preview-content code {
+ background: #e8dcc8;
+ color: #8b4513;
+}
+
+body.theme-sepia .preview-content pre {
+ background: #e8dcc8;
+ border-color: #d4c4a8;
+}
+
+body.theme-sepia .preview-content a {
+ color: #8b4513;
+}
+
+body.theme-sepia .preview-content blockquote {
+ border-left-color: #8b7355;
+ background: #e8dcc8;
+}
+
+body.theme-sepia .status-bar {
+ background: #e8dcc8;
+ border-top-color: #d4c4a8;
+ color: #5b4636;
+}
+
+/* ========================================
+ Paper Theme - Clean white paper
+ ======================================== */
+body.theme-paper {
+ background: #ffffff;
+ color: #1a1a1a;
+}
+
+body.theme-paper .tab-bar {
+ background: #f8f8f8;
+ border-bottom-color: #e0e0e0;
+}
+
+body.theme-paper .tab {
+ background: #f0f0f0;
+ border-color: #e0e0e0;
+ color: #333;
+}
+
+body.theme-paper .tab.active {
+ background: #ffffff;
+ border-bottom-color: #1a1a1a;
+}
+
+body.theme-paper .toolbar {
+ background: #f8f8f8;
+ border-bottom-color: #e0e0e0;
+}
+
+body.theme-paper .toolbar button {
+ color: #333;
+}
+
+body.theme-paper .toolbar button:hover {
+ background: #e8e8e8;
+}
+
+body.theme-paper .editor-textarea {
+ background: #ffffff;
+ color: #1a1a1a;
+}
+
+body.theme-paper .pane:last-child {
+ background: #ffffff;
+}
+
+body.theme-paper .preview-content {
+ color: #1a1a1a;
+}
+
+body.theme-paper .preview-content h1,
+body.theme-paper .preview-content h2 {
+ color: #000;
+ border-bottom-color: #e0e0e0;
+}
+
+body.theme-paper .preview-content code {
+ background: #f5f5f5;
+ color: #c41a16;
+}
+
+body.theme-paper .preview-content pre {
+ background: #f5f5f5;
+ border-color: #e0e0e0;
+}
+
+body.theme-paper .preview-content a {
+ color: #0366d6;
+}
+
+body.theme-paper .preview-content blockquote {
+ border-left-color: #ddd;
+ background: #f9f9f9;
+}
+
+body.theme-paper .status-bar {
+ background: #f8f8f8;
+ border-top-color: #e0e0e0;
+ color: #333;
+}
+
+/* ========================================
+ Rose Pine Dawn - Warm rose-tinted light
+ ======================================== */
+body.theme-rosepine-dawn {
+ background: #faf4ed;
+ color: #575279;
+}
+
+body.theme-rosepine-dawn .tab-bar {
+ background: #f2e9e1;
+ border-bottom-color: #dfdad9;
+}
+
+body.theme-rosepine-dawn .tab {
+ background: #f2e9e1;
+ border-color: #dfdad9;
+ color: #575279;
+}
+
+body.theme-rosepine-dawn .tab.active {
+ background: #faf4ed;
+ border-bottom-color: #907aa9;
+}
+
+body.theme-rosepine-dawn .toolbar {
+ background: #f2e9e1;
+ border-bottom-color: #dfdad9;
+}
+
+body.theme-rosepine-dawn .toolbar button {
+ color: #575279;
+}
+
+body.theme-rosepine-dawn .toolbar button:hover {
+ background: #dfdad9;
+}
+
+body.theme-rosepine-dawn .editor-textarea {
+ background: #faf4ed;
+ color: #575279;
+}
+
+body.theme-rosepine-dawn .pane:last-child {
+ background: #faf4ed;
+}
+
+body.theme-rosepine-dawn .preview-content {
+ color: #575279;
+}
+
+body.theme-rosepine-dawn .preview-content h1,
+body.theme-rosepine-dawn .preview-content h2 {
+ color: #286983;
+ border-bottom-color: #dfdad9;
+}
+
+body.theme-rosepine-dawn .preview-content code {
+ background: #f2e9e1;
+ color: #b4637a;
+}
+
+body.theme-rosepine-dawn .preview-content pre {
+ background: #f2e9e1;
+ border-color: #dfdad9;
+}
+
+body.theme-rosepine-dawn .preview-content a {
+ color: #56949f;
+}
+
+body.theme-rosepine-dawn .preview-content blockquote {
+ border-left-color: #907aa9;
+ background: #f2e9e1;
+ color: #797593;
+}
+
+body.theme-rosepine-dawn .status-bar {
+ background: #f2e9e1;
+ border-top-color: #dfdad9;
+ color: #575279;
+}
+
/* Print Styles */
@media print {
/* Hide UI elements for clean print output */
diff --git a/src/table-generator.html b/src/table-generator.html
new file mode 100644
index 0000000..4ee64e6
--- /dev/null
+++ b/src/table-generator.html
@@ -0,0 +1,545 @@
+
+
+
+
+
+ Table Generator - MarkdownConverter
+
+
+
+
+
+
+
+
+
Table Settings
+
+
+ Rows
+
+
+
+ Columns
+
+
+
+ Alignment
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/tests/preload.test.js b/tests/preload.test.js
new file mode 100644
index 0000000..60d75d9
--- /dev/null
+++ b/tests/preload.test.js
@@ -0,0 +1,121 @@
+/**
+ * Tests for Preload Script Security
+ * Verifies that the IPC bridge is properly configured
+ */
+
+describe('Preload Security', () => {
+ describe('Allowed Channels', () => {
+ const EXPECTED_SEND_CHANNELS = [
+ 'save-file',
+ 'save-current-file',
+ 'set-current-file',
+ 'save-recent-files',
+ 'clear-recent-files',
+ 'renderer-ready',
+ 'get-theme',
+ 'do-print',
+ 'export-with-options',
+ 'export-spreadsheet',
+ 'batch-convert',
+ 'select-folder',
+ 'universal-convert',
+ 'universal-convert-batch',
+ 'get-header-footer-settings',
+ 'save-header-footer-settings',
+ 'browse-header-footer-logo',
+ 'save-header-footer-logo',
+ 'clear-header-footer-logo',
+ 'get-page-settings',
+ 'update-page-settings',
+ 'set-custom-start-page',
+ 'process-pdf-operation',
+ 'get-pdf-page-count',
+ 'select-pdf-folder'
+ ];
+
+ const EXPECTED_RECEIVE_CHANNELS = [
+ 'file-new',
+ 'file-opened',
+ 'file-save',
+ 'get-content-for-save',
+ 'get-content-for-spreadsheet',
+ 'recent-files-cleared',
+ 'toggle-preview',
+ 'toggle-find',
+ 'theme-changed',
+ 'theme-data',
+ 'undo',
+ 'redo',
+ 'adjust-font-size',
+ 'print-preview',
+ 'print-preview-styled',
+ 'show-export-dialog',
+ 'show-batch-dialog',
+ 'show-universal-converter-dialog',
+ 'show-table-generator',
+ 'show-pdf-editor-dialog',
+ 'conversion-status',
+ 'conversion-complete',
+ 'batch-progress',
+ 'folder-selected',
+ 'pdf-folder-selected',
+ 'header-footer-settings-data',
+ 'header-footer-logo-selected',
+ 'header-footer-logo-saved',
+ 'page-settings-data',
+ 'pdf-page-count',
+ 'pdf-operation-complete',
+ 'pdf-operation-error'
+ ];
+
+ test('should define all expected send channels', () => {
+ // This test documents expected channels
+ expect(EXPECTED_SEND_CHANNELS.length).toBeGreaterThan(0);
+ });
+
+ test('should define all expected receive channels', () => {
+ // This test documents expected channels
+ expect(EXPECTED_RECEIVE_CHANNELS.length).toBeGreaterThan(0);
+ });
+ });
+
+ describe('electronAPI Interface', () => {
+ test('should expose send method', () => {
+ expect(window.electronAPI.send).toBeDefined();
+ expect(typeof window.electronAPI.send).toBe('function');
+ });
+
+ test('should expose on method', () => {
+ expect(window.electronAPI.on).toBeDefined();
+ expect(typeof window.electronAPI.on).toBe('function');
+ });
+
+ test('should expose once method', () => {
+ expect(window.electronAPI.once).toBeDefined();
+ expect(typeof window.electronAPI.once).toBe('function');
+ });
+
+ test('should expose invoke method', () => {
+ expect(window.electronAPI.invoke).toBeDefined();
+ expect(typeof window.electronAPI.invoke).toBe('function');
+ });
+
+ test('should expose file convenience methods', () => {
+ expect(window.electronAPI.file).toBeDefined();
+ expect(window.electronAPI.file.save).toBeDefined();
+ expect(window.electronAPI.file.saveCurrent).toBeDefined();
+ expect(window.electronAPI.file.setCurrent).toBeDefined();
+ });
+
+ test('should expose theme convenience methods', () => {
+ expect(window.electronAPI.theme).toBeDefined();
+ expect(window.electronAPI.theme.get).toBeDefined();
+ });
+
+ test('should expose pdf convenience methods', () => {
+ expect(window.electronAPI.pdf).toBeDefined();
+ expect(window.electronAPI.pdf.processOperation).toBeDefined();
+ expect(window.electronAPI.pdf.getPageCount).toBeDefined();
+ });
+ });
+});
diff --git a/tests/setup.js b/tests/setup.js
new file mode 100644
index 0000000..7c6bdaa
--- /dev/null
+++ b/tests/setup.js
@@ -0,0 +1,100 @@
+/**
+ * Jest Test Setup
+ * Provides mocks and utilities for PanConverter tests
+ */
+
+// Mock window.electronAPI for renderer tests
+global.window = global.window || {};
+global.window.electronAPI = {
+ send: jest.fn(),
+ on: jest.fn(() => jest.fn()), // Returns cleanup function
+ once: jest.fn(),
+ invoke: jest.fn(() => Promise.resolve(null)),
+ removeAllListeners: jest.fn(),
+ file: {
+ save: jest.fn(),
+ saveCurrent: jest.fn(),
+ setCurrent: jest.fn(),
+ saveRecent: jest.fn(),
+ clearRecent: jest.fn(),
+ rendererReady: jest.fn()
+ },
+ theme: {
+ get: jest.fn()
+ },
+ print: {
+ doPrint: jest.fn()
+ },
+ export: {
+ withOptions: jest.fn(),
+ spreadsheet: jest.fn()
+ },
+ batch: {
+ convert: jest.fn(),
+ selectFolder: jest.fn()
+ },
+ converter: {
+ convert: jest.fn(),
+ convertBatch: jest.fn()
+ },
+ headerFooter: {
+ getSettings: jest.fn(),
+ saveSettings: jest.fn(),
+ browseLogo: jest.fn(),
+ saveLogo: jest.fn(),
+ clearLogo: jest.fn()
+ },
+ page: {
+ getSettings: jest.fn(),
+ updateSettings: jest.fn(),
+ setCustomStartPage: jest.fn()
+ },
+ pdf: {
+ processOperation: jest.fn(),
+ getPageCount: jest.fn(),
+ selectFolder: jest.fn()
+ }
+};
+
+// Mock marked library
+global.window.marked = {
+ parse: jest.fn((text) => `${text}
`),
+ setOptions: jest.fn()
+};
+
+// Mock DOMPurify
+global.window.DOMPurify = {
+ sanitize: jest.fn((html) => html)
+};
+
+// Mock highlight.js
+global.window.hljs = {
+ highlight: jest.fn((code, options) => ({ value: code })),
+ highlightAuto: jest.fn((code) => ({ value: code })),
+ getLanguage: jest.fn(() => true)
+};
+
+// Mock localStorage
+const localStorageMock = {
+ getItem: jest.fn(),
+ setItem: jest.fn(),
+ removeItem: jest.fn(),
+ clear: jest.fn()
+};
+global.localStorage = localStorageMock;
+
+// Console spy to catch unintended console logs in tests
+const originalConsoleError = console.error;
+console.error = (...args) => {
+ // Allow certain expected errors
+ const message = args[0]?.toString() || '';
+ if (message.includes('Warning:') || message.includes('React')) {
+ return;
+ }
+ originalConsoleError.apply(console, args);
+};
+
+// Cleanup after each test
+afterEach(() => {
+ jest.clearAllMocks();
+});
diff --git a/tests/utils.test.js b/tests/utils.test.js
new file mode 100644
index 0000000..1bdc7df
--- /dev/null
+++ b/tests/utils.test.js
@@ -0,0 +1,137 @@
+/**
+ * Tests for Utility Functions
+ * Tests helper functions that can be extracted and tested
+ */
+
+describe('Utility Functions', () => {
+ describe('parseCommand', () => {
+ // This function parses command strings into command and args
+ function parseCommand(cmdString) {
+ const parts = [];
+ let current = '';
+ let inQuotes = false;
+ let quoteChar = '';
+
+ for (let i = 0; i < cmdString.length; i++) {
+ const char = cmdString[i];
+ if ((char === '"' || char === "'") && !inQuotes) {
+ inQuotes = true;
+ quoteChar = char;
+ } else if (char === quoteChar && inQuotes) {
+ inQuotes = false;
+ quoteChar = '';
+ } else if (char === ' ' && !inQuotes) {
+ if (current) {
+ parts.push(current);
+ current = '';
+ }
+ } else {
+ current += char;
+ }
+ }
+ if (current) {
+ parts.push(current);
+ }
+
+ return {
+ command: parts[0],
+ args: parts.slice(1)
+ };
+ }
+
+ test('should parse simple command', () => {
+ const result = parseCommand('pandoc input.md -o output.pdf');
+ expect(result.command).toBe('pandoc');
+ expect(result.args).toEqual(['input.md', '-o', 'output.pdf']);
+ });
+
+ test('should handle double-quoted paths', () => {
+ const result = parseCommand('pandoc "C:/path with spaces/file.md" -o output.pdf');
+ expect(result.command).toBe('pandoc');
+ expect(result.args).toEqual(['C:/path with spaces/file.md', '-o', 'output.pdf']);
+ });
+
+ test('should handle single-quoted paths', () => {
+ const result = parseCommand("pandoc 'file name.md' -o output.pdf");
+ expect(result.command).toBe('pandoc');
+ expect(result.args).toEqual(['file name.md', '-o', 'output.pdf']);
+ });
+
+ test('should handle multiple options', () => {
+ const result = parseCommand('pandoc input.md --pdf-engine=xelatex -V geometry:margin=1in -o output.pdf');
+ expect(result.command).toBe('pandoc');
+ expect(result.args).toContain('--pdf-engine=xelatex');
+ expect(result.args).toContain('-V');
+ });
+
+ test('should handle empty command', () => {
+ const result = parseCommand('');
+ expect(result.command).toBeUndefined();
+ expect(result.args).toEqual([]);
+ });
+ });
+
+ describe('hexToRgb', () => {
+ // This function converts hex colors to RGB
+ function hexToRgb(hex) {
+ const result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex);
+ return result ? {
+ r: parseInt(result[1], 16) / 255,
+ g: parseInt(result[2], 16) / 255,
+ b: parseInt(result[3], 16) / 255
+ } : null;
+ }
+
+ test('should convert black hex to RGB', () => {
+ const result = hexToRgb('#000000');
+ expect(result).toEqual({ r: 0, g: 0, b: 0 });
+ });
+
+ test('should convert white hex to RGB', () => {
+ const result = hexToRgb('#ffffff');
+ expect(result).toEqual({ r: 1, g: 1, b: 1 });
+ });
+
+ test('should convert red hex to RGB', () => {
+ const result = hexToRgb('#ff0000');
+ expect(result.r).toBeCloseTo(1);
+ expect(result.g).toBeCloseTo(0);
+ expect(result.b).toBeCloseTo(0);
+ });
+
+ test('should handle hex without hash', () => {
+ const result = hexToRgb('00ff00');
+ expect(result.r).toBeCloseTo(0);
+ expect(result.g).toBeCloseTo(1);
+ expect(result.b).toBeCloseTo(0);
+ });
+
+ test('should return null for invalid hex', () => {
+ expect(hexToRgb('invalid')).toBeNull();
+ expect(hexToRgb('#xyz')).toBeNull();
+ });
+ });
+
+ describe('File Path Utilities', () => {
+ test('should extract file extension', () => {
+ const getExtension = (filepath) => {
+ const match = filepath.match(/\.([^/.]+)$/);
+ return match ? match[1].toLowerCase() : '';
+ };
+
+ expect(getExtension('file.md')).toBe('md');
+ expect(getExtension('document.PDF')).toBe('pdf');
+ expect(getExtension('path/to/file.docx')).toBe('docx');
+ expect(getExtension('noextension')).toBe('');
+ });
+
+ test('should replace file extension', () => {
+ const replaceExtension = (filepath, newExt) => {
+ return filepath.replace(/\.[^/.]+$/, `.${newExt}`);
+ };
+
+ expect(replaceExtension('file.md', 'pdf')).toBe('file.pdf');
+ expect(replaceExtension('path/to/doc.docx', 'html')).toBe('path/to/doc.html');
+ });
+ });
+});