release: v0.4.1 — MetonaEditor 0.1.10 升级与三主题系统

- chore: 升级 @metona-team/metona-editor 0.1.3 → 0.1.10
- feat: 三主题系统(亮色/暗色/暖色),循环切换
- feat: 应用颜色自动同步编辑器主题(MeEditor.themes.exportCSSVars)
- feat: 启用编辑器新功能(lineNumbers/autoBrackets/readOnly/autofocus/exportTool)
- refactor: 主题从 boolean darkMode 重构为 ThemeMode 枚举
- refactor: 插件注册改用字符串数组(与 demo 一致)
- refactor: 主题同步从手动 CSS 覆写改为 editor.setTheme() 实例 API
- refactor: Ctrl+S 双通道(编辑器 onSave + 全局兜底)+ 防重入锁
- style: 移除 variables.css 中硬编码 .dark 色值,改为 JS 动态驱动
- test: 更新 editorStore 测试覆盖新的 themeMode/cycleTheme
This commit is contained in:
2026-07-24 13:58:37 +08:00
parent 10cfe0066b
commit 7bd625b9ce
20 changed files with 712 additions and 504 deletions
+40 -48
View File
@@ -1,13 +1,21 @@
import React, { useEffect, useRef } from 'react'
import MeEditor from '@metona-team/metona-editor'
import type { MarkdownEditor, EditMode, PluginObject } from '@metona-team/metona-editor'
import type { MarkdownEditor, MarkdownEditorOptions, EditMode, ThemeName } from '@metona-team/metona-editor'
import { useTabStore } from '../../stores/tabStore'
import { setMetonaEditorGetter, useEditorStore } from '../../stores/editorStore'
import { settingsRepository } from '../../db/settingsRepository'
import { renderMarkdownSync } from '../../lib/markdown'
import type { ThemeMode } from '../../types/settings'
// v0.1.9 demo 验证过但类型声明未收录的配置项
interface ExtendedEditorOptions extends MarkdownEditorOptions {
lineNumbers?: boolean
autoBrackets?: boolean
}
interface EditorProps {
darkMode: boolean
themeMode: ThemeMode
onAppSave?: () => void
}
/** 将应用 viewMode 映射到 MetonaEditor 的 mode */
@@ -24,26 +32,17 @@ function reverseMapMode(mode: EditMode): 'editor' | 'preview' | 'source' {
return 'editor'
}
/** 组装实例级插件列表 */
function buildPlugins(): PluginObject[] {
const plugins: PluginObject[] = []
// 搜索/替换(Ctrl+F / Ctrl+H
if (MeEditor.presetPlugins?.searchReplace) {
plugins.push(MeEditor.presetPlugins.searchReplace)
}
// 粘贴图片 → base64
if (MeEditor.presetPlugins?.imagePaste) {
plugins.push(MeEditor.presetPlugins.imagePaste)
}
return plugins
}
/** 预设插件(字符串形式,与 demo 一致) */
const EDITOR_PLUGINS: string[] = [
'searchReplace', // Ctrl+F/Ctrl+H
'imagePaste', // Ctrl+V 粘贴图片
'exportTool', // 导出 MD/HTML
]
/**
* Editor 组件 — 基于 MetonaEditor 的 Markdown 编辑器。
*/
export const Editor = React.memo(function Editor({ darkMode }: EditorProps) {
export const Editor = React.memo(function Editor({ themeMode, onAppSave }: EditorProps) {
const activeTab = useTabStore(s => s.getActiveTab())
const activeTabId = useTabStore(s => s.activeTabId)
const updateTabContent = useTabStore(s => s.updateTabContent)
@@ -65,7 +64,7 @@ export const Editor = React.memo(function Editor({ darkMode }: EditorProps) {
const container = containerRef.current
if (!container) return
const editor = MeEditor.create(container, {
const config: ExtendedEditorOptions = {
value: activeTab?.content ?? '',
mode: mapViewMode(viewMode),
height: '100%',
@@ -78,12 +77,16 @@ export const Editor = React.memo(function Editor({ darkMode }: EditorProps) {
'edit', 'split', 'preview', 'fullscreen'
],
locale: 'zh-CN',
theme: darkMode ? 'dark' : 'light',
theme: themeMode as ThemeName,
placeholder: '在此输入 Markdown 内容...',
spellcheck: false,
tabSize: 2,
wordCount: true,
plugins: buildPlugins(),
autofocus: true,
lineNumbers: true,
autoBrackets: true,
readOnly: viewMode === 'preview',
plugins: EDITOR_PLUGINS,
// 使用 unified 管线渲染,保留图片路径修复能力
render: (md: string) => {
@@ -110,8 +113,15 @@ export const Editor = React.memo(function Editor({ darkMode }: EditorProps) {
const mapped = reverseMapMode(mode)
setViewMode(mapped)
settingsRepository.save({ viewMode: mapped })
},
// Ctrl+S → 触发应用层保存(Electron IPC 写文件系统)
onSave: () => {
onAppSave?.()
}
})
}
const editor = MeEditor.create(container, config)
editorRef.current = editor
setMetonaEditorGetter(() => editor)
@@ -165,37 +175,17 @@ export const Editor = React.memo(function Editor({ darkMode }: EditorProps) {
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [activeTabId])
// ── 暗色模式同步 ──────────────────────────────────────────
// ── 主题同步 ──────────────────────────────────────────
useEffect(() => {
try {
const themeName = darkMode ? 'dark' : 'light'
// 1. 全局主题(documentElement + localStorage
MeEditor.setTheme(themeName)
// 2. 覆写当前实例 wrapper 的 inline CSS 变量
// MeEditor.setTheme 只更新 documentElement,不更新 wrapper
// 而 create() 时写入的 inline 变量优先级最高,必须手动覆写
const wrapper = containerRef.current?.querySelector('.me-wrapper') as HTMLElement | null
if (wrapper) {
const config: Record<string, string> = MeEditor.themes.getThemeConfig(themeName) as Record<string, string>
wrapper.style.setProperty('--md-bg', config.bg)
wrapper.style.setProperty('--md-text', config.text)
wrapper.style.setProperty('--md-border', config.border)
wrapper.style.setProperty('--md-shadow', config.shadow)
if (config.hoverShadow) wrapper.style.setProperty('--md-hover-shadow', config.hoverShadow)
if (config.toolbarBg) wrapper.style.setProperty('--md-toolbar-bg', config.toolbarBg)
if (config.textareaBg) wrapper.style.setProperty('--md-textarea-bg', config.textareaBg)
if (config.previewBg) wrapper.style.setProperty('--md-preview-bg', config.previewBg)
if (config.codeBg) wrapper.style.setProperty('--md-code-bg', config.codeBg)
if (config.codeText) wrapper.style.setProperty('--md-code-text', config.codeText)
if (config.accent) wrapper.style.setProperty('--md-accent', config.accent)
if (config.muted) wrapper.style.setProperty('--md-muted', config.muted)
}
// 全局主题(documentElement + localStorage
MeEditor.setTheme(themeMode)
// v0.1.5+: 实例级主题,自动处理 wrapper CSS 变量
editorRef.current?.setTheme(themeMode)
} catch {
// 容错
}
}, [darkMode])
}, [themeMode])
// ── 视图模式同步 ──────────────────────────────────────────
useEffect(() => {
@@ -205,6 +195,8 @@ export const Editor = React.memo(function Editor({ darkMode }: EditorProps) {
if (editor.getMode() !== targetMode) {
editor.setMode(targetMode)
}
// 预览模式设为只读
editor.setReadOnly(viewMode === 'preview')
}, [viewMode])
return (
+21 -6
View File
@@ -1,25 +1,34 @@
import React from 'react'
import { FolderOpen, Save, Moon, Sun, Info } from '../Icons'
import type { ThemeMode } from '../../types/settings'
interface ToolbarProps {
onOpen: () => void
onSave: () => void
darkMode: boolean
onToggleDark: () => void
themeMode: ThemeMode
onCycleTheme: () => void
onShowAbout: () => void
isAutoSaving: boolean
autoSaveEnabled: boolean
onToggleAutoSave: () => void
}
const THEME_LABELS: Record<ThemeMode, string> = {
light: '亮色',
dark: '暗色',
warm: '暖色',
}
/**
* 应用顶层工具栏 — 文件操作、自动保存、主题、关于。
* 应用顶层工具栏 — 文件操作、自动保存、主题循环、关于。
* 编辑器格式化和模式切换由 MetonaEditor 内置工具栏处理。
*/
export const Toolbar = React.memo(function Toolbar({
onOpen, onSave, darkMode, onToggleDark, onShowAbout,
onOpen, onSave, themeMode, onCycleTheme, onShowAbout,
isAutoSaving, autoSaveEnabled, onToggleAutoSave
}: ToolbarProps) {
const nextLabel = THEME_LABELS[themeMode] ?? '主题'
return (
<div id="toolbar" role="toolbar" aria-label="工具栏">
<div className="toolbar-left" role="group" aria-label="文件操作">
@@ -42,8 +51,14 @@ export const Toolbar = React.memo(function Toolbar({
</button>
</div>
<div className="toolbar-right" role="group" aria-label="设置">
<button className="toolbar-btn" onClick={onToggleDark} title="切换暗色主题" aria-label={darkMode ? '切换到亮色主题' : '切换到暗色主题'}>
{darkMode ? <Sun size={18} /> : <Moon size={18} />}
<button
className="toolbar-btn"
onClick={onCycleTheme}
title={`当前 ${nextLabel} — 点击切换`}
aria-label={`当前${nextLabel}主题,点击切换`}
>
{themeMode === 'dark' ? <Moon size={18} /> : <Sun size={18} />}
<span style={{ fontSize: 12, marginLeft: 2 }}>{nextLabel}</span>
</button>
<button className="toolbar-btn" onClick={onShowAbout} title="关于" aria-label="关于 MarkLite">
<Info size={18} />