From b1803c6467b699e602d1f2ebff6831022d770d69 Mon Sep 17 00:00:00 2001 From: thzxx Date: Thu, 4 Jun 2026 09:39:10 +0800 Subject: [PATCH] feat: replace CodeMirror 6 with Milkdown 7.21 editor - Remove 7 @codemirror/* packages, add 7 @milkdown/* packages - Rewrite useCodeMirror -> useMilkdown hook (ProseMirror-based) - Rewrite Editor.tsx with Milkdown lifecycle management - Update EditorToolbar to use Milkdown callCommand API - Replace CodeMirror CSS styles with Milkdown/ProseMirror styles - Support: CommonMark + GFM, dark mode, toolbar formatting - Bump version 0.2.0 -> 0.3.0 --- package.json | 16 +- src/renderer/components/Editor/Editor.tsx | 42 +- .../components/Editor/EditorToolbar.tsx | 173 +++++--- .../components/Editor/useCodeMirror.ts | 155 ------- src/renderer/components/Editor/useMilkdown.ts | 159 ++++++++ src/renderer/styles/global.css | 382 +++++------------- 6 files changed, 405 insertions(+), 522 deletions(-) delete mode 100644 src/renderer/components/Editor/useCodeMirror.ts create mode 100644 src/renderer/components/Editor/useMilkdown.ts 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 */