From 543af93802fccf0004066f4a65ec8332ca301cae Mon Sep 17 00:00:00 2001 From: thzxx Date: Thu, 21 May 2026 11:37:02 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20v1.2.0=20=E6=90=9C=E7=B4=A2=E6=9B=BF?= =?UTF-8?q?=E6=8D=A2=20+=20=E6=96=87=E4=BB=B6=E6=A0=91=E4=BE=A7=E8=BE=B9?= =?UTF-8?q?=E6=A0=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 2 +- main.js | 82 +++++++++ package.json | 2 +- preload.js | 7 + renderer/index.html | 34 ++++ renderer/renderer.js | 402 +++++++++++++++++++++++++++++++++++++++++++ renderer/style.css | 270 +++++++++++++++++++++++++++++ 7 files changed, 797 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index f9966bd..7c64e20 100644 --- a/README.md +++ b/README.md @@ -12,7 +12,7 @@ Platform Electron License - Version + Version

diff --git a/main.js b/main.js index ff8e35a..47cc4e9 100644 --- a/main.js +++ b/main.js @@ -305,6 +305,88 @@ ipcMain.handle('file:reload', async () => { } }); +// ===== File Tree (Sidebar) ===== +const SKIP_DIRS = new Set(['node_modules', '.git', '.svn', '.hg', 'dist', 'out', '.next', '.nuxt', '__pycache__', '.DS_Store']); +const MARKDOWN_EXTS = new Set(['.md', '.markdown', '.txt']); + +async function buildDirTree(dirPath) { + const entries = await fs.promises.readdir(dirPath, { withFileTypes: true }); + // Sort: directories first, then files, alphabetically + entries.sort((a, b) => { + if (a.isDirectory() && !b.isDirectory()) return -1; + if (!a.isDirectory() && b.isDirectory()) return 1; + return a.name.localeCompare(b.name); + }); + + const children = []; + for (const entry of entries) { + if (SKIP_DIRS.has(entry.name)) continue; + if (entry.name.startsWith('.')) continue; + + const childPath = path.join(dirPath, entry.name); + if (entry.isDirectory()) { + const subChildren = await buildDirTree(childPath); + if (subChildren.length > 0) { + children.push({ name: entry.name, path: childPath, type: 'dir', children: subChildren }); + } + } else { + const ext = path.extname(entry.name).toLowerCase(); + if (MARKDOWN_EXTS.has(ext)) { + children.push({ name: entry.name, path: childPath, type: 'file' }); + } + } + } + return children; +} + +ipcMain.handle('dir:readTree', async (event, dirPath) => { + try { + const tree = await buildDirTree(dirPath); + return { success: true, tree, rootPath: dirPath }; + } catch (err) { + return { success: false, error: err.message }; + } +}); + +let sidebarWatcher = null; +let sidebarWatchPath = null; + +function startSidebarWatching(dirPath) { + stopSidebarWatching(); + if (!dirPath) return; + try { + sidebarWatchPath = dirPath; + sidebarWatcher = fs.watch(dirPath, { recursive: true }, (eventType, filename) => { + if (mainWindow && !mainWindow.isDestroyed()) { + mainWindow.webContents.send('sidebar:dirChanged'); + } + }); + } catch (err) { /* ignore */ } +} + +function stopSidebarWatching() { + if (sidebarWatcher) { sidebarWatcher.close(); sidebarWatcher = null; } + sidebarWatchPath = null; +} + +ipcMain.handle('dir:watch', (event, dirPath) => { + startSidebarWatching(dirPath); +}); + +ipcMain.handle('dir:unwatch', () => { + stopSidebarWatching(); +}); + +ipcMain.handle('dir:openDialog', async () => { + const result = await dialog.showOpenDialog(mainWindow, { + properties: ['openDirectory'] + }); + if (!result.canceled && result.filePaths.length > 0) { + return result.filePaths[0]; + } + return null; +}); + // Tab switched - update active file tracking ipcMain.handle('tab:switched', (event, filePath) => { switchActiveFile(filePath); diff --git a/package.json b/package.json index 7f8ecd0..98ec914 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "marklite", - "version": "1.1.0", + "version": "1.2.0", "description": "Lightweight Markdown Reader for Windows", "main": "main.js", "scripts": { diff --git a/preload.js b/preload.js index ac4d16f..352131e 100644 --- a/preload.js +++ b/preload.js @@ -20,12 +20,19 @@ contextBridge.exposeInMainWorld('electronAPI', { // Shell openExternal: (url) => shell.openExternal(url), + // File Tree (Sidebar) + readDirTree: (dirPath) => ipcRenderer.invoke('dir:readTree', dirPath), + openFolderDialog: () => ipcRenderer.invoke('dir:openDialog'), + watchDir: (dirPath) => ipcRenderer.invoke('dir:watch', dirPath), + unwatchDir: () => ipcRenderer.invoke('dir:unwatch'), + // Events from main process onFileOpenInTab: (callback) => ipcRenderer.on('file:openInTab', (event, data) => callback(data)), onMenuSave: (callback) => ipcRenderer.on('menu:save', () => callback()), onMenuSaveAs: (callback) => ipcRenderer.on('menu:saveAs', () => callback()), onViewModeChange: (callback) => ipcRenderer.on('menu:viewMode', (event, mode) => callback(mode)), onExternalModification: (callback) => ipcRenderer.on('file:externallyModified', (event, filePath) => callback(filePath)), + onDirChanged: (callback) => ipcRenderer.on('sidebar:dirChanged', () => callback()), onConfirmClose: (callback) => ipcRenderer.on('window:confirmClose', () => callback()), // Remove listeners diff --git a/renderer/index.html b/renderer/index.html index 9653b66..a3d3f5e 100644 --- a/renderer/index.html +++ b/renderer/index.html @@ -91,10 +91,44 @@ + +

+
+ +
diff --git a/renderer/renderer.js b/renderer/renderer.js index 1342170..21175e7 100644 --- a/renderer/renderer.js +++ b/renderer/renderer.js @@ -19,6 +19,25 @@ const mainContent = document.getElementById('main-content'); const tabList = document.getElementById('tab-list'); + // Sidebar + const sidebar = document.getElementById('sidebar'); + const sidebarTree = document.getElementById('sidebar-tree'); + const btnOpenFolder = document.getElementById('btn-open-folder'); + + // Search + const searchBar = document.getElementById('search-bar'); + const searchInput = document.getElementById('search-input'); + const replaceInput = document.getElementById('replace-input'); + const searchCount = document.getElementById('search-count'); + const replaceRow = document.getElementById('replace-row'); + const btnCase = document.getElementById('btn-case'); + const btnRegex = document.getElementById('btn-regex'); + const btnNext = document.getElementById('btn-next'); + const btnPrev = document.getElementById('btn-prev'); + const btnSearchClose = document.getElementById('btn-search-close'); + const btnReplace = document.getElementById('btn-replace'); + const btnReplaceAll = document.getElementById('btn-replace-all'); + // Buttons const btnOpen = document.getElementById('btn-open'); const btnSave = document.getElementById('btn-save'); @@ -41,6 +60,13 @@ let viewMode = 'split'; let updateTimer = null; let lineCountCache = 0; + let currentRootPath = null; + + // Search state + let searchMatches = []; + let searchIndex = -1; + let searchCaseSensitive = false; + let searchUseRegex = false; // ===== Tab Object ===== function createTab(filePath, content) { @@ -889,6 +915,18 @@ switchToTab(tabs[next].id); } } + // Search & Replace + if (e.ctrlKey && e.key === 'f') { e.preventDefault(); openSearch(false); } + if (e.ctrlKey && e.key === 'h') { e.preventDefault(); openSearch(true); } + if (e.key === 'Escape' && !searchBar.classList.contains('hidden')) { e.preventDefault(); closeSearch(); } + if (e.key === 'Enter' && document.activeElement === searchInput) { + e.preventDefault(); + e.shiftKey ? findPrev() : findNext(); + } + if (e.altKey && e.key === 'c') { e.preventDefault(); toggleSearchOption('case'); } + if (e.altKey && e.key === 'r') { e.preventDefault(); toggleSearchOption('regex'); } + if (e.ctrlKey && e.shiftKey && e.key === 'H') { e.preventDefault(); replaceAll(); } + if (e.ctrlKey && e.shiftKey && e.key === 'G') { e.preventDefault(); replaceCurrent(); } }); } @@ -1027,6 +1065,368 @@ } } + // ===== Search & Replace ===== + function openSearch(showReplace) { + searchBar.classList.remove('hidden'); + replaceRow.classList.toggle('hidden', !showReplace); + searchInput.focus(); + // Pre-fill with selected text + const selected = editor.value.substring(editor.selectionStart, editor.selectionEnd); + if (selected && !selected.includes('\n')) { + searchInput.value = selected; + } + doSearch(); + } + + function closeSearch() { + searchBar.classList.add('hidden'); + clearHighlights(); + searchMatches = []; + searchIndex = -1; + searchCount.textContent = ''; + editor.focus(); + } + + function toggleSearchOption(type) { + if (type === 'case') { + searchCaseSensitive = !searchCaseSensitive; + btnCase.classList.toggle('active', searchCaseSensitive); + } else { + searchUseRegex = !searchUseRegex; + btnRegex.classList.toggle('active', searchUseRegex); + } + doSearch(); + } + + function doSearch() { + clearHighlights(); + const text = searchInput.value; + if (!text) { + searchMatches = []; + searchIndex = -1; + searchCount.textContent = ''; + return; + } + searchMatches = findMatches(text); + if (searchMatches.length > 0) { + // Find the match closest to cursor + const cursor = editor.selectionStart; + searchIndex = searchMatches.findIndex(m => m.start >= cursor); + if (searchIndex === -1) searchIndex = 0; + scrollToMatch(searchIndex); + } else { + searchIndex = -1; + } + searchCount.textContent = searchMatches.length > 0 + ? `${searchIndex + 1}/${searchMatches.length}` + : '无结果'; + highlightMatches(); + } + + function findMatches(text) { + const content = editor.value; + const matches = []; + if (searchUseRegex) { + try { + const flags = searchCaseSensitive ? 'g' : 'gi'; + const regex = new RegExp(text, flags); + let m; + while ((m = regex.exec(content)) !== null) { + matches.push({ start: m.index, end: m.index + m[0].length }); + if (m[0].length === 0) regex.lastIndex++; + } + } catch { /* invalid regex */ } + } else { + const haystack = searchCaseSensitive ? content : content.toLowerCase(); + const needle = searchCaseSensitive ? text : text.toLowerCase(); + let idx = 0; + while ((idx = haystack.indexOf(needle, idx)) !== -1) { + matches.push({ start: idx, end: idx + needle.length }); + idx += needle.length; + } + } + return matches; + } + + function highlightMatches() { + clearHighlights(); + if (searchMatches.length === 0) return; + + const content = editor.value; + const lineHeight = parseFloat(getComputedStyle(editor).lineHeight) || 20.8; + const editorStyle = getComputedStyle(editor); + const paddingTop = parseFloat(editorStyle.paddingTop); + const paddingLeft = parseFloat(editorStyle.paddingLeft); + + // Compute line starts for fast lookup + const lineStarts = [0]; + for (let i = 0; i < content.length; i++) { + if (content.charCodeAt(i) === 10) lineStarts.push(i + 1); + } + + function posToXY(pos) { + let line = 0; + for (let i = 0; i < lineStarts.length; i++) { + if (lineStarts[i] > pos) break; + line = i; + } + const col = pos - lineStarts[line]; + return { + top: line * lineHeight + paddingTop, + left: col * 8 + paddingLeft + }; + } + + const overlay = document.createElement('div'); + overlay.id = 'search-highlights'; + overlay.style.cssText = 'position:absolute;top:0;left:0;right:0;bottom:0;pointer-events:none;overflow:hidden;z-index:1;'; + editorWrapper.style.position = 'relative'; + editorWrapper.appendChild(overlay); + + for (let i = 0; i < searchMatches.length; i++) { + const m = searchMatches[i]; + const startPos = posToXY(m.start); + const endPos = posToXY(m.end); + const isCurrent = i === searchIndex; + const div = document.createElement('div'); + div.className = 'search-hl' + (isCurrent ? ' current' : ''); + div.style.cssText = `position:absolute;top:${startPos.top}px;left:${startPos.left}px;height:${lineHeight}px;width:${Math.max(endPos.left - startPos.left, 8)}px;`; + overlay.appendChild(div); + } + } + + function clearHighlights() { + const old = document.getElementById('search-highlights'); + if (old) old.remove(); + } + + function scrollToMatch(index) { + if (index < 0 || index >= searchMatches.length) return; + searchIndex = index; + const m = searchMatches[index]; + editor.setSelectionRange(m.start, m.end); + // Scroll the match into view + const lineHeight = parseFloat(getComputedStyle(editor).lineHeight) || 20.8; + const content = editor.value; + let line = 0; + for (let i = 0; i < m.start; i++) { + if (content.charCodeAt(i) === 10) line++; + } + const matchTop = line * lineHeight; + const viewTop = editor.scrollTop; + const viewBottom = viewTop + editor.clientHeight; + if (matchTop < viewTop + 40 || matchTop > viewBottom - 40) { + editor.scrollTop = matchTop - editor.clientHeight / 2; + } + searchCount.textContent = `${index + 1}/${searchMatches.length}`; + highlightMatches(); + } + + function findNext() { + if (searchMatches.length === 0) return; + scrollToMatch((searchIndex + 1) % searchMatches.length); + } + + function findPrev() { + if (searchMatches.length === 0) return; + scrollToMatch((searchIndex - 1 + searchMatches.length) % searchMatches.length); + } + + function replaceCurrent() { + if (searchMatches.length === 0 || searchIndex < 0) return; + const m = searchMatches[searchIndex]; + const replacement = replaceInput.value; + // Use execCommand to preserve undo history + editor.setSelectionRange(m.start, m.end); + document.execCommand('insertText', false, replacement); + // Re-run search + doSearch(); + } + + function replaceAll() { + if (searchMatches.length === 0) return; + const replacement = replaceInput.value; + // Replace from end to start to preserve positions + for (let i = searchMatches.length - 1; i >= 0; i--) { + const m = searchMatches[i]; + editor.setSelectionRange(m.start, m.end); + document.execCommand('insertText', false, replacement); + } + doSearch(); + } + + // ===== Sidebar (File Tree) ===== + function initSidebar() { + const settings = loadSettings(); + if (settings.sidebarCollapsed) sidebar.classList.add('collapsed'); + + btnOpenFolder.addEventListener('click', openFolder); + if (typeof window.electronAPI !== 'undefined') { + window.electronAPI.onDirChanged(() => { + if (currentRootPath) refreshTree(); + }); + } + } + + async function openFolder() { + if (typeof window.electronAPI === 'undefined') return; + const { dialog } = require('electron'); + openFolderDialog(); + } + + async function openFolderDialog() { + // We'll add a dir:openDialog IPC + if (typeof window.electronAPI !== 'undefined' && window.electronAPI.openFolderDialog) { + const result = await window.electronAPI.openFolderDialog(); + if (result) { + loadFolder(result); + } + } + } + + async function loadFolder(dirPath) { + if (typeof window.electronAPI === 'undefined') return; + currentRootPath = dirPath; + const result = await window.electronAPI.readDirTree(dirPath); + if (result.success) { + renderTree(result.tree, dirPath); + sidebar.classList.remove('collapsed'); + window.electronAPI.watchDir(dirPath); + } + } + + async function refreshTree() { + if (!currentRootPath) return; + const result = await window.electronAPI.readDirTree(currentRootPath); + if (result.success) { + renderTree(result.tree, currentRootPath); + } + } + + // Track expanded state + const expandedDirs = new Set(); + + function renderTree(tree, rootPath) { + sidebarTree.innerHTML = ''; + const rootNode = { name: path.basename(rootPath) || rootPath, path: rootPath, type: 'dir', children: tree }; + expandedDirs.add(rootPath); + const fragment = document.createDocumentFragment(); + buildTreeDOM(fragment, rootNode, 0); + sidebarTree.appendChild(fragment); + } + + function pathBasename(p) { + return p.split(/[/\\]/).pop(); + } + + function buildTreeDOM(parent, node, depth) { + const item = document.createElement('div'); + item.className = 'tree-item'; + item.style.paddingLeft = (8 + depth * 16) + 'px'; + + if (node.type === 'dir') { + const isExpanded = expandedDirs.has(node.path); + const arrow = document.createElement('span'); + arrow.className = 'tree-arrow' + (isExpanded ? ' expanded' : ''); + arrow.innerHTML = ''; + item.appendChild(arrow); + + const icon = document.createElement('span'); + icon.className = 'tree-icon'; + icon.innerHTML = ''; + item.appendChild(icon); + + const name = document.createElement('span'); + name.className = 'tree-name'; + name.textContent = node.name; + item.appendChild(name); + + parent.appendChild(item); + + if (isExpanded && node.children) { + for (const child of node.children) { + buildTreeDOM(parent, child, depth + 1); + } + } + + item.addEventListener('click', (e) => { + e.stopPropagation(); + if (expandedDirs.has(node.path)) { + expandedDirs.delete(node.path); + } else { + expandedDirs.add(node.path); + } + renderTree(getTreeData(), currentRootPath); + }); + } else { + // Spacer for alignment (no arrow) + const spacer = document.createElement('span'); + spacer.style.width = '16px'; + spacer.style.flexShrink = '0'; + item.appendChild(spacer); + + const icon = document.createElement('span'); + icon.className = 'tree-icon'; + icon.innerHTML = ''; + item.appendChild(icon); + + const name = document.createElement('span'); + name.className = 'tree-name'; + name.textContent = node.name; + item.appendChild(name); + + // Highlight if this file is open in active tab + const tab = getActiveTab(); + if (tab && tab.filePath === node.path) { + item.classList.add('active'); + } + + parent.appendChild(item); + + item.addEventListener('click', async (e) => { + e.stopPropagation(); + // Check if already open + const existing = tabs.find(t => t.filePath === node.path); + if (existing) { + switchToTab(existing.id); + } else { + if (typeof window.electronAPI !== 'undefined') { + const result = await window.electronAPI.readFile(node.path); + if (result.success) { + createNewTab(node.path, result.content); + } + } + } + }); + } + } + + // Cache the last tree data for re-render + let lastTreeData = null; + + function getTreeData() { + return lastTreeData || []; + } + + // Override renderTree to cache data + const _originalRenderTree = renderTree; + renderTree = function(tree, rootPath) { + lastTreeData = tree; + _originalRenderTree(tree, rootPath); + }; + + // ===== Search Bar Init ===== + function initSearchBar() { + searchInput.addEventListener('input', doSearch); + btnCase.addEventListener('click', () => toggleSearchOption('case')); + btnRegex.addEventListener('click', () => toggleSearchOption('regex')); + btnNext.addEventListener('click', findNext); + btnPrev.addEventListener('click', findPrev); + btnSearchClose.addEventListener('click', closeSearch); + btnReplace.addEventListener('click', replaceCurrent); + btnReplaceAll.addEventListener('click', replaceAll); + } + // ===== Initialize ===== function init() { initMarked(); @@ -1039,6 +1439,8 @@ initEventListeners(); initFileWatch(); initUnsavedWarning(); + initSidebar(); + initSearchBar(); const settings = loadSettings(); setViewMode(settings.viewMode || 'split'); diff --git a/renderer/style.css b/renderer/style.css index aa8bac4..0ac9930 100644 --- a/renderer/style.css +++ b/renderer/style.css @@ -23,6 +23,13 @@ --radius: 6px; --toolbar-height: 44px; --statusbar-height: 28px; + --sidebar-width: 240px; + --sidebar-bg: var(--bg-secondary); + --sidebar-border: var(--border); + --sidebar-hover: var(--bg-tertiary); + --sidebar-active: var(--primary-light); + --search-bg: var(--bg-secondary); + --search-border: var(--border); --font-ui: system-ui, -apple-system, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif; --font-mono: "Cascadia Code", "Fira Code", "JetBrains Mono", Consolas, "Courier New", monospace; } @@ -713,6 +720,269 @@ html, body { margin-bottom: 0; } +/* ===== Sidebar ===== */ +#sidebar { + width: var(--sidebar-width); + min-width: 180px; + max-width: 500px; + background: var(--sidebar-bg); + border-right: 1px solid var(--sidebar-border); + display: flex; + flex-direction: column; + flex-shrink: 0; + overflow: hidden; + user-select: none; +} + +#sidebar.collapsed { + display: none; +} + +#sidebar-header { + display: flex; + align-items: center; + justify-content: space-between; + padding: 8px 12px; + border-bottom: 1px solid var(--sidebar-border); + flex-shrink: 0; +} + +#sidebar-title { + font-size: 12px; + font-weight: 600; + color: var(--text-secondary); + text-transform: uppercase; + letter-spacing: 0.5px; +} + +.sidebar-header-btn { + display: flex; + align-items: center; + justify-content: center; + width: 24px; + height: 24px; + border: none; + background: transparent; + color: var(--text-secondary); + border-radius: 4px; + cursor: pointer; + transition: all 0.15s ease; +} + +.sidebar-header-btn:hover { + background: var(--sidebar-hover); + color: var(--text); +} + +#sidebar-tree { + flex: 1; + overflow-y: auto; + overflow-x: hidden; + padding: 4px 0; +} + +.tree-item { + display: flex; + align-items: center; + padding: 3px 8px 3px 0; + cursor: pointer; + font-size: 13px; + color: var(--text); + white-space: nowrap; + transition: background 0.1s ease; + border-radius: 0; +} + +.tree-item:hover { + background: var(--sidebar-hover); +} + +.tree-item.active { + background: var(--sidebar-active); + color: var(--primary); + font-weight: 500; +} + +.tree-indent { + flex-shrink: 0; +} + +.tree-arrow { + display: flex; + align-items: center; + justify-content: center; + width: 16px; + height: 16px; + flex-shrink: 0; + color: var(--text-tertiary); + transition: transform 0.15s ease; +} + +.tree-arrow.expanded { + transform: rotate(90deg); +} + +.tree-icon { + display: flex; + align-items: center; + justify-content: center; + width: 16px; + height: 16px; + flex-shrink: 0; + margin-right: 4px; +} + +.tree-icon svg { + width: 14px; + height: 14px; +} + +.tree-name { + overflow: hidden; + text-overflow: ellipsis; +} + +/* ===== Search & Replace Bar ===== */ +#search-bar { + background: var(--search-bg); + border-bottom: 1px solid var(--search-border); + padding: 6px 10px; + flex-shrink: 0; +} + +#search-bar.hidden { + display: none; +} + +.search-row, +#replace-row { + display: flex; + align-items: center; + gap: 4px; +} + +#replace-row { + margin-top: 4px; +} + +#replace-row.hidden { + display: none; +} + +#search-input, +#replace-input { + flex: 1; + min-width: 0; + height: 26px; + padding: 0 8px; + border: 1px solid var(--border); + border-radius: 4px; + background: var(--bg); + color: var(--text); + font-size: 13px; + font-family: var(--font-ui); + outline: none; + transition: border-color 0.15s ease; +} + +#search-input:focus, +#replace-input:focus { + border-color: var(--primary); +} + +#search-count { + font-size: 11px; + color: var(--text-tertiary); + white-space: nowrap; + min-width: 40px; + text-align: center; +} + +.search-opt-btn { + display: flex; + align-items: center; + justify-content: center; + min-width: 26px; + height: 26px; + padding: 0 4px; + border: 1px solid var(--border); + border-radius: 4px; + background: transparent; + color: var(--text-secondary); + font-size: 11px; + font-weight: 600; + font-family: var(--font-mono); + cursor: pointer; + transition: all 0.15s ease; +} + +.search-opt-btn:hover { + background: var(--bg-tertiary); +} + +.search-opt-btn.active { + background: var(--primary-light); + color: var(--primary); + border-color: var(--primary); +} + +.search-nav-btn { + display: flex; + align-items: center; + justify-content: center; + width: 26px; + height: 26px; + border: none; + background: transparent; + color: var(--text-secondary); + border-radius: 4px; + cursor: pointer; + font-size: 14px; + transition: all 0.15s ease; +} + +.search-nav-btn:hover { + background: var(--bg-tertiary); + color: var(--text); +} + +.replace-btn { + height: 26px; + padding: 0 10px; + border: 1px solid var(--border); + border-radius: 4px; + background: transparent; + color: var(--text-secondary); + font-size: 12px; + font-family: var(--font-ui); + cursor: pointer; + white-space: nowrap; + transition: all 0.15s ease; +} + +.replace-btn:hover { + background: var(--bg-tertiary); + color: var(--text); +} + +/* ===== Search Highlight Overlays ===== */ +.search-hl { + background: rgba(255, 220, 0, 0.35); + border-radius: 2px; +} + +.search-hl.current { + background: rgba(255, 150, 0, 0.55); +} + +:root.dark .search-hl { + background: rgba(255, 220, 0, 0.25); +} + +:root.dark .search-hl.current { + background: rgba(255, 150, 0, 0.4); +} + /* ===== View Modes ===== */ #app.mode-editor #preview-panel, #app.mode-editor #resizer {