diff --git a/main.js b/main.js index 2ffb10b..4fbab82 100644 --- a/main.js +++ b/main.js @@ -1,13 +1,14 @@ -const { app, BrowserWindow, dialog, ipcMain, Menu } = require('electron'); +const { app, BrowserWindow, dialog, ipcMain } = require('electron'); const path = require('path'); const fs = require('fs'); let mainWindow = null; let currentFilePath = null; -let pendingFilePath = null; // For file association race condition +let pendingFilePath = null; +let fileWatcher = null; +let isModifiedExternally = false; function createWindow() { - // Create the browser window mainWindow = new BrowserWindow({ width: 1200, height: 800, @@ -23,7 +24,6 @@ function createWindow() { show: false }); - // Register did-finish-load BEFORE loadFile to avoid race condition mainWindow.webContents.on('did-finish-load', () => { if (pendingFilePath) { openFile(pendingFilePath); @@ -31,102 +31,48 @@ function createWindow() { } }); - // Load the index.html mainWindow.loadFile(path.join(__dirname, 'renderer', 'index.html')); - // Show window when ready mainWindow.once('ready-to-show', () => { mainWindow.show(); }); - // Remove native menu bar mainWindow.setMenu(null); + // Unsaved changes warning on close + mainWindow.on('close', (e) => { + mainWindow.webContents.send('window:closing'); + e.preventDefault(); + mainWindow.webContents.send('window:confirmClose'); + }); + mainWindow.on('closed', () => { + stopWatching(); mainWindow = null; }); } -function buildMenu() { - const template = [ - { - label: '文件', - submenu: [ - { - label: '打开文件', - accelerator: 'CmdOrCtrl+O', - click: () => handleOpenFile() - }, - { - label: '保存', - accelerator: 'CmdOrCtrl+S', - click: () => mainWindow.webContents.send('menu:save') - }, - { - label: '另存为', - accelerator: 'CmdOrCtrl+Shift+S', - click: () => mainWindow.webContents.send('menu:saveAs') - }, - { type: 'separator' }, - { - label: '退出', - accelerator: 'CmdOrCtrl+Q', - click: () => app.quit() - } - ] - }, - { - label: '视图', - submenu: [ - { - label: '编辑 + 预览', - accelerator: 'CmdOrCtrl+1', - click: () => mainWindow.webContents.send('menu:viewMode', 'split') - }, - { - label: '纯编辑', - accelerator: 'CmdOrCtrl+2', - click: () => mainWindow.webContents.send('menu:viewMode', 'editor') - }, - { - label: '纯预览', - accelerator: 'CmdOrCtrl+3', - click: () => mainWindow.webContents.send('menu:viewMode', 'preview') - }, - { type: 'separator' }, - { - label: '开发者工具', - accelerator: 'F12', - click: () => mainWindow.webContents.toggleDevTools() - }, - { type: 'separator' }, - { - label: '重新加载', - accelerator: 'CmdOrCtrl+R', - click: () => mainWindow.webContents.reload() - } - ] - }, - { - label: '帮助', - submenu: [ - { - label: '关于 MarkLite', - click: () => { - dialog.showMessageBox(mainWindow, { - type: 'info', - title: '关于 MarkLite', - message: 'MarkLite v1.0.0', - detail: '一款轻量级的 Markdown 阅读器。\n\n技术栈:Electron + marked.js + highlight.js' - }); - } - } - ] - } - ]; +// File watcher - detect external modifications +function startWatching(filePath) { + stopWatching(); + try { + isModifiedExternally = false; + fileWatcher = fs.watch(filePath, (eventType) => { + if (eventType === 'change' && mainWindow && !mainWindow.isDestroyed()) { + isModifiedExternally = true; + mainWindow.webContents.send('file:externallyModified', filePath); + } + }); + } catch (err) { + // Silently ignore watch errors + } +} - const menu = Menu.buildFromTemplate(template); - Menu.setApplicationMenu(menu); +function stopWatching() { + if (fileWatcher) { + fileWatcher.close(); + fileWatcher = null; + } } async function showOpenDialog() { @@ -143,17 +89,11 @@ async function showOpenDialog() { return null; } -async function handleOpenFile() { - const filePath = await showOpenDialog(); - if (filePath) { - openFile(filePath); - } -} - function openFile(filePath) { try { const content = fs.readFileSync(filePath, 'utf-8'); currentFilePath = filePath; + startWatching(filePath); mainWindow.setTitle(`MarkLite - ${path.basename(filePath)}`); mainWindow.webContents.send('file:opened', { filePath, content }); } catch (err) { @@ -163,14 +103,19 @@ function openFile(filePath) { // IPC Handlers ipcMain.handle('dialog:openFile', async () => { - const filePath = await showOpenDialog(); - if (filePath) { - const content = fs.readFileSync(filePath, 'utf-8'); - currentFilePath = filePath; - mainWindow.setTitle(`MarkLite - ${path.basename(filePath)}`); - return { filePath, content }; + try { + const filePath = await showOpenDialog(); + if (filePath) { + const content = fs.readFileSync(filePath, 'utf-8'); + currentFilePath = filePath; + startWatching(filePath); + mainWindow.setTitle(`MarkLite - ${path.basename(filePath)}`); + return { filePath, content }; + } + return null; + } catch (err) { + return { error: err.message }; } - return null; }); ipcMain.handle('file:read', async (event, filePath) => { @@ -185,12 +130,13 @@ ipcMain.handle('file:read', async (event, filePath) => { ipcMain.handle('file:save', async (event, { filePath, content }) => { try { if (filePath) { + stopWatching(); fs.writeFileSync(filePath, content, 'utf-8'); currentFilePath = filePath; + startWatching(filePath); mainWindow.setTitle(`MarkLite - ${path.basename(filePath)}`); return { success: true, filePath }; } else { - // No file path, do save as const result = await dialog.showSaveDialog(mainWindow, { filters: [ { name: 'Markdown 文件', extensions: ['md'] }, @@ -198,8 +144,10 @@ ipcMain.handle('file:save', async (event, { filePath, content }) => { ] }); if (!result.canceled) { + stopWatching(); fs.writeFileSync(result.filePath, content, 'utf-8'); currentFilePath = result.filePath; + startWatching(result.filePath); mainWindow.setTitle(`MarkLite - ${path.basename(result.filePath)}`); return { success: true, filePath: result.filePath }; } @@ -219,8 +167,10 @@ ipcMain.handle('file:saveAs', async (event, { content }) => { ] }); if (!result.canceled) { + stopWatching(); fs.writeFileSync(result.filePath, content, 'utf-8'); currentFilePath = result.filePath; + startWatching(result.filePath); mainWindow.setTitle(`MarkLite - ${path.basename(result.filePath)}`); return { success: true, filePath: result.filePath }; } @@ -234,6 +184,36 @@ ipcMain.handle('file:getCurrentPath', () => { return currentFilePath; }); +ipcMain.handle('file:stats', async (event, filePath) => { + try { + const stats = fs.statSync(filePath); + return { + success: true, + size: stats.size, + mtime: stats.mtime.toISOString() + }; + } catch (err) { + return { success: false, error: err.message }; + } +}); + +ipcMain.handle('file:reload', async () => { + if (!currentFilePath) return { success: false, error: '没有打开的文件' }; + try { + const content = fs.readFileSync(currentFilePath, 'utf-8'); + isModifiedExternally = false; + return { success: true, content, filePath: currentFilePath }; + } catch (err) { + return { success: false, error: err.message }; + } +}); + +ipcMain.handle('window:forceClose', () => { + stopWatching(); + mainWindow.removeAllListeners('close'); + mainWindow.close(); +}); + // Handle file open from command line or file association function getCommandLineFile() { const args = process.argv.slice(1); @@ -248,28 +228,23 @@ function getCommandLineFile() { // App lifecycle app.whenReady().then(() => { - // Register open-file handler BEFORE creating window (fixes race condition) app.on('open-file', (event, filePath) => { event.preventDefault(); if (mainWindow && mainWindow.webContents.isLoading()) { - // Window is still loading, queue the file pendingFilePath = filePath; } else if (mainWindow) { openFile(filePath); } else { - // Window not created yet, store for later pendingFilePath = filePath; } }); createWindow(); - // Check for file passed via command line const fileToOpen = getCommandLineFile(); if (fileToOpen) { pendingFilePath = fileToOpen; } - // pendingFilePath will be opened by the did-finish-load handler in createWindow() }); app.on('window-all-closed', () => { diff --git a/preload.js b/preload.js index 29faeea..f043511 100644 --- a/preload.js +++ b/preload.js @@ -7,6 +7,11 @@ contextBridge.exposeInMainWorld('electronAPI', { saveFile: (data) => ipcRenderer.invoke('file:save', data), saveFileAs: (data) => ipcRenderer.invoke('file:saveAs', data), getCurrentPath: () => ipcRenderer.invoke('file:getCurrentPath'), + getFileStats: (filePath) => ipcRenderer.invoke('file:stats', filePath), + reloadFile: () => ipcRenderer.invoke('file:reload'), + + // Window control + forceClose: () => ipcRenderer.invoke('window:forceClose'), // Menu events onFileOpened: (callback) => ipcRenderer.on('file:opened', (event, data) => callback(data)), @@ -14,6 +19,13 @@ contextBridge.exposeInMainWorld('electronAPI', { onMenuSaveAs: (callback) => ipcRenderer.on('menu:saveAs', () => callback()), onViewModeChange: (callback) => ipcRenderer.on('menu:viewMode', (event, mode) => callback(mode)), + // File modification detection + onExternalModification: (callback) => ipcRenderer.on('file:externallyModified', (event, filePath) => callback(filePath)), + + // Window close confirmation + onConfirmClose: (callback) => ipcRenderer.on('window:confirmClose', () => callback()), + onClosing: (callback) => ipcRenderer.on('window:closing', () => callback()), + // Remove listeners removeAllListeners: (channel) => ipcRenderer.removeAllListeners(channel) }); diff --git a/renderer/index.html b/renderer/index.html index c65a81c..41cd540 100644 --- a/renderer/index.html +++ b/renderer/index.html @@ -3,7 +3,7 @@ - + MarkLite @@ -50,6 +50,31 @@ 预览 +
+ +
+ + + + @@ -81,6 +106,8 @@ | Markdown | + + | 行 1, 列 1 diff --git a/renderer/renderer.js b/renderer/renderer.js index 4d06a23..d0ebfb7 100644 --- a/renderer/renderer.js +++ b/renderer/renderer.js @@ -8,8 +8,11 @@ const lineNumbers = document.getElementById('line-numbers'); const welcomeScreen = document.getElementById('welcome-screen'); const dropOverlay = document.getElementById('drop-overlay'); + const modifiedBanner = document.getElementById('modified-banner'); const statusText = document.getElementById('status-text'); const statusCursor = document.getElementById('status-cursor'); + const statusSize = document.getElementById('status-size'); + const statusSizeDivider = document.querySelector('.status-size-divider'); const resizer = document.getElementById('resizer'); const editorPanel = document.getElementById('editor-panel'); const previewPanel = document.getElementById('preview-panel'); @@ -23,18 +26,58 @@ const btnPreview = document.getElementById('btn-preview'); const btnWelcomeOpen = document.getElementById('btn-welcome-open'); const btnWelcomeNew = document.getElementById('btn-welcome-new'); + const btnDark = document.getElementById('btn-dark'); + const btnReload = document.getElementById('btn-reload'); + const btnDismiss = document.getElementById('btn-dismiss'); // ===== State ===== let currentFilePath = null; let currentContent = ''; - let viewMode = 'split'; // split, editor, preview + let viewMode = 'split'; let isModified = false; let updateTimer = null; + let lineCountCache = 0; + + // ===== localStorage Helpers ===== + function loadSettings() { + try { + const s = JSON.parse(localStorage.getItem('marklite-settings') || '{}'); + return s; + } catch { return {}; } + } + + function saveSetting(key, value) { + try { + const s = loadSettings(); + s[key] = value; + localStorage.setItem('marklite-settings', JSON.stringify(s)); + } catch { /* ignore */ } + } + + // ===== Dark Mode ===== + function initDarkMode() { + const settings = loadSettings(); + const iconDark = document.getElementById('icon-dark'); + const iconLight = document.getElementById('icon-light'); + + function apply(isDark) { + document.documentElement.classList.toggle('dark', isDark); + iconDark.style.display = isDark ? 'none' : ''; + iconLight.style.display = isDark ? '' : 'none'; + } + + apply(!!settings.darkMode); + + btnDark.addEventListener('click', () => { + const isDark = !document.documentElement.classList.contains('dark'); + apply(isDark); + saveSetting('darkMode', isDark); + }); + } // ===== Initialize marked.js ===== function initMarked() { if (typeof marked !== 'undefined') { - // Custom renderer for code block highlighting (compatible with marked v12+) const renderer = new marked.Renderer(); renderer.code = function ({ text, lang }) { let highlighted = text; @@ -72,15 +115,17 @@ } } - // ===== Line Numbers ===== + // ===== Line Numbers (optimized: batch innerHTML) ===== function updateLineNumbers() { - const lines = editor.value.split('\n'); - const count = lines.length; - let html = ''; + const count = editor.value.split('\n').length; + if (count === lineCountCache) return; + lineCountCache = count; + + const fragment = []; for (let i = 1; i <= count; i++) { - html += '
' + i + '
'; + fragment.push('
', i, '
'); } - lineNumbers.innerHTML = html; + lineNumbers.innerHTML = fragment.join(''); } // ===== Cursor Position ===== @@ -94,25 +139,55 @@ statusCursor.textContent = `行 ${line}, 列 ${col}`; } - // ===== Sync Scroll (line-based for more accurate sync) ===== + // ===== File Size ===== + function updateFileSize() { + const bytes = new TextEncoder().encode(editor.value).length; + let display; + if (bytes < 1024) display = bytes + ' B'; + else if (bytes < 1024 * 1024) display = (bytes / 1024).toFixed(1) + ' KB'; + else display = (bytes / (1024 * 1024)).toFixed(1) + ' MB'; + statusSize.textContent = display; + statusSizeDivider.classList.add('visible'); + } + + // ===== Improved Scroll Sync (DOM position mapping) ===== + let previewElements = null; + let previewPositions = null; + + function cachePreviewPositions() { + previewElements = preview.children; + previewPositions = []; + for (let i = 0; i < previewElements.length; i++) { + previewPositions.push(previewElements[i].offsetTop); + } + } + function syncScroll() { if (viewMode !== 'split') return; - // Calculate which line is at the top of the editor viewport + // Map editor scroll to preview using line-to-DOM correlation const lineHeight = parseFloat(getComputedStyle(editor).lineHeight) || 20.8; const scrollTop = editor.scrollTop; const topLine = Math.floor(scrollTop / lineHeight); - - // Calculate corresponding scroll position in preview - const previewChildren = preview.children; - if (!previewChildren.length) return; - - // Find the element that corresponds to the current top line - // Approximate: map editor line ratio to preview element positions const totalLines = editor.value.split('\n').length; - const ratio = totalLines > 0 ? topLine / totalLines : 0; - const targetScrollTop = ratio * (previewPanel.scrollHeight - previewPanel.clientHeight); - previewPanel.scrollTop = targetScrollTop; + + if (!previewPositions || previewPositions.length === 0) { + cachePreviewPositions(); + } + + if (!previewPositions || previewPositions.length === 0) return; + + // Find which preview element corresponds to the current editor line + // Use a rough heuristic: map line ratio to element ratio + const ratio = totalLines > 1 ? topLine / (totalLines - 1) : 0; + const targetIndex = Math.min( + Math.floor(ratio * previewPositions.length), + previewPositions.length - 1 + ); + + if (targetIndex >= 0 && previewPositions[targetIndex] !== undefined) { + previewPanel.scrollTop = previewPositions[targetIndex]; + } } // ===== Update Preview ===== @@ -122,6 +197,8 @@ const text = editor.value; renderMarkdown(text); updateLineNumbers(); + updateFileSize(); + cachePreviewPositions(); }, 150); } @@ -138,35 +215,55 @@ return filePath.split(/[/\\]/).pop(); } + function formatBytes(bytes) { + if (bytes < 1024) return bytes + ' B'; + if (bytes < 1024 * 1024) return (bytes / 1024).toFixed(1) + ' KB'; + return (bytes / (1024 * 1024)).toFixed(1) + ' MB'; + } + + async function updateFileStats(filePath) { + if (typeof window.electronAPI !== 'undefined' && filePath) { + const stats = await window.electronAPI.getFileStats(filePath); + if (stats.success) { + statusSize.textContent = formatBytes(stats.size); + statusSizeDivider.classList.add('visible'); + } + } + } + function setEditorContent(text, filePath) { editor.value = text; currentContent = text; currentFilePath = filePath || null; isModified = false; + lineCountCache = 0; updateLineNumbers(); renderMarkdown(text); updateCursorPosition(); + updateFileSize(); + cachePreviewPositions(); if (filePath) { document.title = `MarkLite - ${getFileName(filePath)}`; statusText.textContent = getFileName(filePath); + updateFileStats(filePath); } else { document.title = 'MarkLite - 未命名'; statusText.textContent = '未命名'; + statusSize.textContent = ''; + statusSizeDivider.classList.remove('visible'); } - // Hide welcome screen welcomeScreen.classList.add('hidden'); } async function handleOpenFile() { if (typeof window.electronAPI !== 'undefined') { const result = await window.electronAPI.openFile(); - if (result) { + if (result && !result.error) { setEditorContent(result.content, result.filePath); } } else { - // Fallback: use file input const input = document.createElement('input'); input.type = 'file'; input.accept = '.md,.markdown,.txt'; @@ -200,7 +297,6 @@ }, 2000); } } else { - // Fallback: download file const blob = new Blob([editor.value], { type: 'text/markdown' }); const a = document.createElement('a'); a.href = URL.createObjectURL(blob); @@ -230,15 +326,13 @@ } } - // ===== View Mode ===== + // ===== View Mode (with localStorage) ===== function setViewMode(mode) { viewMode = mode; const app = document.getElementById('app'); - // Remove all mode classes app.classList.remove('mode-editor', 'mode-preview'); - // Update button states btnSplit.classList.remove('active'); btnEditor.classList.remove('active'); btnPreview.classList.remove('active'); @@ -254,16 +348,25 @@ case 'preview': btnPreview.classList.add('active'); app.classList.add('mode-preview'); - // Update preview when switching to preview mode renderMarkdown(editor.value); break; } + + saveSetting('viewMode', mode); } - // ===== Resizer ===== + // ===== Resizer (with localStorage) ===== let isResizing = false; function initResizer() { + // Restore saved ratio + const settings = loadSettings(); + if (settings.splitRatio) { + const ratio = settings.splitRatio; + editorPanel.style.flex = `0 0 ${ratio}%`; + previewPanel.style.flex = `0 0 ${100 - ratio}%`; + } + resizer.addEventListener('mousedown', (e) => { isResizing = true; resizer.classList.add('active'); @@ -279,6 +382,7 @@ const clamped = Math.max(20, Math.min(80, percentage)); editorPanel.style.flex = `0 0 ${clamped}%`; previewPanel.style.flex = `0 0 ${100 - clamped}%`; + saveSetting('splitRatio', clamped); }); document.addEventListener('mouseup', () => { @@ -325,10 +429,9 @@ const files = e.dataTransfer.files; if (files.length > 0) { const file = files[0]; - const filePath = file.path; // Electron provides the real file path + const filePath = file.path; if (filePath && typeof window.electronAPI !== 'undefined') { - // Use IPC to read file through main process (tracks currentFilePath) const result = await window.electronAPI.readFile(filePath); if (result.success) { setEditorContent(result.content, filePath); @@ -336,7 +439,6 @@ statusText.textContent = '打开失败: ' + result.error; } } else { - // Fallback: use FileReader (browser mode) const reader = new FileReader(); reader.onload = (ev) => { setEditorContent(ev.target.result, file.name); @@ -350,32 +452,26 @@ // ===== Keyboard Shortcuts ===== function initKeyboard() { document.addEventListener('keydown', (e) => { - // Ctrl+O: Open if (e.ctrlKey && e.key === 'o') { e.preventDefault(); handleOpenFile(); } - // Ctrl+S: Save if (e.ctrlKey && e.key === 's' && !e.shiftKey) { e.preventDefault(); handleSave(); } - // Ctrl+Shift+S: Save As if (e.ctrlKey && e.shiftKey && e.key === 'S') { e.preventDefault(); handleSaveAs(); } - // Ctrl+1: Split view if (e.ctrlKey && e.key === '1') { e.preventDefault(); setViewMode('split'); } - // Ctrl+2: Editor only if (e.ctrlKey && e.key === '2') { e.preventDefault(); setViewMode('editor'); } - // Ctrl+3: Preview only if (e.ctrlKey && e.key === '3') { e.preventDefault(); setViewMode('preview'); @@ -383,14 +479,12 @@ }); } - // ===== Tab key support in editor (preserves undo history) ===== + // ===== Tab key support ===== function initEditorTab() { editor.addEventListener('keydown', (e) => { if (e.key === 'Tab') { e.preventDefault(); - // Use insertText command to preserve undo/redo history if (!document.execCommand('insertText', false, ' ')) { - // Fallback for environments where execCommand is unsupported const start = editor.selectionStart; const end = editor.selectionEnd; editor.setRangeText(' ', start, end, 'end'); @@ -400,36 +494,88 @@ }); } + // ===== External File Modification ===== + function initFileWatch() { + if (typeof window.electronAPI === 'undefined') return; + + window.electronAPI.onExternalModification((filePath) => { + modifiedBanner.classList.remove('hidden'); + }); + + btnReload.addEventListener('click', async () => { + const result = await window.electronAPI.reloadFile(); + if (result.success) { + editor.value = result.content; + currentContent = result.content; + currentFilePath = result.filePath; + isModified = false; + lineCountCache = 0; + updateLineNumbers(); + renderMarkdown(result.content); + updateCursorPosition(); + updateFileSize(); + cachePreviewPositions(); + } + modifiedBanner.classList.add('hidden'); + }); + + btnDismiss.addEventListener('click', () => { + modifiedBanner.classList.add('hidden'); + }); + } + + // ===== Unsaved Changes Warning ===== + function initUnsavedWarning() { + if (typeof window.electronAPI === 'undefined') { + // Browser fallback + window.addEventListener('beforeunload', (e) => { + if (isModified) { + e.preventDefault(); + e.returnValue = ''; + } + }); + return; + } + + // Electron: handle close confirmation from main process + window.electronAPI.onConfirmClose(() => { + if (!isModified) { + window.electronAPI.forceClose(); + return; + } + + // Show a simple confirm dialog + const shouldClose = confirm('文件尚未保存,确定要关闭吗?'); + if (shouldClose) { + window.electronAPI.forceClose(); + } + }); + } + // ===== Event Listeners ===== function initEventListeners() { - // Editor input editor.addEventListener('input', () => { setModified(true); scheduleUpdate(); }); - // Editor scroll sync editor.addEventListener('scroll', syncScroll); - // Editor cursor position editor.addEventListener('click', updateCursorPosition); editor.addEventListener('keyup', updateCursorPosition); editor.addEventListener('select', updateCursorPosition); - // Toolbar buttons btnOpen.addEventListener('click', handleOpenFile); btnSave.addEventListener('click', handleSave); btnSplit.addEventListener('click', () => setViewMode('split')); btnEditor.addEventListener('click', () => setViewMode('editor')); btnPreview.addEventListener('click', () => setViewMode('preview')); - // Welcome buttons btnWelcomeOpen.addEventListener('click', handleOpenFile); btnWelcomeNew.addEventListener('click', () => { setEditorContent('', null); }); - // Electron IPC events if (typeof window.electronAPI !== 'undefined') { window.electronAPI.onFileOpened((data) => { setEditorContent(data.content, data.filePath); @@ -452,16 +598,21 @@ // ===== Initialize ===== function init() { initMarked(); + initDarkMode(); initResizer(); initDragDrop(); initKeyboard(); initEditorTab(); initEventListeners(); + initFileWatch(); + initUnsavedWarning(); updateLineNumbers(); - setViewMode('split'); + + // Restore saved view mode + const settings = loadSettings(); + setViewMode(settings.viewMode || 'split'); } - // Run when DOM is ready if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', init); } else { diff --git a/renderer/style.css b/renderer/style.css index cc0b971..b96adac 100644 --- a/renderer/style.css +++ b/renderer/style.css @@ -27,6 +27,24 @@ --font-mono: "Cascadia Code", "Fira Code", "JetBrains Mono", Consolas, "Courier New", monospace; } +/* ===== Dark Theme ===== */ +:root.dark { + --primary: #8ab4f8; + --primary-light: #1a3a5c; + --primary-dark: #aecbfa; + --bg: #1e1e1e; + --bg-secondary: #252526; + --bg-tertiary: #2d2d2d; + --text: #d4d4d4; + --text-secondary: #9e9e9e; + --text-tertiary: #6e6e6e; + --border: #3e3e3e; + --border-light: #333333; + --code-bg: #2d2d2d; + --shadow: 0 1px 3px rgba(0, 0, 0, 0.3); + --shadow-lg: 0 4px 12px rgba(0, 0, 0, 0.4); +} + html, body { height: 100%; font-family: var(--font-ui); @@ -50,12 +68,14 @@ html, body { border-bottom: 1px solid var(--border); display: flex; align-items: center; + justify-content: space-between; padding: 0 8px; flex-shrink: 0; z-index: 10; } -.toolbar-left { +.toolbar-left, +.toolbar-right { display: flex; align-items: center; gap: 2px; @@ -98,6 +118,55 @@ html, body { margin: 0 6px; } +/* ===== Modified Banner ===== */ +#modified-banner { + display: flex; + align-items: center; + gap: 12px; + padding: 6px 12px; + background: #fff3cd; + border-bottom: 1px solid #ffc107; + font-size: 13px; + color: #856404; + flex-shrink: 0; +} + +:root.dark #modified-banner { + background: #3a3000; + border-bottom-color: #665500; + color: #ffd54f; +} + +#modified-banner.hidden { + display: none; +} + +.banner-btn { + padding: 2px 10px; + border: 1px solid #ffc107; + border-radius: 4px; + background: transparent; + color: #856404; + font-size: 12px; + cursor: pointer; + font-family: var(--font-ui); +} + +:root.dark .banner-btn { + border-color: #665500; + color: #ffd54f; +} + +.banner-btn:hover { + background: #ffc107; + color: #000; +} + +:root.dark .banner-btn:hover { + background: #665500; + color: #fff; +} + /* ===== Main Content ===== */ #main-content { flex: 1; @@ -298,6 +367,10 @@ html, body { color: #e83e8c; } +:root.dark .markdown-body code { + color: #f48fb1; +} + .markdown-body pre { margin-top: 0; margin-bottom: 16px; @@ -370,6 +443,14 @@ html, body { color: var(--border); } +.status-size-divider { + display: none; +} + +.status-size-divider.visible { + display: inline; +} + /* ===== Drop Overlay ===== */ #drop-overlay { position: fixed;