refactor: 删除自定义搜索,统一使用 CodeMirror 6 搜索
删除文件:
- SearchBar/SearchBar.tsx — 自定义搜索栏组件
- stores/searchStore.ts — 自定义搜索状态管理
- lib/searchEngine.ts — 自定义搜索匹配引擎
- types/search.ts — 搜索相关类型定义
清理代码:
- App.tsx:移除 SearchBar 导入和渲染
- useKeyboard.ts:移除 searchStore 引用和搜索快捷键
- Editor.tsx:移除 searchStore 引用和搜索逻辑
- types/index.ts:移除 search 类型导出
- global.css:删除旧搜索栏样式(~113 行)
CodeMirror 6 搜索面板移到顶部:
- .cm-panels { top: 0; bottom: auto; }
- .cm-panels-bottom { top: 0; bottom: auto; }
快捷键(CodeMirror 内置):
- Ctrl+F 搜索
- Ctrl+H 替换
- Enter/Shift+Enter 下一个/上一个
- Alt+C 大小写
- Alt+R 正则
This commit is contained in:
+3
-10
@@ -1,25 +1,23 @@
|
||||
import React, { useState, useCallback, useEffect, useRef } from 'react'
|
||||
import { useTabStore } from './stores/tabStore'
|
||||
import { useEditorStore } from './stores/editorStore'
|
||||
import { useSearchStore } from './stores/searchStore'
|
||||
import { useTheme } from './hooks/useTheme'
|
||||
import { useSettings } from './hooks/useSettings'
|
||||
import { useKeyboard } from './hooks/useKeyboard'
|
||||
import { useUnsavedWarning } from './hooks/useUnsavedWarning'
|
||||
import { useFileWatch } from './hooks/useFileWatch'
|
||||
import { recentFilesRepository } from './db/recentFilesRepository'
|
||||
import { useDragDrop } from './hooks/useDragDrop'
|
||||
import { Toolbar } from './components/Toolbar/Toolbar'
|
||||
import { TabBar } from './components/TabBar/TabBar'
|
||||
import { Editor } from './components/Editor/Editor'
|
||||
import { Preview } from './components/Preview/Preview'
|
||||
import { Sidebar } from './components/Sidebar/Sidebar'
|
||||
import { SearchBar } from './components/SearchBar/SearchBar'
|
||||
import { StatusBar } from './components/StatusBar/StatusBar'
|
||||
import { WelcomeScreen } from './components/WelcomeScreen/WelcomeScreen'
|
||||
import { Toast } from './components/Toast/Toast'
|
||||
import { ModifiedBanner } from './components/ModifiedBanner/ModifiedBanner'
|
||||
import { DropOverlay } from './components/DropOverlay/DropOverlay'
|
||||
import { useDragDrop } from './hooks/useDragDrop'
|
||||
|
||||
export default function App() {
|
||||
const tabs = useTabStore(s => s.tabs)
|
||||
@@ -29,7 +27,6 @@ export default function App() {
|
||||
const setModified = useTabStore(s => s.setModified)
|
||||
const updateTabContent = useTabStore(s => s.updateTabContent)
|
||||
const loadFromDB = useTabStore(s => s.loadFromDB)
|
||||
const saveToDB = useTabStore(s => s.saveToDB)
|
||||
|
||||
const viewMode = useEditorStore(s => s.viewMode)
|
||||
const splitRatio = useEditorStore(s => s.splitRatio)
|
||||
@@ -80,9 +77,7 @@ export default function App() {
|
||||
const result = await window.electronAPI.openFile()
|
||||
if (result && 'filePath' in result) {
|
||||
createTab(result.filePath, result.content)
|
||||
// 追踪最近打开文件
|
||||
if (result.filePath) recentFilesRepository.add(result.filePath)
|
||||
// 保存标签页到 DB
|
||||
setTimeout(() => useTabStore.getState().saveToDB(), 100)
|
||||
}
|
||||
}
|
||||
@@ -137,7 +132,7 @@ export default function App() {
|
||||
saveViewMode(mode)
|
||||
}, [saveViewMode])
|
||||
|
||||
// M-11: 主进程事件注册(使用 ref 保持最新回调引用 + cleanup 清理)
|
||||
// M-11: 主进程事件注册
|
||||
const handleSaveRef = useRef(handleSave)
|
||||
const handleSaveAsRef = useRef(handleSaveAs)
|
||||
handleSaveRef.current = handleSave
|
||||
@@ -149,7 +144,7 @@ export default function App() {
|
||||
|
||||
const onOpen = (data: { filePath: string; content: string }) => {
|
||||
createTab(data.filePath, data.content)
|
||||
recentFilesRepository.add(data.filePath)
|
||||
if (data.filePath) recentFilesRepository.add(data.filePath)
|
||||
setTimeout(() => useTabStore.getState().saveToDB(), 100)
|
||||
}
|
||||
const onSave = () => handleSaveRef.current()
|
||||
@@ -218,7 +213,6 @@ export default function App() {
|
||||
<div id="content-wrapper">
|
||||
{viewMode !== 'preview' && (
|
||||
<div id="editor-panel" style={editorStyle}>
|
||||
<SearchBar />
|
||||
<Editor darkMode={darkMode} />
|
||||
</div>
|
||||
)}
|
||||
@@ -270,7 +264,6 @@ function Resizer({ onResize }: { onResize: (ratio: number) => void }) {
|
||||
document.removeEventListener('mouseup', handleMouseUp)
|
||||
document.body.style.cursor = ''
|
||||
document.body.style.userSelect = ''
|
||||
// 保存最终比例
|
||||
const wrapper = document.getElementById('content-wrapper')
|
||||
if (wrapper) {
|
||||
const rect = wrapper.getBoundingClientRect()
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import React, { useEffect, useCallback } from 'react'
|
||||
import { useTabStore } from '../../stores/tabStore'
|
||||
import { useSearchStore } from '../../stores/searchStore'
|
||||
import { useCodeMirror } from './useCodeMirror'
|
||||
import { EditorToolbar } from './EditorToolbar'
|
||||
|
||||
@@ -20,12 +19,10 @@ export function Editor({ darkMode }: EditorProps) {
|
||||
containerRef,
|
||||
viewRef,
|
||||
setContent,
|
||||
getContent,
|
||||
getScrollTop,
|
||||
setScrollTop,
|
||||
getSelection,
|
||||
setSelection,
|
||||
scrollTo
|
||||
setSelection
|
||||
} = useCodeMirror({
|
||||
content: activeTab?.content ?? '',
|
||||
onChange: useCallback((value: string) => {
|
||||
@@ -43,15 +40,13 @@ export function Editor({ darkMode }: EditorProps) {
|
||||
useEffect(() => {
|
||||
if (!activeTab) return
|
||||
setContent(activeTab.content)
|
||||
|
||||
// 恢复滚动和选区
|
||||
requestAnimationFrame(() => {
|
||||
setScrollTop(activeTab.scrollTop)
|
||||
setSelection(activeTab.selectionStart, activeTab.selectionEnd)
|
||||
})
|
||||
}, [activeTabId])
|
||||
|
||||
// 保存当前标签状态(切换前)
|
||||
// 保存当前标签状态
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
if (!activeTabId) return
|
||||
@@ -63,12 +58,10 @@ export function Editor({ darkMode }: EditorProps) {
|
||||
}
|
||||
}, [activeTabId])
|
||||
|
||||
// 搜索:Ctrl+F 聚焦搜索框,Enter 导航
|
||||
// Ctrl+B 粗体, Ctrl+I 斜体
|
||||
useEffect(() => {
|
||||
const handleKeyDown = (e: KeyboardEvent) => {
|
||||
const isCtrl = e.ctrlKey || e.metaKey
|
||||
|
||||
// Ctrl+B 粗体
|
||||
if (isCtrl && e.key === 'b') {
|
||||
e.preventDefault()
|
||||
const view = viewRef.current
|
||||
@@ -80,8 +73,6 @@ export function Editor({ darkMode }: EditorProps) {
|
||||
selection: { anchor: from + 2, head: from + 2 + selected.length }
|
||||
})
|
||||
}
|
||||
|
||||
// Ctrl+I 斜体
|
||||
if (isCtrl && e.key === 'i') {
|
||||
e.preventDefault()
|
||||
const view = viewRef.current
|
||||
@@ -94,7 +85,6 @@ export function Editor({ darkMode }: EditorProps) {
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
document.addEventListener('keydown', handleKeyDown)
|
||||
return () => document.removeEventListener('keydown', handleKeyDown)
|
||||
}, [viewRef])
|
||||
|
||||
@@ -1,147 +0,0 @@
|
||||
import React, { useCallback, useRef, useEffect } from 'react'
|
||||
import { useSearchStore } from '../../stores/searchStore'
|
||||
import { useTabStore } from '../../stores/tabStore'
|
||||
import { findMatches } from '../../lib/searchEngine'
|
||||
import { ChevronUp, ChevronDown, X } from '../Icons'
|
||||
|
||||
export function SearchBar() {
|
||||
const searchInputRef = useRef<HTMLInputElement>(null)
|
||||
|
||||
const isVisible = useSearchStore(s => s.isVisible)
|
||||
const showReplace = useSearchStore(s => s.showReplace)
|
||||
const searchText = useSearchStore(s => s.searchText)
|
||||
const replaceText = useSearchStore(s => s.replaceText)
|
||||
const matches = useSearchStore(s => s.matches)
|
||||
const currentIndex = useSearchStore(s => s.currentIndex)
|
||||
const options = useSearchStore(s => s.options)
|
||||
const setSearchText = useSearchStore(s => s.setSearchText)
|
||||
const setReplaceText = useSearchStore(s => s.setReplaceText)
|
||||
const setShowReplace = useSearchStore(s => s.setShowReplace)
|
||||
const setMatches = useSearchStore(s => s.setMatches)
|
||||
const findNext = useSearchStore(s => s.findNext)
|
||||
const findPrev = useSearchStore(s => s.findPrev)
|
||||
const toggleCaseSensitive = useSearchStore(s => s.toggleCaseSensitive)
|
||||
const toggleRegex = useSearchStore(s => s.toggleRegex)
|
||||
const close = useSearchStore(s => s.close)
|
||||
|
||||
const getActiveTab = useTabStore(s => s.getActiveTab)
|
||||
|
||||
const doSearch = useCallback((text: string) => {
|
||||
const tab = getActiveTab()
|
||||
if (!tab || !text) {
|
||||
setMatches([])
|
||||
return
|
||||
}
|
||||
const found = findMatches(tab.content, text, options)
|
||||
setMatches(found)
|
||||
}, [getActiveTab, options, setMatches])
|
||||
|
||||
const handleSearchChange = useCallback((e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const text = e.target.value
|
||||
setSearchText(text)
|
||||
// Editor 会监听 searchText 变化并执行搜索和高亮
|
||||
}, [setSearchText])
|
||||
|
||||
const handleReplaceCurrent = useCallback(() => {
|
||||
const tab = getActiveTab()
|
||||
if (!tab || matches.length === 0 || currentIndex < 0) return
|
||||
const m = matches[currentIndex]
|
||||
const newContent = tab.content.substring(0, m.start) + replaceText + tab.content.substring(m.end)
|
||||
useTabStore.getState().updateTabContent(tab.id, newContent)
|
||||
doSearch(searchText)
|
||||
}, [getActiveTab, matches, currentIndex, replaceText, searchText, doSearch])
|
||||
|
||||
// B-02: 全部替换(从后向前逐个替换)
|
||||
const handleReplaceAll = useCallback(() => {
|
||||
const tab = getActiveTab()
|
||||
if (!tab || matches.length === 0) return
|
||||
let result = tab.content
|
||||
for (let i = matches.length - 1; i >= 0; i--) {
|
||||
const m = matches[i]
|
||||
result = result.substring(0, m.start) + replaceText + result.substring(m.end)
|
||||
}
|
||||
useTabStore.getState().updateTabContent(tab.id, result)
|
||||
doSearch(searchText)
|
||||
}, [getActiveTab, matches, replaceText, searchText, doSearch])
|
||||
|
||||
useEffect(() => {
|
||||
if (isVisible && searchInputRef.current) {
|
||||
searchInputRef.current.focus()
|
||||
}
|
||||
}, [isVisible])
|
||||
|
||||
if (!isVisible) return null
|
||||
|
||||
return (
|
||||
<div id="search-bar">
|
||||
<div className="search-row">
|
||||
<button
|
||||
id="btn-toggle-replace"
|
||||
className={`search-opt-btn ${showReplace ? 'expanded' : ''}`}
|
||||
onClick={() => setShowReplace(!showReplace)}
|
||||
title="展开替换行"
|
||||
>
|
||||
▾
|
||||
</button>
|
||||
<input
|
||||
ref={searchInputRef}
|
||||
type="text"
|
||||
id="search-input"
|
||||
placeholder="查找..."
|
||||
value={searchText}
|
||||
onChange={handleSearchChange}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === 'Enter') {
|
||||
e.preventDefault()
|
||||
e.shiftKey ? findPrev() : findNext()
|
||||
}
|
||||
if (e.key === 'Escape') {
|
||||
e.preventDefault()
|
||||
close()
|
||||
}
|
||||
}}
|
||||
/>
|
||||
<span id="search-count">
|
||||
{matches.length > 0 ? `${currentIndex + 1}/${matches.length}` : searchText ? '无结果' : ''}
|
||||
</span>
|
||||
<button
|
||||
className={`search-opt-btn ${options.caseSensitive ? 'active' : ''}`}
|
||||
onClick={toggleCaseSensitive}
|
||||
title="区分大小写 (Alt+C)"
|
||||
>
|
||||
Aa
|
||||
</button>
|
||||
<button
|
||||
className={`search-opt-btn ${options.useRegex ? 'active' : ''}`}
|
||||
onClick={toggleRegex}
|
||||
title="正则表达式 (Alt+R)"
|
||||
>
|
||||
.*
|
||||
</button>
|
||||
<button className="search-nav-btn" onClick={findPrev} title="上一个 (Shift+Enter)">
|
||||
<ChevronUp size={12} />
|
||||
</button>
|
||||
<button className="search-nav-btn" onClick={findNext} title="下一个 (Enter)">
|
||||
<ChevronDown size={12} />
|
||||
</button>
|
||||
<button className="search-nav-btn" onClick={close} title="关闭 (Escape)">
|
||||
<X size={14} />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{showReplace && (
|
||||
<div id="replace-row">
|
||||
<input
|
||||
type="text"
|
||||
id="replace-input"
|
||||
placeholder="替换..."
|
||||
value={replaceText}
|
||||
onChange={(e) => setReplaceText(e.target.value)}
|
||||
/>
|
||||
<button className="replace-btn" onClick={handleReplaceCurrent} title="替换 (Ctrl+Shift+G)">替换</button>
|
||||
<button className="replace-btn" onClick={handleReplaceAll} title="全部替换 (Ctrl+Shift+H)">全部</button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -1,13 +1,11 @@
|
||||
import { useEffect, useCallback } from 'react'
|
||||
import { useTabStore } from '../stores/tabStore'
|
||||
import { useEditorStore } from '../stores/editorStore'
|
||||
import { useSearchStore } from '../stores/searchStore'
|
||||
import type { ViewMode } from '../types/settings'
|
||||
|
||||
export function useKeyboard(handleOpenFile: () => void, handleSave: () => void, handleSaveAs: () => void) {
|
||||
const setViewMode = useEditorStore(s => s.setViewMode)
|
||||
|
||||
// M-08: 使用 getState() 而非依赖 tabs/activeTabId 等频繁变化的值
|
||||
const handleKeydown = useCallback((e: KeyboardEvent) => {
|
||||
const isCtrl = e.ctrlKey || e.metaKey
|
||||
|
||||
@@ -18,7 +16,6 @@ export function useKeyboard(handleOpenFile: () => void, handleSave: () => void,
|
||||
if (isCtrl && e.key === '2') { e.preventDefault(); setViewMode('editor'); return }
|
||||
if (isCtrl && e.key === '3') { e.preventDefault(); setViewMode('preview'); return }
|
||||
|
||||
// M-08: 通过 getState() 读取最新状态
|
||||
const tabState = useTabStore.getState()
|
||||
if (isCtrl && e.key === 't') { e.preventDefault(); tabState.createTab(null, ''); return }
|
||||
if (isCtrl && e.key === 'w') {
|
||||
@@ -47,15 +44,6 @@ export function useKeyboard(handleOpenFile: () => void, handleSave: () => void,
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// Search & Replace
|
||||
const searchState = useSearchStore.getState()
|
||||
if (isCtrl && e.key === 'f') { e.preventDefault(); searchState.setVisible(true); searchState.setShowReplace(false); return }
|
||||
if (isCtrl && e.key === 'h') { e.preventDefault(); searchState.setVisible(true); searchState.setShowReplace(true); return }
|
||||
if (e.key === 'Escape' && searchState.isVisible) { e.preventDefault(); searchState.close(); return }
|
||||
if (e.altKey && e.key === 'c') { e.preventDefault(); searchState.toggleCaseSensitive(); return }
|
||||
if (e.altKey && e.key === 'r') { e.preventDefault(); searchState.toggleRegex(); return }
|
||||
if (isCtrl && e.shiftKey && e.key === 'G') { e.preventDefault(); searchState.findPrev(); return }
|
||||
}, [handleOpenFile, handleSave, handleSaveAs, setViewMode])
|
||||
|
||||
useEffect(() => {
|
||||
|
||||
@@ -1,56 +0,0 @@
|
||||
import type { SearchMatch, SearchOptions } from '../types/search'
|
||||
|
||||
export function findMatches(
|
||||
content: string,
|
||||
searchText: string,
|
||||
options: SearchOptions
|
||||
): SearchMatch[] {
|
||||
if (!searchText) return []
|
||||
|
||||
const matches: SearchMatch[] = []
|
||||
|
||||
if (options.useRegex) {
|
||||
try {
|
||||
const flags = options.caseSensitive ? 'g' : 'gi'
|
||||
const regex = new RegExp(searchText, flags)
|
||||
let m: RegExpExecArray | null
|
||||
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 — return empty
|
||||
}
|
||||
} else {
|
||||
const haystack = options.caseSensitive ? content : content.toLowerCase()
|
||||
const needle = options.caseSensitive ? searchText : searchText.toLowerCase()
|
||||
let idx = 0
|
||||
while ((idx = haystack.indexOf(needle, idx)) !== -1) {
|
||||
matches.push({ start: idx, end: idx + needle.length })
|
||||
idx += needle.length
|
||||
}
|
||||
}
|
||||
|
||||
return matches
|
||||
}
|
||||
|
||||
// 构建行首位置缓存(用于 O(log N) 二分查找行号)
|
||||
export function buildLineStarts(content: string): number[] {
|
||||
const starts = [0]
|
||||
for (let i = 0; i < content.length; i++) {
|
||||
if (content.charCodeAt(i) === 10) starts.push(i + 1) // '\n' = 0x0A
|
||||
}
|
||||
return starts
|
||||
}
|
||||
|
||||
// 二分查找:返回 pos 所在行号
|
||||
export function getLineNumber(lineStarts: number[], pos: number): number {
|
||||
let lo = 0
|
||||
let hi = lineStarts.length - 1
|
||||
while (lo < hi) {
|
||||
const mid = (lo + hi + 1) >> 1
|
||||
if (lineStarts[mid] <= pos) lo = mid
|
||||
else hi = mid - 1
|
||||
}
|
||||
return lo
|
||||
}
|
||||
@@ -1,64 +0,0 @@
|
||||
import { create } from 'zustand'
|
||||
import type { SearchMatch, SearchOptions } from '../types/search'
|
||||
|
||||
interface SearchState {
|
||||
isVisible: boolean
|
||||
showReplace: boolean
|
||||
searchText: string
|
||||
replaceText: string
|
||||
matches: SearchMatch[]
|
||||
currentIndex: number
|
||||
options: SearchOptions
|
||||
|
||||
setVisible: (visible: boolean) => void
|
||||
setShowReplace: (show: boolean) => void
|
||||
setSearchText: (text: string) => void
|
||||
setReplaceText: (text: string) => void
|
||||
setMatches: (matches: SearchMatch[]) => void
|
||||
setCurrentIndex: (index: number) => void
|
||||
toggleCaseSensitive: () => void
|
||||
toggleRegex: () => void
|
||||
findNext: () => void
|
||||
findPrev: () => void
|
||||
close: () => void
|
||||
}
|
||||
|
||||
export const useSearchStore = create<SearchState>((set, get) => ({
|
||||
isVisible: false,
|
||||
showReplace: false,
|
||||
searchText: '',
|
||||
replaceText: '',
|
||||
matches: [],
|
||||
currentIndex: -1,
|
||||
options: { caseSensitive: false, useRegex: false },
|
||||
|
||||
setVisible: (visible) => set({ isVisible: visible }),
|
||||
setShowReplace: (show) => set({ showReplace: show }),
|
||||
setSearchText: (text) => set({ searchText: text }),
|
||||
setReplaceText: (text) => set({ replaceText: text }),
|
||||
setMatches: (matches) => set({ matches, currentIndex: matches.length > 0 ? 0 : -1 }),
|
||||
setCurrentIndex: (index) => set({ currentIndex: index }),
|
||||
toggleCaseSensitive: () =>
|
||||
set(state => ({ options: { ...state.options, caseSensitive: !state.options.caseSensitive } })),
|
||||
toggleRegex: () =>
|
||||
set(state => ({ options: { ...state.options, useRegex: !state.options.useRegex } })),
|
||||
findNext: () => {
|
||||
const { matches, currentIndex } = get()
|
||||
if (matches.length === 0) return
|
||||
set({ currentIndex: (currentIndex + 1) % matches.length })
|
||||
},
|
||||
findPrev: () => {
|
||||
const { matches, currentIndex } = get()
|
||||
if (matches.length === 0) return
|
||||
set({ currentIndex: (currentIndex - 1 + matches.length) % matches.length })
|
||||
},
|
||||
close: () =>
|
||||
set({
|
||||
isVisible: false,
|
||||
showReplace: false,
|
||||
searchText: '',
|
||||
replaceText: '',
|
||||
matches: [],
|
||||
currentIndex: -1
|
||||
})
|
||||
}))
|
||||
+13
-113
@@ -208,6 +208,19 @@ html, body {
|
||||
|
||||
/* ===== CodeMirror 搜索样式美化 ===== */
|
||||
|
||||
/* 搜索面板移到顶部 */
|
||||
.cm-panels {
|
||||
top: 0 !important;
|
||||
bottom: auto !important;
|
||||
}
|
||||
|
||||
.cm-panels-bottom {
|
||||
top: 0 !important;
|
||||
bottom: auto !important;
|
||||
border-bottom: 1px solid var(--border) !important;
|
||||
border-top: none !important;
|
||||
}
|
||||
|
||||
/* 搜索面板整体 */
|
||||
.cm-panel.cm-search {
|
||||
background: var(--bg) !important;
|
||||
@@ -984,119 +997,6 @@ html, body {
|
||||
background: var(--primary);
|
||||
}
|
||||
|
||||
/* Search & Replace Bar */
|
||||
#btn-toggle-replace {
|
||||
font-size: 10px;
|
||||
transition: transform 0.15s ease;
|
||||
}
|
||||
|
||||
#btn-toggle-replace.expanded { transform: rotate(90deg); }
|
||||
|
||||
#search-bar {
|
||||
background: var(--search-bg);
|
||||
border-bottom: 1px solid var(--search-border);
|
||||
padding: 6px 10px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.search-row, #replace-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
#replace-row { margin-top: 4px; }
|
||||
|
||||
#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);
|
||||
}
|
||||
|
||||
/* View Modes */
|
||||
#app.mode-preview #editor-panel,
|
||||
#app.mode-preview #resizer { display: none; }
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
export type { Tab } from './tab'
|
||||
export type { FileNode, DirTreeResult } from './file'
|
||||
export type { ViewMode, Settings } from './settings'
|
||||
export type { SearchMatch, SearchOptions } from './search'
|
||||
export type { ElectronAPI, IpcInvokeMap } from './ipc'
|
||||
|
||||
@@ -1,9 +0,0 @@
|
||||
export interface SearchMatch {
|
||||
start: number
|
||||
end: number
|
||||
}
|
||||
|
||||
export interface SearchOptions {
|
||||
caseSensitive: boolean
|
||||
useRegex: boolean
|
||||
}
|
||||
Reference in New Issue
Block a user