From b34cbb5de31021772dcfd4f6612bf36470b89454 Mon Sep 17 00:00:00 2001 From: thzxx Date: Thu, 4 Jun 2026 10:22:15 +0800 Subject: [PATCH] =?UTF-8?q?v0.3.1:=20=E6=B7=BB=E5=8A=A0=E7=BC=96=E8=BE=91?= =?UTF-8?q?=E5=99=A8=E6=90=9C=E7=B4=A2/=E6=9B=BF=E6=8D=A2=E5=8A=9F?= =?UTF-8?q?=E8=83=BD=EF=BC=8C=E4=BF=AE=E5=A4=8DESLint=E5=92=8Clint-staged?= =?UTF-8?q?=E9=94=99=E8=AF=AF?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 新增 SearchReplace 组件:Ctrl+F 搜索、Ctrl+H 替换、实时高亮、大小写切换、导航、全部替换 - 修复 tabStore.test.ts 未使用变量的 ESLint 错误 - 修复 lint-staged tsc 命令不兼容 Milkdown .d.ts 的问题 - 版本号 0.3.0 → 0.3.1 (package.json + AboutDialog) - tsc 零错误、78/78 测试通过、构建成功 --- package.json | 4 +- .../components/AboutDialog/AboutDialog.tsx | 2 +- src/renderer/components/Editor/Editor.tsx | 21 +- .../SearchReplace/SearchReplace.tsx | 375 ++++++++++++++++++ .../components/SearchReplace/index.ts | 1 + .../stores/__tests__/tabStore.test.ts | 7 +- src/renderer/styles/global.css | 278 +++++++++++-- 7 files changed, 642 insertions(+), 46 deletions(-) create mode 100644 src/renderer/components/SearchReplace/SearchReplace.tsx create mode 100644 src/renderer/components/SearchReplace/index.ts diff --git a/package.json b/package.json index 9422da1..a4f0ae0 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "marklite", - "version": "0.3.0", + "version": "0.3.1", "description": "Lightweight Markdown Editor for Windows", "main": "./dist/main/index.js", "scripts": { @@ -19,7 +19,7 @@ "lint-staged": { "*.{ts,tsx}": [ "eslint --fix", - "tsc --noEmit --pretty" + "bash -c 'tsc --noEmit --pretty --skipLibCheck'" ], "*.{json,css,md}": [ "prettier --write" diff --git a/src/renderer/components/AboutDialog/AboutDialog.tsx b/src/renderer/components/AboutDialog/AboutDialog.tsx index 9e99fd4..22ebdf0 100644 --- a/src/renderer/components/AboutDialog/AboutDialog.tsx +++ b/src/renderer/components/AboutDialog/AboutDialog.tsx @@ -1,7 +1,7 @@ import React from 'react' import { AppIcon, Gitee } from '../Icons' -const APP_VERSION = 'v0.2.0' +const APP_VERSION = 'v0.3.1' interface AboutDialogProps { onClose: () => void diff --git a/src/renderer/components/Editor/Editor.tsx b/src/renderer/components/Editor/Editor.tsx index de448fe..bd59e12 100644 --- a/src/renderer/components/Editor/Editor.tsx +++ b/src/renderer/components/Editor/Editor.tsx @@ -1,9 +1,10 @@ -import React, { useEffect, useCallback } from 'react' +import React, { useEffect, useCallback, useState } from 'react' import { callCommand } from '@milkdown/utils' import { toggleStrongCommand, toggleEmphasisCommand } from '@milkdown/preset-commonmark' import { useTabStore } from '../../stores/tabStore' import { useMilkdown } from './useMilkdown' import { EditorToolbar } from './EditorToolbar' +import { SearchReplace } from '../SearchReplace' interface EditorProps { darkMode: boolean @@ -15,6 +16,7 @@ export function Editor({ darkMode }: EditorProps) { const updateTabContent = useTabStore(s => s.updateTabContent) const setModified = useTabStore(s => s.setModified) const updateTabScroll = useTabStore(s => s.updateTabScroll) + const [showSearch, setShowSearch] = useState(false) const { containerRef, @@ -58,7 +60,7 @@ export function Editor({ darkMode }: EditorProps) { // eslint-disable-next-line react-hooks/exhaustive-deps }, [activeTabId]) - // Ctrl+B bold, Ctrl+I italic + // Ctrl+B bold, Ctrl+I italic, Ctrl+F search, Ctrl+H replace useEffect(() => { const handleKeyDown = (e: KeyboardEvent) => { const isCtrl = e.ctrlKey || e.metaKey @@ -74,14 +76,29 @@ export function Editor({ darkMode }: EditorProps) { editor.action(callCommand(toggleEmphasisCommand.key)) }) } + if (isCtrl && (e.key === 'f' || e.key === 'h')) { + e.preventDefault() + setShowSearch(true) + } } document.addEventListener('keydown', handleKeyDown) return () => document.removeEventListener('keydown', handleKeyDown) }, [action]) + // 获取编辑器容器 DOM(用于搜索高亮) + const getEditorContainer = useCallback((): HTMLDivElement | null => { + return containerRef.current + }, [containerRef]) + return (
+ {showSearch && ( + setShowSearch(false)} + /> + )}
) diff --git a/src/renderer/components/SearchReplace/SearchReplace.tsx b/src/renderer/components/SearchReplace/SearchReplace.tsx new file mode 100644 index 0000000..4b93d76 --- /dev/null +++ b/src/renderer/components/SearchReplace/SearchReplace.tsx @@ -0,0 +1,375 @@ +import React, { useState, useCallback, useRef, useEffect, memo } from 'react' + +interface SearchReplaceProps { + /** 获取 Milkdown 编辑器容器 DOM(用于遍历文本节点) */ + editorContainer: HTMLDivElement | null + /** 关闭面板 */ + onClose: () => void +} + +const HIGHLIGHT_CLASS = 'search-match-highlight' +const ACTIVE_HIGHLIGHT_CLASS = 'search-match-active' + +/** + * 清除所有搜索高亮 + */ +function clearHighlights(root: HTMLElement) { + const highlighted = root.querySelectorAll(`.${HIGHLIGHT_CLASS}, .${ACTIVE_HIGHLIGHT_CLASS}`) + highlighted.forEach(el => { + const parent = el.parentNode + if (!parent) return + while (el.firstChild) parent.insertBefore(el.firstChild, el) + parent.removeChild(el) + }) + // 合并相邻文本节点 + root.normalize() +} + +/** + * 高亮所有匹配项 + * 返回高亮后的元素列表(用于导航) + */ +function highlightMatches(root: HTMLElement, query: string, caseSensitive: boolean): HTMLElement[] { + clearHighlights(root) + if (!query) return [] + + const matchElements: HTMLElement[] = [] + const walker = document.createTreeWalker(root, NodeFilter.SHOW_TEXT, null) + const normalizedQuery = caseSensitive ? query : query.toLowerCase() + + const textNodes: Text[] = [] + let node: Node | null + while ((node = walker.nextNode())) { + textNodes.push(node as Text) + } + + // 从后往前处理,避免偏移量变化 + for (let i = textNodes.length - 1; i >= 0; i--) { + const textNode = textNodes[i] + const text = textNode.textContent || '' + const searchText = caseSensitive ? text : text.toLowerCase() + + // 收集该文本节点中的所有匹配位置 + const positions: number[] = [] + let offset = 0 + let idx: number + while ((idx = searchText.indexOf(normalizedQuery, offset)) !== -1) { + positions.push(idx) + offset = idx + 1 + } + + if (positions.length === 0) continue + + // 从后往前拆分文本节点并插入高亮 + const parent = textNode.parentNode + if (!parent) continue + + for (let j = positions.length - 1; j >= 0; j--) { + const pos = positions[j] + const afterText = text.substring(pos + query.length) + const matchText = text.substring(pos, pos + query.length) + const beforeText = text.substring(0, pos) + + // 移除原始文本节点 + if (j === positions.length - 1) { + parent.removeChild(textNode) + } + + // 插入后续文本 + if (afterText) { + parent.insertBefore(document.createTextNode(afterText), parent.children[0] || null) + } + + // 插入高亮标记 + const mark = document.createElement('mark') + mark.className = HIGHLIGHT_CLASS + mark.textContent = matchText + mark.setAttribute('data-match-index', String(matchElements.length)) + parent.insertBefore(mark, parent.children[0] || null) + matchElements.unshift(mark) + + // 插入前面文本(仅第一个匹配时) + if (j === 0 && beforeText) { + parent.insertBefore(document.createTextNode(beforeText), parent.children[0] || null) + } + } + } + + return matchElements +} + +/** + * SearchReplace — 编辑器内搜索/替换面板 + * + * 功能: + * - Ctrl+F 打开搜索,Ctrl+H 打开替换 + * - 大小写敏感切换 + * - Enter 下一个,Shift+Enter 上一个 + * - 替换当前匹配 / 全部替换 + * - ESC 关闭面板 + */ +export const SearchReplace = memo(function SearchReplace({ + editorContainer, + onClose +}: SearchReplaceProps) { + const [query, setQuery] = useState('') + const [replacement, setReplacement] = useState('') + const [showReplace, setShowReplace] = useState(false) + const [caseSensitive, setCaseSensitive] = useState(false) + const [matchCount, setMatchCount] = useState(0) + const [currentMatch, setCurrentMatch] = useState(-1) + + const searchInputRef = useRef(null) + const matchElementsRef = useRef([]) + const panelRef = useRef(null) + + // 执行搜索 + const doSearch = useCallback((searchQuery: string, cs: boolean) => { + if (!editorContainer) return + + if (!searchQuery.trim()) { + clearHighlights(editorContainer) + matchElementsRef.current = [] + setMatchCount(0) + setCurrentMatch(-1) + return + } + + const elements = highlightMatches(editorContainer, searchQuery, cs) + matchElementsRef.current = elements + setMatchCount(elements.length) + + if (elements.length > 0) { + setCurrentMatch(0) + elements[0].classList.add(ACTIVE_HIGHLIGHT_CLASS) + elements[0].scrollIntoView({ behavior: 'smooth', block: 'center' }) + } else { + setCurrentMatch(-1) + } + }, [editorContainer]) + + // 搜索框输入变化 + const handleQueryChange = useCallback((e: React.ChangeEvent) => { + const value = e.target.value + setQuery(value) + doSearch(value, caseSensitive) + }, [doSearch, caseSensitive]) + + // 大小写切换 + const toggleCaseSensitive = useCallback(() => { + const newCS = !caseSensitive + setCaseSensitive(newCS) + doSearch(query, newCS) + }, [caseSensitive, query, doSearch]) + + // 导航到下一个/上一个匹配 + const navigateMatch = useCallback((direction: 1 | -1) => { + const elements = matchElementsRef.current + if (elements.length === 0) return + + // 移除当前高亮 + if (currentMatch >= 0 && currentMatch < elements.length) { + elements[currentMatch].classList.remove(ACTIVE_HIGHLIGHT_CLASS) + } + + let next = currentMatch + direction + if (next >= elements.length) next = 0 + if (next < 0) next = elements.length - 1 + + setCurrentMatch(next) + elements[next].classList.add(ACTIVE_HIGHLIGHT_CLASS) + elements[next].scrollIntoView({ behavior: 'smooth', block: 'center' }) + }, [currentMatch]) + + // 替换当前匹配 + const replaceCurrent = useCallback(() => { + if (currentMatch < 0 || !editorContainer) return + + const elements = matchElementsRef.current + const el = elements[currentMatch] + if (!el || !el.parentNode) return + + // 替换文本 + const textNode = document.createTextNode(replacement) + el.parentNode.replaceChild(textNode, el) + editorContainer.normalize() + + // 重新搜索 + doSearch(query, caseSensitive) + }, [currentMatch, replacement, editorContainer, query, caseSensitive, doSearch]) + + // 全部替换 + const replaceAll = useCallback(() => { + if (!editorContainer || matchCount === 0) return + + const elements = [...matchElementsRef.current] + elements.forEach(el => { + if (el.parentNode) { + el.parentNode.replaceChild(document.createTextNode(replacement), el) + } + }) + editorContainer.normalize() + + doSearch(query, caseSensitive) + }, [editorContainer, matchCount, replacement, query, caseSensitive, doSearch]) + + // 键盘事件 + const handleKeyDown = useCallback((e: React.KeyboardEvent) => { + if (e.key === 'Escape') { + e.preventDefault() + if (editorContainer) clearHighlights(editorContainer) + onClose() + return + } + if (e.key === 'Enter') { + e.preventDefault() + if (e.shiftKey) { + navigateMatch(-1) + } else { + navigateMatch(1) + } + } + }, [onClose, navigateMatch, editorContainer]) + + // 全局 Ctrl+F / Ctrl+H 拦截 + useEffect(() => { + const handleGlobalKey = (e: KeyboardEvent) => { + const isCtrl = e.ctrlKey || e.metaKey + if (isCtrl && e.key === 'f') { + e.preventDefault() + setShowReplace(false) + searchInputRef.current?.focus() + searchInputRef.current?.select() + } + if (isCtrl && e.key === 'h') { + e.preventDefault() + setShowReplace(true) + searchInputRef.current?.focus() + searchInputRef.current?.select() + } + } + document.addEventListener('keydown', handleGlobalKey) + return () => document.removeEventListener('keydown', handleGlobalKey) + }, []) + + // 打开时聚焦搜索框 + useEffect(() => { + searchInputRef.current?.focus() + searchInputRef.current?.select() + }, []) + + // 清理高亮 + useEffect(() => { + return () => { + if (editorContainer) clearHighlights(editorContainer) + } + }, [editorContainer]) + + return ( +
+
+
+ + + {matchCount > 0 ? `${currentMatch + 1}/${matchCount}` : query ? '无匹配' : ''} + +
+ + + + + +
+ {showReplace && ( +
+
+ setReplacement(e.target.value)} + aria-label="替换文本" + /> +
+ + +
+ )} +
+ ) +}) + +SearchReplace.displayName = 'SearchReplace' diff --git a/src/renderer/components/SearchReplace/index.ts b/src/renderer/components/SearchReplace/index.ts new file mode 100644 index 0000000..ca2683a --- /dev/null +++ b/src/renderer/components/SearchReplace/index.ts @@ -0,0 +1 @@ +export { SearchReplace } from './SearchReplace' diff --git a/src/renderer/stores/__tests__/tabStore.test.ts b/src/renderer/stores/__tests__/tabStore.test.ts index d83e673..46448d4 100644 --- a/src/renderer/stores/__tests__/tabStore.test.ts +++ b/src/renderer/stores/__tests__/tabStore.test.ts @@ -52,14 +52,13 @@ describe('tabStore', () => { }) it('should switch to existing tab if same filePath is opened', () => { - const { createTab } = useTabStore.getState() - createTab('/test.md', '# First') + useTabStore.getState().createTab('/test.md', '# First') // Create another tab to make it active - createTab('/other.md', '# Other') + useTabStore.getState().createTab('/other.md', '# Other') // Now try to open the first file again - const existing = createTab('/test.md', '# Updated') + const existing = useTabStore.getState().createTab('/test.md', '# Updated') const state = useTabStore.getState() expect(state.tabs).toHaveLength(2) diff --git a/src/renderer/styles/global.css b/src/renderer/styles/global.css index 1300775..a4a3b12 100644 --- a/src/renderer/styles/global.css +++ b/src/renderer/styles/global.css @@ -4,7 +4,8 @@ box-sizing: border-box; } -html, body { +html, +body { height: 100%; font-family: var(--font-ui); font-size: 14px; @@ -33,7 +34,8 @@ html, body { z-index: 10; } -.toolbar-left, .toolbar-right { +.toolbar-left, +.toolbar-right { display: flex; align-items: center; gap: 2px; @@ -65,7 +67,9 @@ html, body { color: var(--primary); } -.toolbar-btn svg { flex-shrink: 0; } +.toolbar-btn svg { + flex-shrink: 0; +} .toolbar-divider { width: 1px; @@ -129,7 +133,9 @@ html, body { overflow-x: auto; } -.editor-toolbar::-webkit-scrollbar { height: 0; } +.editor-toolbar::-webkit-scrollbar { + height: 0; +} .toolbar-btn-sm { display: flex; @@ -203,9 +209,15 @@ html, body { color: var(--text); } -.milkdown-wrapper .milkdown .editor h1 { font-size: 1.6em; } -.milkdown-wrapper .milkdown .editor h2 { font-size: 1.4em; } -.milkdown-wrapper .milkdown .editor h3 { font-size: 1.2em; } +.milkdown-wrapper .milkdown .editor h1 { + font-size: 1.6em; +} +.milkdown-wrapper .milkdown .editor h2 { + font-size: 1.4em; +} +.milkdown-wrapper .milkdown .editor h3 { + font-size: 1.2em; +} .milkdown-wrapper .milkdown .editor blockquote { border-left: 3px solid var(--primary); @@ -420,7 +432,9 @@ html, body { .tab-item:hover .tab-close, .tab-item.active .tab-close, -.tab-item.modified .tab-close { opacity: 1; } +.tab-item.modified .tab-close { + opacity: 1; +} .tab-close:hover { background: var(--border); @@ -541,13 +555,16 @@ html, body { flex-shrink: 0; } -.status-left, .status-right { +.status-left, +.status-right { display: flex; align-items: center; gap: 8px; } -.status-divider { color: var(--border); } +.status-divider { + color: var(--border); +} /* Drop Overlay */ #drop-overlay { @@ -603,7 +620,9 @@ html, body { animation: fadeIn 0.3s ease; } -.welcome-icon { margin-bottom: 24px; } +.welcome-icon { + margin-bottom: 24px; +} .welcome-content h1 { font-size: 28px; @@ -655,7 +674,9 @@ html, body { border: 1px solid var(--border); } -.welcome-btn.secondary:hover { background: var(--bg-tertiary); } +.welcome-btn.secondary:hover { + background: var(--bg-tertiary); +} /* 最近打开文件 */ .welcome-recent { @@ -698,7 +719,9 @@ html, body { border-bottom: 1px solid var(--border-light); } -.recent-item:last-child { border-bottom: none; } +.recent-item:last-child { + border-bottom: none; +} .recent-item:hover { background: var(--bg-tertiary); @@ -737,7 +760,9 @@ html, body { margin-bottom: 6px; } -.welcome-tips p:last-child { margin-bottom: 0; } +.welcome-tips p:last-child { + margin-bottom: 0; +} /* Sidebar */ #sidebar { @@ -754,7 +779,9 @@ html, body { position: relative; } -#sidebar.collapsed { display: none; } +#sidebar.collapsed { + display: none; +} #sidebar-header { display: flex; @@ -811,7 +838,9 @@ html, body { border-radius: 0; } -.tree-item:hover { background: var(--sidebar-hover); } +.tree-item:hover { + background: var(--sidebar-hover); +} .tree-item.active { background: var(--sidebar-active); @@ -819,7 +848,9 @@ html, body { font-weight: 500; } -.tree-indent { flex-shrink: 0; } +.tree-indent { + flex-shrink: 0; +} .tree-arrow { display: flex; @@ -832,7 +863,9 @@ html, body { transition: transform 0.15s ease; } -.tree-arrow.expanded { transform: rotate(90deg); } +.tree-arrow.expanded { + transform: rotate(90deg); +} .tree-icon { display: flex; @@ -844,7 +877,10 @@ html, body { margin-right: 4px; } -.tree-icon svg { width: 14px; height: 14px; } +.tree-icon svg { + width: 14px; + height: 14px; +} .tree-name { overflow: hidden; @@ -858,7 +894,8 @@ html, body { padding-bottom: 4px; } -.sidebar-section-header, .independent-files-header { +.sidebar-section-header, +.independent-files-header { font-size: 11px; font-weight: 600; color: var(--text-tertiary); @@ -889,8 +926,12 @@ html, body { } /* View Modes */ -#app.mode-preview #editor-panel { display: none; } -#app.mode-editor #preview-panel { display: none; } +#app.mode-preview #editor-panel { + display: none; +} +#app.mode-editor #preview-panel { + display: none; +} /* Toast */ #toast-notification { @@ -907,7 +948,9 @@ html, body { box-shadow: var(--shadow-lg); opacity: 0; pointer-events: none; - transition: opacity 0.3s ease, transform 0.3s ease; + transition: + opacity 0.3s ease, + transform 0.3s ease; z-index: 9999; max-width: 480px; text-align: center; @@ -920,15 +963,31 @@ html, body { } /* Scrollbar */ -::-webkit-scrollbar { width: 8px; height: 8px; } -::-webkit-scrollbar-track { background: transparent; } -::-webkit-scrollbar-thumb { background: var(--border); border-radius: 4px; } -::-webkit-scrollbar-thumb:hover { background: var(--text-tertiary); } +::-webkit-scrollbar { + width: 8px; + height: 8px; +} +::-webkit-scrollbar-track { + background: transparent; +} +::-webkit-scrollbar-thumb { + background: var(--border); + border-radius: 4px; +} +::-webkit-scrollbar-thumb:hover { + background: var(--text-tertiary); +} /* Animations */ @keyframes fadeIn { - from { opacity: 0; transform: translateY(8px); } - to { opacity: 1; transform: translateY(0); } + from { + opacity: 0; + transform: translateY(8px); + } + to { + opacity: 1; + transform: translateY(0); + } } /* About Dialog */ @@ -1094,9 +1153,15 @@ html, body { margin: 0; } -.confirm-header.confirm-danger h3 { color: #d93025; } -.confirm-header.confirm-warning h3 { color: #e37400; } -:root.dark .confirm-header.confirm-warning h3 { color: #fdd663; } +.confirm-header.confirm-danger h3 { + color: #d93025; +} +.confirm-header.confirm-warning h3 { + color: #e37400; +} +:root.dark .confirm-header.confirm-warning h3 { + color: #fdd663; +} .confirm-body { padding: 12px 24px 20px; @@ -1212,8 +1277,12 @@ html, body { } @keyframes spin { - from { transform: rotate(0deg); } - to { transform: rotate(360deg); } + from { + transform: rotate(0deg); + } + to { + transform: rotate(360deg); + } } /* Status bar loading indicator */ @@ -1365,9 +1434,9 @@ html, body { /* Focus visible 为所有交互元素提供清晰的焦点指示 */ button:focus-visible, -[role="treeitem"]:focus-visible, -[role="tab"]:focus-visible, -[role="menuitem"]:focus-visible { +[role='treeitem']:focus-visible, +[role='tab']:focus-visible, +[role='menuitem']:focus-visible { outline: 2px solid var(--primary); outline-offset: 2px; } @@ -1396,3 +1465,138 @@ button:focus-visible, white-space: nowrap; border: 0; } + +/* ===== Search & Replace Panel ===== */ + +.search-replace-panel { + display: flex; + flex-direction: column; + gap: 4px; + padding: 8px 12px; + background: var(--bg-secondary); + border-bottom: 1px solid var(--border); + flex-shrink: 0; + animation: slideDown 0.15s ease-out; +} + +@keyframes slideDown { + from { + transform: translateY(-100%); + opacity: 0; + } + to { + transform: translateY(0); + opacity: 1; + } +} + +.search-row { + display: flex; + align-items: center; + gap: 4px; +} + +.search-input-group { + flex: 1; + display: flex; + align-items: center; + position: relative; + min-width: 0; +} + +.search-input { + flex: 1; + height: 28px; + padding: 0 8px; + border: 1px solid var(--border); + border-radius: 4px; + background: var(--bg); + color: var(--text); + font-size: 12px; + font-family: var(--font-ui); + outline: none; + transition: border-color 0.15s; +} + +.search-input:focus { + border-color: var(--primary); +} + +.search-input::placeholder { + color: var(--text-tertiary); +} + +.search-count { + position: absolute; + right: 8px; + font-size: 11px; + color: var(--text-tertiary); + pointer-events: none; + white-space: nowrap; +} + +.search-btn { + display: flex; + align-items: center; + justify-content: center; + min-width: 28px; + height: 28px; + padding: 0 6px; + border: none; + background: transparent; + color: var(--text-secondary); + font-size: 12px; + font-family: var(--font-ui); + border-radius: 4px; + cursor: pointer; + transition: all 0.15s ease; + white-space: nowrap; +} + +.search-btn:hover { + background: var(--bg-tertiary); + color: var(--text); +} + +.search-btn:disabled { + opacity: 0.4; + cursor: not-allowed; +} + +.search-btn.active { + background: var(--primary-light); + color: var(--primary); +} + +.search-close:hover { + background: #e74c3c20; + color: #e74c3c; +} + +.replace-btn { + font-size: 11px; + padding: 0 8px; +} + +/* Search match highlights */ +mark.search-match-highlight { + background: rgba(255, 213, 0, 0.4); + color: inherit; + border-radius: 2px; + padding: 0; +} + +mark.search-match-active { + background: rgba(255, 150, 0, 0.6); + outline: 1px solid rgba(255, 150, 0, 0.8); + border-radius: 2px; +} + +:root.dark mark.search-match-highlight { + background: rgba(255, 213, 0, 0.25); +} + +:root.dark mark.search-match-active { + background: rgba(255, 180, 0, 0.4); + outline-color: rgba(255, 180, 0, 0.6); +}