diff --git a/package.json b/package.json
index 26981d1..9422da1 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "marklite",
- "version": "0.2.0",
+ "version": "0.3.0",
"description": "Lightweight Markdown Editor for Windows",
"main": "./dist/main/index.js",
"scripts": {
@@ -28,13 +28,13 @@
"author": "MarkLite",
"license": "MIT",
"dependencies": {
- "@codemirror/commands": "^6.10.3",
- "@codemirror/lang-markdown": "^6.5.0",
- "@codemirror/language": "^6.12.3",
- "@codemirror/search": "^6.7.0",
- "@codemirror/state": "^6.6.0",
- "@codemirror/theme-one-dark": "^6.1.3",
- "@codemirror/view": "^6.43.0",
+ "@milkdown/core": "^7.21.1",
+ "@milkdown/ctx": "^7.21.1",
+ "@milkdown/preset-commonmark": "^7.21.1",
+ "@milkdown/preset-gfm": "^7.21.1",
+ "@milkdown/prose": "^7.21.2",
+ "@milkdown/react": "^7.21.1",
+ "@milkdown/utils": "^7.21.1",
"dexie": "^4.0.11",
"nanoid": "^5.1.5",
"react": "^18.3.1",
diff --git a/src/renderer/components/Editor/Editor.tsx b/src/renderer/components/Editor/Editor.tsx
index 8a1bd7c..de448fe 100644
--- a/src/renderer/components/Editor/Editor.tsx
+++ b/src/renderer/components/Editor/Editor.tsx
@@ -1,6 +1,8 @@
import React, { useEffect, useCallback } from 'react'
+import { callCommand } from '@milkdown/utils'
+import { toggleStrongCommand, toggleEmphasisCommand } from '@milkdown/preset-commonmark'
import { useTabStore } from '../../stores/tabStore'
-import { useCodeMirror } from './useCodeMirror'
+import { useMilkdown } from './useMilkdown'
import { EditorToolbar } from './EditorToolbar'
interface EditorProps {
@@ -16,13 +18,13 @@ export function Editor({ darkMode }: EditorProps) {
const {
containerRef,
- viewRef,
+ action,
setContent,
getScrollTop,
setScrollTop,
getSelection,
setSelection
- } = useCodeMirror({
+ } = useMilkdown({
content: activeTab?.content ?? '',
onChange: useCallback((value: string) => {
if (!activeTabId) return
@@ -32,17 +34,18 @@ export function Editor({ darkMode }: EditorProps) {
darkMode
})
- // 切换标签时加载内容
+ // Load content when switching tabs
useEffect(() => {
if (!activeTab) return
setContent(activeTab.content)
requestAnimationFrame(() => {
setScrollTop(activeTab.scrollTop)
- setSelection(activeTab.selectionStart, activeTab.selectionEnd)
+ setSelection()
})
+ // eslint-disable-next-line react-hooks/exhaustive-deps
}, [activeTabId])
- // 保存当前标签状态
+ // Save current tab state on unmount
useEffect(() => {
return () => {
if (!activeTabId) return
@@ -52,43 +55,34 @@ export function Editor({ darkMode }: EditorProps) {
selectionEnd: getSelection().to
})
}
+ // eslint-disable-next-line react-hooks/exhaustive-deps
}, [activeTabId])
- // Ctrl+B 粗体, Ctrl+I 斜体
+ // Ctrl+B bold, Ctrl+I italic
useEffect(() => {
const handleKeyDown = (e: KeyboardEvent) => {
const isCtrl = e.ctrlKey || e.metaKey
if (isCtrl && e.key === 'b') {
e.preventDefault()
- const view = viewRef.current
- if (!view) return
- const { from, to } = view.state.selection.main
- const selected = view.state.sliceDoc(from, to) || '粗体'
- view.dispatch({
- changes: { from, to, insert: `**${selected}**` },
- selection: { anchor: from + 2, head: from + 2 + selected.length }
+ action((editor) => {
+ editor.action(callCommand(toggleStrongCommand.key))
})
}
if (isCtrl && e.key === 'i') {
e.preventDefault()
- const view = viewRef.current
- if (!view) return
- const { from, to } = view.state.selection.main
- const selected = view.state.sliceDoc(from, to) || '斜体'
- view.dispatch({
- changes: { from, to, insert: `*${selected}*` },
- selection: { anchor: from + 1, head: from + 1 + selected.length }
+ action((editor) => {
+ editor.action(callCommand(toggleEmphasisCommand.key))
})
}
}
document.addEventListener('keydown', handleKeyDown)
return () => document.removeEventListener('keydown', handleKeyDown)
- }, [viewRef])
+ }, [action])
return (
)
}
diff --git a/src/renderer/components/Editor/EditorToolbar.tsx b/src/renderer/components/Editor/EditorToolbar.tsx
index 5bd067c..e9bd26d 100644
--- a/src/renderer/components/Editor/EditorToolbar.tsx
+++ b/src/renderer/components/Editor/EditorToolbar.tsx
@@ -1,100 +1,157 @@
import React, { useCallback } from 'react'
-import type { EditorView } from '@codemirror/view'
+import type { Editor } from '@milkdown/core'
+import { callCommand } from '@milkdown/utils'
+import type { $Command } from '@milkdown/utils'
+import {
+ toggleStrongCommand,
+ toggleEmphasisCommand,
+ wrapInHeadingCommand,
+ wrapInBlockquoteCommand,
+ wrapInBulletListCommand,
+ wrapInOrderedListCommand,
+ createCodeBlockCommand,
+ insertImageCommand,
+ toggleLinkCommand,
+ insertHrCommand
+} from '@milkdown/preset-commonmark'
+import { toggleStrikethroughCommand } from '@milkdown/preset-gfm'
interface EditorToolbarProps {
- viewRef: React.MutableRefObject
+ action: (fn: (editor: Editor) => void) => void
}
-export const EditorToolbar = React.memo(function EditorToolbar({ viewRef }: EditorToolbarProps) {
- const insertFormatting = useCallback((before: string, after: string, placeholder: string) => {
- const view = viewRef.current
- if (!view) return
- const { from, to } = view.state.selection.main
- const selected = view.state.sliceDoc(from, to)
- const text = selected || placeholder
- const insert = before + text + after
- view.dispatch({
- changes: { from, to, insert },
- selection: { anchor: from + before.length, head: from + before.length + text.length }
+export const EditorToolbar = React.memo(function EditorToolbar({ action }: EditorToolbarProps) {
+ const exec = useCallback((command: $Command, payload?: T) => {
+ action((editor) => {
+ try {
+ editor.action(callCommand(command.key, payload))
+ } catch {
+ // command may not be registered
+ }
})
- view.focus()
- }, [viewRef])
+ }, [action])
- const insertLinePrefix = useCallback((prefix: string) => {
- const view = viewRef.current
- if (!view) return
- const { from } = view.state.selection.main
- const line = view.state.doc.lineAt(from)
- const currentLine = view.state.sliceDoc(line.from, line.to)
- if (currentLine.startsWith(prefix)) {
- view.dispatch({
- changes: { from: line.from, to: line.from + prefix.length, insert: '' }
- })
- } else {
- view.dispatch({
- changes: { from: line.from, to: line.from, insert: prefix }
- })
- }
- view.focus()
- }, [viewRef])
-
- const insertBlock = useCallback((text: string) => {
- const view = viewRef.current
- if (!view) return
- const { from } = view.state.selection.main
- const line = view.state.doc.lineAt(from)
- const insertPos = line.to + 1
- view.dispatch({
- changes: { from: insertPos, to: insertPos, insert: '\n' + text + '\n' },
- selection: { anchor: insertPos + 1, head: insertPos + 1 + text.length }
- })
- view.focus()
- }, [viewRef])
+ const handleHeading = useCallback((level: number) => {
+ exec(wrapInHeadingCommand, level)
+ }, [exec])
return (
-
)
})
diff --git a/src/renderer/components/Editor/useCodeMirror.ts b/src/renderer/components/Editor/useCodeMirror.ts
deleted file mode 100644
index 0dc3807..0000000
--- a/src/renderer/components/Editor/useCodeMirror.ts
+++ /dev/null
@@ -1,155 +0,0 @@
-import { useEffect, useRef, useCallback } from 'react'
-import { EditorState, Compartment, type Extension } from '@codemirror/state'
-import { EditorView, keymap, lineNumbers, highlightActiveLine, highlightSpecialChars, drawSelection, rectangularSelection } from '@codemirror/view'
-import { defaultKeymap, history, historyKeymap, indentWithTab } from '@codemirror/commands'
-import { markdown, markdownLanguage } from '@codemirror/lang-markdown'
-import { syntaxHighlighting, defaultHighlightStyle, indentOnInput, bracketMatching, foldGutter } from '@codemirror/language'
-import { search, searchKeymap, highlightSelectionMatches } from '@codemirror/search'
-import { oneDark } from '@codemirror/theme-one-dark'
-
-interface UseCodeMirrorOptions {
- content: string
- onChange: (value: string) => void
- darkMode: boolean
-}
-
-// PF-04: 创建主题 Compartment,用于动态切换主题而不重建 EditorView
-const themeCompartment = new Compartment()
-
-export function useCodeMirror({ content, onChange, darkMode }: UseCodeMirrorOptions) {
- const containerRef = useRef(null)
- const viewRef = useRef(null)
- const isExternalUpdate = useRef(false)
- const onChangeRef = useRef(onChange)
- const darkModeRef = useRef(darkMode)
-
- // 始终保持 onChangeRef 为最新回调
- useEffect(() => {
- onChangeRef.current = onChange
- }, [onChange])
-
- // PF-04: darkMode 变化时通过 Compartment.reconfigure() 切换主题
- useEffect(() => {
- darkModeRef.current = darkMode
- const view = viewRef.current
- if (!view) return
- view.dispatch({
- effects: themeCompartment.reconfigure(darkMode ? oneDark : [])
- })
- }, [darkMode])
-
- // 初始化编辑器(仅在首次挂载时执行)
- useEffect(() => {
- if (!containerRef.current) return
-
- const zhPhrases = EditorState.phrases.of({
- 'Find': '查找',
- 'Replace': '替换',
- 'next': '下一个',
- 'previous': '上一个',
- 'all': '全部',
- 'match case': '区分大小写',
- 'regexp': '正则',
- 'by word': '全字匹配',
- 'replace': '替换',
- 'replace all': '全部替换',
- 'close': '关闭',
- })
-
- const extensions: Extension[] = [
- zhPhrases,
- search({ top: true }),
- lineNumbers(),
- highlightActiveLine(),
- highlightSpecialChars(),
- drawSelection(),
- rectangularSelection(),
- history(),
- indentOnInput(),
- bracketMatching(),
- foldGutter(),
- highlightSelectionMatches(),
- keymap.of([
- ...defaultKeymap,
- ...historyKeymap,
- ...searchKeymap,
- indentWithTab
- ]),
- markdown({ base: markdownLanguage }),
- syntaxHighlighting(defaultHighlightStyle),
- EditorView.lineWrapping,
- EditorView.updateListener.of((update) => {
- if (update.docChanged && !isExternalUpdate.current) {
- onChangeRef.current(update.state.doc.toString())
- }
- }),
- themeCompartment.of(darkModeRef.current ? oneDark : [])
- ]
-
- const state = EditorState.create({
- doc: content,
- extensions
- })
-
- const view = new EditorView({
- state,
- parent: containerRef.current
- })
-
- viewRef.current = view
-
- return () => {
- view.destroy()
- viewRef.current = null
- }
- // eslint-disable-next-line react-hooks/exhaustive-deps
- }, [])
-
- // 外部内容更新(切换标签时)
- const setContent = useCallback((newContent: string) => {
- const view = viewRef.current
- if (!view) return
- const current = view.state.doc.toString()
- if (current !== newContent) {
- isExternalUpdate.current = true
- view.dispatch({
- changes: { from: 0, to: current.length, insert: newContent }
- })
- isExternalUpdate.current = false
- }
- }, [])
-
- const getScrollTop = useCallback((): number => {
- return viewRef.current?.scrollDOM.scrollTop ?? 0
- }, [])
-
- const setScrollTop = useCallback((top: number) => {
- if (viewRef.current) {
- viewRef.current.scrollDOM.scrollTop = top
- }
- }, [])
-
- const getSelection = useCallback((): { from: number; to: number } => {
- const view = viewRef.current
- if (!view) return { from: 0, to: 0 }
- const ranges = view.state.selection.ranges
- return { from: ranges[0].from, to: ranges[0].to }
- }, [])
-
- const setSelection = useCallback((from: number, to: number) => {
- const view = viewRef.current
- if (!view) return
- view.dispatch({ selection: { anchor: from, head: to } })
- view.focus()
- }, [])
-
- return {
- containerRef,
- viewRef,
- setContent,
- getScrollTop,
- setScrollTop,
- getSelection,
- setSelection
- }
-}
diff --git a/src/renderer/components/Editor/useMilkdown.ts b/src/renderer/components/Editor/useMilkdown.ts
new file mode 100644
index 0000000..e67ddb2
--- /dev/null
+++ b/src/renderer/components/Editor/useMilkdown.ts
@@ -0,0 +1,159 @@
+import { useCallback, useRef, useEffect } from 'react'
+import { Editor, rootCtx, defaultValueCtx } from '@milkdown/core'
+import { replaceAll as milkdownReplaceAll } from '@milkdown/utils'
+import { commonmark } from '@milkdown/preset-commonmark'
+import { gfm } from '@milkdown/preset-gfm'
+import { history } from '@milkdown/plugin-history'
+import { listener, listenerCtx } from '@milkdown/plugin-listener'
+import { indent } from '@milkdown/plugin-indent'
+import { trailing } from '@milkdown/plugin-trailing'
+import { clipboard } from '@milkdown/plugin-clipboard'
+
+interface UseMilkdownOptions {
+ content: string
+ onChange: (value: string) => void
+ darkMode: boolean
+}
+
+export function useMilkdown({ content, onChange, darkMode }: UseMilkdownOptions) {
+ const containerRef = useRef(null)
+ const editorRef = useRef(null)
+ const onChangeRef = useRef(onChange)
+ const isExternalUpdate = useRef(false)
+ const initialContentRef = useRef(content)
+
+ // Keep onChangeRef fresh
+ useEffect(() => {
+ onChangeRef.current = onChange
+ }, [onChange])
+
+ // Initialize editor
+ useEffect(() => {
+ if (!containerRef.current) return
+
+ const editor = Editor.make()
+ .config((ctx) => {
+ ctx.set(rootCtx, containerRef.current!)
+ ctx.set(defaultValueCtx, initialContentRef.current)
+
+ // Configure listener for content changes
+ const lm = ctx.get(listenerCtx)
+ lm.markdownUpdated((_ctx, markdown, prevMarkdown) => {
+ if (markdown === prevMarkdown) return
+ if (!isExternalUpdate.current) {
+ onChangeRef.current(markdown)
+ }
+ })
+
+ // Listen for blur events (used for potential future state saving)
+ lm.blur(() => {
+ // blur handler placeholder
+ })
+ })
+ .use(commonmark)
+ .use(gfm)
+ .use(history)
+ .use(listener)
+ .use(indent)
+ .use(trailing)
+ .use(clipboard)
+
+ editor.create().then((created) => {
+ editorRef.current = created
+ })
+
+ return () => {
+ editorRef.current?.destroy()
+ editorRef.current = null
+ }
+ }, [])
+
+ // Dark mode: update CSS custom properties on the container
+ useEffect(() => {
+ const container = containerRef.current
+ if (!container) return
+ if (darkMode) {
+ container.classList.add('milkdown-dark')
+ } else {
+ container.classList.remove('milkdown-dark')
+ }
+ }, [darkMode])
+
+ // External content update (tab switch)
+ const setContent = useCallback((newContent: string) => {
+ const editor = editorRef.current
+ if (!editor) return
+
+ isExternalUpdate.current = true
+ try {
+ editor.action(milkdownReplaceAll(newContent))
+ } catch {
+ // replaceAll may fail if editor is not fully ready
+ } finally {
+ isExternalUpdate.current = false
+ }
+ }, [])
+
+ // Scroll position management
+ const getScrollTop = useCallback((): number => {
+ const container = containerRef.current
+ if (!container) return 0
+ const scroller = container.querySelector('.milkdown .editor') as HTMLElement | null
+ return scroller?.scrollTop ?? container.scrollTop ?? 0
+ }, [])
+
+ const setScrollTop = useCallback((top: number) => {
+ const container = containerRef.current
+ if (!container) return
+ const scroller = container.querySelector('.milkdown .editor') as HTMLElement | null
+ if (scroller) {
+ scroller.scrollTop = top
+ } else {
+ container.scrollTop = top
+ }
+ }, [])
+
+ // Selection management
+ const getSelection = useCallback((): { from: number; to: number } => {
+ const editor = editorRef.current
+ if (!editor) return { from: 0, to: 0 }
+ try {
+ return editor.action((ctx) => {
+ const view = ctx.get('editorView' as never) as { state?: { selection?: { from: number; to: number } } } | undefined
+ if (view?.state?.selection) {
+ return { from: view.state.selection.from, to: view.state.selection.to }
+ }
+ return { from: 0, to: 0 }
+ })
+ } catch {
+ return { from: 0, to: 0 }
+ }
+ }, [])
+
+ const setSelection = useCallback(() => {
+ // ProseMirror selection setting requires the view instance
+ // which we access lazily; for now we focus the editor
+ const container = containerRef.current
+ if (!container) return
+ const pmEditor = container.querySelector('.ProseMirror') as HTMLElement | null
+ pmEditor?.focus()
+ }, [])
+
+ // Execute a Milkdown action (for toolbar commands)
+ const action = useCallback((fn: (editor: Editor) => void) => {
+ const editor = editorRef.current
+ if (!editor) return
+ fn(editor)
+ }, [])
+
+ return {
+ containerRef,
+ editorRef,
+ action,
+ setContent,
+ getScrollTop,
+ setScrollTop,
+ getSelection,
+ setSelection
+ }
+}
diff --git a/src/renderer/styles/global.css b/src/renderer/styles/global.css
index 6b41265..1300775 100644
--- a/src/renderer/styles/global.css
+++ b/src/renderer/styles/global.css
@@ -161,311 +161,139 @@ html, body {
margin: 0 4px;
}
-/* CodeMirror Wrapper */
-.codemirror-wrapper {
+/* Milkdown Wrapper */
+.milkdown-wrapper {
flex: 1;
overflow: hidden;
+ height: 100%;
}
-.codemirror-wrapper .cm-editor {
+.milkdown-wrapper .milkdown {
height: 100%;
font-family: var(--font-mono);
font-size: 13px;
+ color: var(--text);
+ background: var(--bg);
}
-.codemirror-wrapper .cm-editor .cm-scroller {
+.milkdown-wrapper .milkdown .editor {
overflow: auto;
+ height: 100%;
+ padding: 12px 16px;
+ outline: none;
+ user-select: text;
+ -webkit-user-select: text;
+ line-height: 1.7;
}
-.codemirror-wrapper .cm-editor .cm-content {
- padding: 12px 0;
+.milkdown-wrapper .milkdown .editor p {
+ margin: 0.5em 0;
}
-.codemirror-wrapper .cm-editor .cm-line {
- padding: 0 16px;
+.milkdown-wrapper .milkdown .editor h1,
+.milkdown-wrapper .milkdown .editor h2,
+.milkdown-wrapper .milkdown .editor h3,
+.milkdown-wrapper .milkdown .editor h4,
+.milkdown-wrapper .milkdown .editor h5,
+.milkdown-wrapper .milkdown .editor h6 {
+ font-family: var(--font-ui);
+ font-weight: 600;
+ margin: 1em 0 0.5em;
+ line-height: 1.3;
+ color: var(--text);
}
-.codemirror-wrapper .cm-editor .cm-gutters {
- background: var(--bg-secondary);
- border-right: 1px solid var(--border-light);
- color: var(--text-tertiary);
- font-size: 12px;
- min-width: 40px;
-}
+.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; }
-.codemirror-wrapper .cm-editor .cm-activeLineGutter {
- background: var(--bg-tertiary);
+.milkdown-wrapper .milkdown .editor blockquote {
+ border-left: 3px solid var(--primary);
+ padding-left: 12px;
+ margin: 0.5em 0;
color: var(--text-secondary);
}
-.codemirror-wrapper .cm-editor .cm-activeLine {
+.milkdown-wrapper .milkdown .editor code {
+ font-family: var(--font-mono);
background: var(--bg-tertiary);
+ padding: 2px 6px;
+ border-radius: 3px;
+ font-size: 0.9em;
}
-.codemirror-wrapper .cm-editor.cm-focused {
+.milkdown-wrapper .milkdown .editor pre {
+ background: var(--bg-secondary);
+ border: 1px solid var(--border-light);
+ border-radius: var(--radius);
+ padding: 12px 16px;
+ overflow-x: auto;
+ margin: 0.5em 0;
+}
+
+.milkdown-wrapper .milkdown .editor pre code {
+ background: transparent;
+ padding: 0;
+ border-radius: 0;
+}
+
+.milkdown-wrapper .milkdown .editor ul,
+.milkdown-wrapper .milkdown .editor ol {
+ padding-left: 1.5em;
+ margin: 0.5em 0;
+}
+
+.milkdown-wrapper .milkdown .editor li {
+ margin: 0.25em 0;
+}
+
+.milkdown-wrapper .milkdown .editor hr {
+ border: none;
+ border-top: 1px solid var(--border);
+ margin: 1.5em 0;
+}
+
+.milkdown-wrapper .milkdown .editor a {
+ color: var(--primary);
+ text-decoration: none;
+}
+
+.milkdown-wrapper .milkdown .editor a:hover {
+ text-decoration: underline;
+}
+
+.milkdown-wrapper .milkdown .editor img {
+ max-width: 100%;
+ border-radius: var(--radius);
+}
+
+/* ProseMirror selection */
+.milkdown-wrapper .milkdown .editor .ProseMirror-selectednode {
+ outline: 2px solid var(--primary);
+ border-radius: 2px;
+}
+
+.milkdown-wrapper .milkdown .editor ::selection {
+ background: rgba(26, 115, 232, 0.3);
+}
+
+:root.dark .milkdown-wrapper .milkdown .editor ::selection {
+ background: rgba(100, 160, 255, 0.35);
+}
+
+/* ProseMirror cursor */
+.milkdown-wrapper .milkdown .editor .ProseMirror-focused {
outline: none;
}
-/* ===== CodeMirror 搜索样式美化 ===== */
-
-/* 搜索面板移到顶部 */
-.cm-panels {
- top: 0 !important;
- bottom: auto !important;
+/* Dark mode styles for Milkdown */
+.milkdown-wrapper.milkdown-dark .milkdown {
+ background: var(--bg);
+ color: var(--text);
}
-.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;
- border-bottom: 1px solid var(--border) !important;
- padding: 8px 12px !important;
- font-family: var(--font-ui) !important;
- font-size: 13px !important;
- box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08);
-}
-
-/* 搜索输入框 */
-.cm-panel .cm-textfield {
- background: var(--bg-secondary) !important;
- border: 1px solid var(--border) !important;
- border-radius: 4px !important;
- padding: 4px 8px !important;
- font-size: 13px !important;
- font-family: var(--font-ui) !important;
- color: var(--text) !important;
- outline: none !important;
- transition: border-color 0.15s ease !important;
- height: 28px !important;
-}
-
-.cm-panel .cm-textfield:focus {
- border-color: var(--primary) !important;
- box-shadow: 0 0 0 2px rgba(26, 115, 232, 0.15) !important;
-}
-
-/* 搜索按钮 */
-.cm-panel .cm-button {
- background: var(--bg-secondary) !important;
- border: 1px solid var(--border) !important;
- border-radius: 4px !important;
- color: var(--text-secondary) !important;
- font-size: 12px !important;
- font-family: var(--font-ui) !important;
- padding: 4px 10px !important;
- cursor: pointer !important;
- transition: all 0.15s ease !important;
- height: 28px !important;
- line-height: 1 !important;
-}
-
-.cm-panel .cm-button:hover {
- background: var(--bg-tertiary) !important;
- color: var(--text) !important;
- border-color: var(--text-tertiary) !important;
-}
-
-.cm-panel .cm-button:active {
- background: var(--primary-light) !important;
- color: var(--primary) !important;
-}
-
-/* 搜索选项复选框(区分大小写、正则、全字匹配) */
-.cm-search label {
- display: inline-flex !important;
- align-items: center !important;
- gap: 5px !important;
- font-size: 13px !important;
- font-family: var(--font-ui) !important;
- color: var(--text-secondary) !important;
- cursor: pointer !important;
- user-select: none !important;
- vertical-align: middle !important;
- line-height: 1 !important;
- margin: 0 6px 0 0 !important;
- padding: 2px 0 !important;
-}
-
-.cm-search input[type="checkbox"] {
- -webkit-appearance: none !important;
- appearance: none !important;
- width: 18px !important;
- height: 18px !important;
- border: 1.5px solid var(--border) !important;
- border-radius: 4px !important;
- background: var(--bg) !important;
- cursor: pointer !important;
- position: relative !important;
- flex-shrink: 0 !important;
- vertical-align: middle !important;
- margin: 0 !important;
- transition: all 0.15s ease !important;
-}
-
-.cm-search input[type="checkbox"]:hover {
- border-color: var(--primary) !important;
-}
-
-.cm-search input[type="checkbox"]:checked {
- background: var(--primary) !important;
- border-color: var(--primary) !important;
-}
-
-.cm-search input[type="checkbox"]:checked::after {
- content: '' !important;
- position: absolute !important;
- left: 5px !important;
- top: 2px !important;
- width: 6px !important;
- height: 10px !important;
- border: solid white !important;
- border-width: 0 2px 2px 0 !important;
- transform: rotate(45deg) !important;
-}
-
-/* 搜索结果计数 */
-.cm-panel .cm-search-info {
- color: var(--text-tertiary) !important;
- font-size: 11px !important;
- font-family: var(--font-ui) !important;
- padding: 0 8px !important;
- white-space: nowrap !important;
-}
-
-/* 关闭按钮 — 注意:CodeMirror 关闭按钮用的是 name="close",没有 cm-close class */
-.cm-panel [name="close"] {
- position: absolute !important;
- top: 4px !important;
- right: 4px !important;
- color: var(--text-tertiary) !important;
- font-size: 22px !important;
- line-height: 1 !important;
- cursor: pointer !important;
- padding: 4px 6px !important;
- border-radius: 4px !important;
- border: none !important;
- background: transparent !important;
- transition: all 0.15s ease !important;
- z-index: 1 !important;
-}
-
-.cm-panel [name="close"]:hover {
- background: var(--bg-tertiary) !important;
- color: var(--text) !important;
-}
-
-/* 替换按钮组 */
-.cm-panel .cm-replace,
-.cm-panel .cm-replaceAll {
- background: var(--bg-secondary) !important;
- border: 1px solid var(--border) !important;
- border-radius: 4px !important;
- color: var(--text-secondary) !important;
- font-size: 12px !important;
- padding: 4px 8px !important;
- cursor: pointer !important;
- transition: all 0.15s ease !important;
-}
-
-.cm-panel .cm-replace:hover,
-.cm-panel .cm-replaceAll:hover {
- background: var(--bg-tertiary) !important;
- color: var(--text) !important;
-}
-
-/* 搜索高亮匹配 */
-.cm-searchMatch {
- background: rgba(255, 220, 0, 0.35) !important;
- border-radius: 2px !important;
- outline: 1px solid rgba(255, 220, 0, 0.5) !important;
-}
-
-.cm-searchMatch.cm-searchMatch-selected {
- background: rgba(255, 150, 0, 0.55) !important;
- outline: 2px solid rgba(255, 150, 0, 0.8) !important;
-}
-
-/* 暗色主题适配 */
-:root.dark .cm-searchMatch {
- background: rgba(255, 220, 0, 0.2) !important;
- outline-color: rgba(255, 220, 0, 0.3) !important;
-}
-
-:root.dark .cm-searchMatch.cm-searchMatch-selected {
- background: rgba(255, 150, 0, 0.35) !important;
- outline-color: rgba(255, 150, 0, 0.6) !important;
-}
-
-:root.dark .cm-panel {
- background: var(--bg-secondary) !important;
- border-color: var(--border) !important;
- box-shadow: 0 2px 8px rgba(0, 0, 0, 0.3) !important;
-}
-
-:root.dark .cm-panel .cm-textfield {
- background: var(--bg-tertiary) !important;
- border-color: var(--border) !important;
-}
-
-:root.dark .cm-panel .cm-button,
-:root.dark .cm-panel .cm-replace,
-:root.dark .cm-panel .cm-replaceAll {
- background: var(--bg-tertiary) !important;
- border-color: var(--border) !important;
- color: var(--text-secondary) !important;
-}
-
-:root.dark .cm-panel .cm-button:hover,
-:root.dark .cm-panel .cm-replace:hover,
-:root.dark .cm-panel .cm-replaceAll:hover {
- background: var(--border) !important;
- color: var(--text) !important;
-}
-
-/* Fold gutter */
-.cm-foldGutter .cm-gutterElement {
- cursor: pointer;
- opacity: 0.5;
-}
-
-.cm-foldGutter .cm-gutterElement:hover {
- opacity: 1;
-}
-
-/* Selection — 必须匹配oneDark主题的选择器特异性才能覆盖 */
-.cm-selectionBackground {
- background: rgba(26, 115, 232, 0.25) !important;
-}
-
-.cm-editor.cm-focused > .cm-scroller > .cm-selectionLayer .cm-selectionBackground,
-.cm-editor.cm-focused .cm-selectionBackground,
-.cm-content ::selection {
- background: rgba(26, 115, 232, 0.3) !important;
-}
-
-:root.dark .cm-selectionBackground {
- background: rgba(100, 160, 255, 0.25) !important;
-}
-
-:root.dark .cm-editor.cm-focused > .cm-scroller > .cm-selectionLayer .cm-selectionBackground,
-:root.dark .cm-editor.cm-focused .cm-selectionBackground,
-:root.dark .cm-content ::selection {
- background: rgba(100, 160, 255, 0.35) !important;
-}
-
-.cm-editor .cm-content {
- user-select: text !important;
- -webkit-user-select: text !important;
-}
-
-.cm-editor .cm-cursor {
- border-left-color: var(--text);
+.milkdown-wrapper.milkdown-dark .milkdown .editor blockquote {
+ border-left-color: var(--primary);
}
/* Preview Panel */