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:
@@ -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 (
|
||||
|
||||
@@ -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} />
|
||||
|
||||
Reference in New Issue
Block a user