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
+3 -3
View File
@@ -33,7 +33,7 @@ export function App() {
const viewMode = useEditorStore(s => s.viewMode)
const externallyModified = useEditorStore(s => s.externallyModified)
const setExternallyModified = useEditorStore(s => s.setExternallyModified)
const { darkMode, toggleDarkMode } = useTheme()
const { themeMode, cycleTheme } = useTheme()
const { confirm, confirmDialogProps } = useConfirm()
const [showAbout, setShowAbout] = useState(false)
const handleCloseAbout = useCallback(() => setShowAbout(false), [])
@@ -86,7 +86,7 @@ export function App() {
<div id="app" className={`mode-${viewMode}`}>
<Toolbar
onOpen={handleOpenFile} onSave={handleSave}
darkMode={darkMode} onToggleDark={toggleDarkMode}
themeMode={themeMode} onCycleTheme={cycleTheme}
onShowAbout={() => setShowAbout(true)}
isAutoSaving={isAutoSaving}
autoSaveEnabled={autoSaveEnabled}
@@ -101,7 +101,7 @@ export function App() {
)}
{tabs.length > 0 ? (
<div id="content-wrapper">
<div id="editor-panel"><Editor darkMode={darkMode} /></div>
<div id="editor-panel"><Editor themeMode={themeMode} onAppSave={handleSave} /></div>
</div>
) : (
<WelcomeScreen onOpen={handleOpenFile} onNew={() => createTab(null, '')} onOpenRecent={handleOpenRecent} />
+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} />
+1 -1
View File
@@ -18,7 +18,7 @@ export interface ActiveTabRecord {
export interface SettingsRecord {
id: string // 固定为 'default'
darkMode: boolean
themeMode: 'light' | 'dark' | 'warm'
viewMode: 'editor' | 'preview' | 'source'
sidebarCollapsed: boolean
sidebarWidth: number
+1 -1
View File
@@ -8,7 +8,7 @@ export const settingsRepository = {
const record: SettingsRecord | undefined = await db.settings.get('default')
if (record) {
return {
darkMode: record.darkMode ?? DEFAULT_SETTINGS.darkMode,
themeMode: record.themeMode ?? DEFAULT_SETTINGS.themeMode,
viewMode: record.viewMode ?? DEFAULT_SETTINGS.viewMode,
sidebarCollapsed: record.sidebarCollapsed ?? DEFAULT_SETTINGS.sidebarCollapsed,
sidebarWidth: record.sidebarWidth ?? DEFAULT_SETTINGS.sidebarWidth
+11 -1
View File
@@ -1,4 +1,4 @@
import { useCallback } from 'react'
import { useCallback, useRef } from 'react'
import { useTabStore } from '../stores/tabStore'
import { useEditorStore } from '../stores/editorStore'
import { recentFilesRepository } from '../db/recentFilesRepository'
@@ -8,12 +8,16 @@ import { showToast } from '../lib/toast'
/**
* AR-01: 从 App.tsx 提取的文件操作逻辑
* UX-02: 添加 loading 状态指示
* v0.1.9: handleSave 添加防重入锁,避免编辑器 onSave + 全局 Ctrl+S 双重触发
*/
export function useFileOperations() {
const createTab = useTabStore(s => s.createTab)
const getActiveTab = useTabStore(s => s.getActiveTab)
const setLoading = useEditorStore(s => s.setLoading)
// 防重入:onSave 回调 + 全局 Ctrl+S handler 可能在 300ms 内双重触发
const savingGate = useRef(false)
const handleOpenFile = useCallback(async (): Promise<void> => {
try {
if (!window.electronAPI) return
@@ -33,6 +37,9 @@ export function useFileOperations() {
}, [createTab, setLoading])
const handleSave = useCallback(async (): Promise<void> => {
// 防重入:300ms 内忽略重复调用(编辑器 onSave + 全局 Ctrl+S 双重触发)
if (savingGate.current) return
savingGate.current = true
try {
const tab = getActiveTab()
if (!tab || !window.electronAPI) return
@@ -47,6 +54,9 @@ export function useFileOperations() {
} catch (error) {
logError('保存文件失败', error)
showToast('保存失败', 'error')
} finally {
// 300ms 后释放锁,允许下次保存
setTimeout(() => { savingGate.current = false }, 300)
}
}, [getActiveTab])
+3 -1
View File
@@ -4,13 +4,15 @@ import { useTabStore } from '../stores/tabStore'
/**
* 全局键盘快捷键 hook。
* MetonaEditor 内置工具栏处理格式化和模式切换(Ctrl+B/I/1/2/3),
* 本 hook 仅处理应用级快捷键。
* v0.1.9 onSave 回调处理编辑器聚焦时的 Ctrl+S,
* 全局 handler 作为焦点外兜底(工具栏/侧边栏聚焦时仍可保存)。
*/
export function useKeyboard(handleOpenFile: () => void, handleSave: () => void, handleSaveAs: () => void) {
const handleKeydown = useCallback((e: KeyboardEvent) => {
const isCtrl = e.ctrlKey || e.metaKey
if (isCtrl && e.key === 'o') { e.preventDefault(); handleOpenFile(); return }
// 全局兜底:编辑器未聚焦时仍可保存(编辑器聚焦时由 onSave 回调处理)
if (isCtrl && e.key === 's' && !e.shiftKey) { e.preventDefault(); handleSave(); return }
if (isCtrl && e.shiftKey && e.key === 'S') { e.preventDefault(); handleSaveAs(); return }
+5 -6
View File
@@ -10,7 +10,7 @@ import { logError } from '../lib/errorHandler'
* 替代各 hook 各自独立加载设置的模式。
*/
export function useSettingsInit() {
const setDarkMode = useEditorStore(s => s.setDarkMode)
const setThemeMode = useEditorStore(s => s.setThemeMode)
const setViewMode = useEditorStore(s => s.setViewMode)
const isInitialized = useRef(false)
@@ -20,11 +20,10 @@ export function useSettingsInit() {
settingsRepository.load()
.then((settings) => {
// 主题
// 主题:优先使用保存的设置,否则跟随系统偏好
const prefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches
const darkMode = settings.darkMode !== undefined ? settings.darkMode : prefersDark
setDarkMode(darkMode)
document.documentElement.classList.toggle('dark', darkMode)
const themeMode = settings.themeMode ?? (prefersDark ? 'dark' : 'light')
setThemeMode(themeMode)
// 视图模式
setViewMode(settings.viewMode ?? 'editor')
@@ -42,7 +41,7 @@ export function useSettingsInit() {
.catch((error: unknown) => {
logError('加载设置失败', error)
})
}, [setDarkMode, setViewMode])
}, [setThemeMode, setViewMode])
return { isInitialized }
}
+80 -13
View File
@@ -1,23 +1,90 @@
import { useEffect } from 'react'
import { useEffect, useCallback } from 'react'
import MeEditor from '@metona-team/metona-editor'
import { useEditorStore } from '../stores/editorStore'
import { settingsRepository } from '../db/settingsRepository'
import { syncToastTheme } from '../lib/toast'
import type { ThemeMode } from '../types/settings'
/** 将 hex 颜色转为带 alpha 的版本,用于背景色 */
function hexWithAlpha(hex: string, alpha: number): string {
if (!hex || !hex.startsWith('#')) return hex
const a = Math.round(alpha * 255).toString(16).padStart(2, '0')
return hex.length === 7 ? hex + a : hex.slice(0, 7) + a
}
/** 将编辑器 CSS 变量同步到应用根元素,保持颜色一致 */
function syncAppColorsToEditor(): void {
try {
const vars = MeEditor.themes.exportCSSVars()
if (!vars || typeof vars !== 'object') return
const root = document.documentElement
const set = (name: string, value: string | undefined) => {
if (value) root.style.setProperty(name, value)
}
const accent = vars['--md-accent'] ?? '#1a73e8'
// 直接映射 — 文本/边框用纯色
set('--bg', vars['--md-bg'])
set('--text', vars['--md-text'])
set('--border', vars['--md-border'])
set('--primary', accent)
set('--primary-dark', accent)
set('--text-secondary', vars['--md-muted'])
set('--text-tertiary', vars['--md-muted'])
set('--code-bg', vars['--md-code-bg'])
// 背景派生 — 用 accent 的低透明度版本
const accentBg = hexWithAlpha(accent, 0.12)
set('--primary-light', accentBg)
set('--sidebar-active', accentBg)
set('--sidebar-bg', vars['--md-bg'])
set('--sidebar-border', vars['--md-border'])
set('--sidebar-hover', hexWithAlpha(vars['--md-border'] ?? '#e1e4e8', 0.4))
set('--bg-secondary', hexWithAlpha(vars['--md-text'] ?? '#333', 0.04))
set('--bg-tertiary', hexWithAlpha(vars['--md-text'] ?? '#333', 0.08))
set('--border-light', hexWithAlpha(vars['--md-border'] ?? '#e1e4e8', 0.5))
set('--search-bg', hexWithAlpha(vars['--md-text'] ?? '#333', 0.04))
set('--search-border', vars['--md-border'])
// 阴影根据主题适配
const isDark = vars['--md-bg'] && vars['--md-bg'] !== '#ffffff' && vars['--md-bg'] !== '#fff'
root.style.setProperty('--shadow', isDark
? '0 1px 3px rgba(0,0,0,0.3)'
: '0 1px 3px rgba(0,0,0,0.08)')
root.style.setProperty('--shadow-lg', isDark
? '0 4px 12px rgba(0,0,0,0.4)'
: '0 4px 12px rgba(0,0,0,0.1)')
} catch { /* 容错 */ }
}
const THEME_TO_TOAST: Record<ThemeMode, string> = {
light: 'light',
dark: 'dark',
warm: 'warm',
}
/**
* AR-04: 主题 hook
* 不再独立加载设置(由 useSettingsInit 统一加载)
* 仅负责主题切换时的同步和保存
* 主题 hook — 三主题循环(亮色 → 暗色 → 暖色),
* 应用颜色自动同步编辑器主题,保持一致。
*/
export function useTheme() {
const darkMode = useEditorStore(s => s.darkMode)
const toggleDarkMode = useEditorStore(s => s.toggleDarkMode)
const themeMode = useEditorStore(s => s.themeMode)
const cycleTheme = useEditorStore(s => s.cycleTheme)
// darkMode 变化时同步 class 并保存
const handleCycleTheme = useCallback(() => {
const next = cycleTheme()
// 同步编辑器全局主题
MeEditor.setTheme(next)
// 同步 Toast 主题
import('../lib/toast').then(({ MeToast }) => {
MeToast.themes.switchTheme(THEME_TO_TOAST[next] ?? 'auto')
})
// 持久化
settingsRepository.save({ themeMode: next })
}, [cycleTheme])
// themeMode 变化时同步应用颜色
useEffect(() => {
document.documentElement.classList.toggle('dark', darkMode)
settingsRepository.save({ darkMode })
syncToastTheme(darkMode)
}, [darkMode])
// 暗色/暖色统一加 .dark class(兼容 global.css 中的 :root.dark 硬编码规则)
document.documentElement.classList.toggle('dark', themeMode === 'dark' || themeMode === 'warm')
syncAppColorsToEditor()
}, [themeMode])
return { darkMode, toggleDarkMode }
return { themeMode, cycleTheme: handleCycleTheme }
}
@@ -5,7 +5,7 @@ describe('editorStore', () => {
beforeEach(() => {
useEditorStore.setState({
viewMode: 'editor',
darkMode: false,
themeMode: 'light',
externallyModified: null,
loadingStates: {},
})
@@ -24,29 +24,36 @@ describe('editorStore', () => {
})
})
describe('setDarkMode', () => {
it('should enable dark mode', () => {
useEditorStore.getState().setDarkMode(true)
expect(useEditorStore.getState().darkMode).toBe(true)
describe('themeMode', () => {
it('should default to light', () => {
expect(useEditorStore.getState().themeMode).toBe('light')
})
it('should disable dark mode', () => {
useEditorStore.setState({ darkMode: true })
useEditorStore.getState().setDarkMode(false)
expect(useEditorStore.getState().darkMode).toBe(false)
it('should set theme mode', () => {
useEditorStore.getState().setThemeMode('dark')
expect(useEditorStore.getState().themeMode).toBe('dark')
})
it('should set warm theme', () => {
useEditorStore.getState().setThemeMode('warm')
expect(useEditorStore.getState().themeMode).toBe('warm')
})
})
describe('toggleDarkMode', () => {
it('should toggle from false to true', () => {
useEditorStore.getState().toggleDarkMode()
expect(useEditorStore.getState().darkMode).toBe(true)
describe('cycleTheme', () => {
it('should cycle light → dark', () => {
expect(useEditorStore.getState().cycleTheme()).toBe('dark')
expect(useEditorStore.getState().themeMode).toBe('dark')
})
it('should toggle from true to false', () => {
useEditorStore.setState({ darkMode: true })
useEditorStore.getState().toggleDarkMode()
expect(useEditorStore.getState().darkMode).toBe(false)
it('should cycle dark → warm', () => {
useEditorStore.setState({ themeMode: 'dark' })
expect(useEditorStore.getState().cycleTheme()).toBe('warm')
})
it('should cycle warm → light', () => {
useEditorStore.setState({ themeMode: 'warm' })
expect(useEditorStore.getState().cycleTheme()).toBe('light')
})
})
+15 -7
View File
@@ -1,5 +1,5 @@
import { create } from 'zustand'
import type { ViewMode } from '../types/settings'
import type { ViewMode, ThemeMode } from '../types/settings'
import type { MarkdownEditor } from '@metona-team/metona-editor'
// Module-level getter for the MetonaEditor instance
@@ -14,17 +14,19 @@ export function getMetonaEditor(): MarkdownEditor | null {
return _getEditor ? _getEditor() : null
}
const THEME_CYCLE: ThemeMode[] = ['light', 'dark', 'warm']
interface EditorState {
viewMode: ViewMode
darkMode: boolean
themeMode: ThemeMode
// AR-03: 外部修改检测状态,替代 DOM CustomEvent
externallyModified: { filePath: string } | null
// UX-02: 全局加载状态
loadingStates: Record<string, boolean>
setViewMode: (mode: ViewMode) => void
setDarkMode: (dark: boolean) => void
toggleDarkMode: () => void
setThemeMode: (mode: ThemeMode) => void
cycleTheme: () => ThemeMode
setExternallyModified: (info: { filePath: string } | null) => void
// UX-02: 加载状态管理
setLoading: (key: string, loading: boolean) => void
@@ -33,13 +35,19 @@ interface EditorState {
export const useEditorStore = create<EditorState>((set, get) => ({
viewMode: 'editor',
darkMode: false,
themeMode: 'light',
externallyModified: null,
loadingStates: {},
setViewMode: (mode: ViewMode) => set({ viewMode: mode }),
setDarkMode: (dark: boolean) => set({ darkMode: dark }),
toggleDarkMode: () => set(state => ({ darkMode: !state.darkMode })),
setThemeMode: (mode: ThemeMode) => set({ themeMode: mode }),
cycleTheme: () => {
const current = get().themeMode
const idx = THEME_CYCLE.indexOf(current)
const next = THEME_CYCLE[(idx + 1) % THEME_CYCLE.length]
set({ themeMode: next })
return next
},
setExternallyModified: (info: { filePath: string } | null) => set({ externallyModified: info }),
setLoading: (key: string, loading: boolean) => set(state => ({
loadingStates: { ...state.loadingStates, [key]: loading }
+2 -19
View File
@@ -23,23 +23,6 @@
--sidebar-active: var(--primary-light);
--search-bg: var(--bg-secondary);
--search-border: var(--border);
--font-ui: system-ui, -apple-system, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
--font-mono: "Cascadia Code", "Fira Code", "JetBrains Mono", Consolas, "Courier New", monospace;
}
:root.dark {
--primary: #8ab4f8;
--primary-light: #1a3a5c;
--primary-dark: #aecbfa;
--bg: #1e1e1e;
--bg-secondary: #252526;
--bg-tertiary: #2d2d2d;
--text: #d4d4d4;
--text-secondary: #9e9e9e;
--text-tertiary: #6e6e6e;
--border: #3e3e3e;
--border-light: #333333;
--code-bg: #2d2d2d;
--shadow: 0 1px 3px rgba(0, 0, 0, 0.3);
--shadow-lg: 0 4px 12px rgba(0, 0, 0, 0.4);
--font-ui: system-ui, -apple-system, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
--font-mono: 'Cascadia Code', 'Fira Code', 'JetBrains Mono', Consolas, 'Courier New', monospace;
}
+3 -2
View File
@@ -1,14 +1,15 @@
export type ThemeMode = 'light' | 'dark' | 'warm'
export type ViewMode = 'editor' | 'preview' | 'source'
export interface Settings {
darkMode: boolean
themeMode: ThemeMode
viewMode: ViewMode
sidebarCollapsed: boolean
sidebarWidth: number
}
export const DEFAULT_SETTINGS: Settings = {
darkMode: false,
themeMode: 'light',
viewMode: 'editor',
sidebarCollapsed: false,
sidebarWidth: 240
+1 -1
View File
@@ -1,5 +1,5 @@
// 共享常量 — 主进程和渲染进程共用
export const APP_VERSION = 'v0.4.0'
export const APP_VERSION = 'v0.4.1'
export const MAX_FILE_SIZE = 20 * 1024 * 1024 // 20MB
export const ALLOWED_EXTENSIONS = ['.md', '.markdown', '.txt'] as const
export const SKIP_DIRS = new Set([