release: v0.3.13 — MeToast 集成与全面质量修复

- 集成 MetonaToast v2.0.0 替换自研 Toast 组件(右上角、进度条、自动关闭、主题联动)
- 修复 sidebar resize 未持久化到 IndexedDB
- 修复 Ctrl+Tab MRU 切换逻辑与文档不一致
- 完善 WYSIWYG 模式标签切换选区恢复
- 清理死代码(useActiveHeading observerRef、旧 Toast 样式、重复滚动条样式、SearchReplace 重复检查)
- 同步 DESIGN.md / docs 文档(CodeMirror→Milkdown、stores 计数、editorStore 字段)
- 新增 .npmrc.example 模板
- 更新关于对话框与文档远程地址为 Gitea
- 版本号 0.3.12 → 0.3.13
This commit is contained in:
2026-07-22 15:55:45 +08:00
parent 77fdf03ed7
commit 43b0778849
24 changed files with 170 additions and 387 deletions
+1 -4
View File
@@ -1,4 +1,4 @@
import { useEffect, useRef, useState, useCallback } from 'react'
import { useEffect, useState, useCallback } from 'react'
/**
* D4: 基于视口的活跃标题追踪 hook
@@ -16,7 +16,6 @@ export function useActiveHeading(
headings: { level: number; text: string }[]
): number | null {
const [activeIndex, setActiveIndex] = useState<number | null>(null)
const observerRef = useRef<IntersectionObserver | null>(null)
const handleScroll = useCallback(() => {
const container = containerRef.current
@@ -71,10 +70,8 @@ export function useActiveHeading(
// 初始计算
handleScroll()
const observer = observerRef.current
return () => {
container.removeEventListener('scroll', handleScroll)
observer?.disconnect()
}
}, [containerRef, headings, handleScroll])
+3 -2
View File
@@ -3,8 +3,9 @@ import { useTabStore } from '../stores/tabStore'
import { isAllowedFile } from '../lib/fileUtils'
import { MAX_FILE_SIZE } from '../lib/constants'
import { logError } from '../lib/errorHandler'
import { showToast } from '../lib/toast'
export function useDragDrop(showToast: (msg: string) => void) {
export function useDragDrop() {
const createTab = useTabStore(s => s.createTab)
const handleDrop = useCallback(async (e: DragEvent) => {
@@ -50,7 +51,7 @@ export function useDragDrop(showToast: (msg: string) => void) {
if (rejected > 0) {
showToast(`仅支持 .md / .markdown / .txt 文件,已忽略 ${rejected} 个文件`)
}
}, [createTab, showToast])
}, [createTab])
useEffect(() => {
const prevent = (e: DragEvent) => {
+5 -5
View File
@@ -3,13 +3,13 @@ import { useTabStore } from '../stores/tabStore'
import { useEditorStore } from '../stores/editorStore'
import { recentFilesRepository } from '../db/recentFilesRepository'
import { logError } from '../lib/errorHandler'
import type { ToastType } from '../components/Toast/Toast'
import { showToast } from '../lib/toast'
/**
* AR-01: 从 App.tsx 提取的文件操作逻辑
* UX-02: 添加 loading 状态指示
*/
export function useFileOperations(showToast: (msg: string, type?: ToastType) => void) {
export function useFileOperations() {
const createTab = useTabStore(s => s.createTab)
const getActiveTab = useTabStore(s => s.getActiveTab)
const setLoading = useEditorStore(s => s.setLoading)
@@ -30,7 +30,7 @@ export function useFileOperations(showToast: (msg: string, type?: ToastType) =>
} finally {
setLoading('file-open', false)
}
}, [createTab, showToast, setLoading])
}, [createTab, setLoading])
const handleSave = useCallback(async (): Promise<void> => {
try {
@@ -48,7 +48,7 @@ export function useFileOperations(showToast: (msg: string, type?: ToastType) =>
logError('保存文件失败', error)
showToast('保存失败', 'error')
}
}, [getActiveTab, showToast])
}, [getActiveTab])
const handleSaveAs = useCallback(async (): Promise<void> => {
try {
@@ -63,7 +63,7 @@ export function useFileOperations(showToast: (msg: string, type?: ToastType) =>
logError('另存为失败', error)
showToast('另存为失败', 'error')
}
}, [getActiveTab, showToast])
}, [getActiveTab])
const handleOpenRecent = useCallback(async (filePath: string): Promise<void> => {
if (!window.electronAPI) return
+13 -11
View File
@@ -23,23 +23,25 @@ export function useKeyboard(handleOpenFile: () => void, handleSave: () => void,
return
}
// Ctrl+Tab / Ctrl+Shift+Tab
// Ctrl+Tab / Ctrl+Shift+Tab — MRU 顺序切换
if (isCtrl && e.key === 'Tab') {
e.preventDefault()
const { tabs, activeTabId, mruStack } = tabState
if (tabs.length > 1) {
if (e.shiftKey) {
if (mruStack.length > 0) {
const targetId = mruStack[0]
if (tabs.find(t => t.id === targetId)) {
tabState.switchToTab(targetId)
}
// mruStack[0] 是最近一个被切走的标签(即上一个活动标签)
if (mruStack.length > 0) {
const targetId = mruStack[0]
if (tabs.find(t => t.id === targetId)) {
tabState.switchToTab(targetId)
return
}
} else {
const idx = tabs.findIndex(t => t.id === activeTabId)
const next = (idx + 1) % tabs.length
tabState.switchToTab(tabs[next].id)
}
// MRU 栈为空或不合法时回退到顺序切换
const idx = tabs.findIndex(t => t.id === activeTabId)
const next = e.shiftKey
? (idx - 1 + tabs.length) % tabs.length
: (idx + 1) % tabs.length
tabState.switchToTab(tabs[next].id)
}
return
}
+13 -1
View File
@@ -1,7 +1,11 @@
import { useState, useRef, useEffect } from 'react'
import { useSidebarStore } from '../stores/sidebarStore'
/**
* AR-02: 从 Sidebar.tsx 提取的 resize 逻辑
*
* 拖拽过程中直接操作 DOM 宽度(避免每次 mousemove 触发 store 更新+重渲染),
* 拖拽结束时将最终宽度同步到 sidebarStore(持久化到 IndexedDB)。
*/
export function useSidebarResize() {
const [isResizing, setIsResizing] = useState(false)
@@ -17,7 +21,15 @@ export function useSidebarResize() {
}
}
const handleMouseUp = (): void => setIsResizing(false)
const handleMouseUp = (): void => {
// 拖拽结束时将最终宽度持久化到 store(→ IndexedDB
if (sidebarRef.current) {
const width = sidebarRef.current.offsetWidth
const clamped = Math.max(180, Math.min(500, width))
useSidebarStore.getState().setSidebarWidth(clamped)
}
setIsResizing(false)
}
document.addEventListener('mousemove', handleMouseMove)
document.addEventListener('mouseup', handleMouseUp)
+2
View File
@@ -1,6 +1,7 @@
import { useEffect } from 'react'
import { useEditorStore } from '../stores/editorStore'
import { settingsRepository } from '../db/settingsRepository'
import { syncToastTheme } from '../lib/toast'
/**
* AR-04: 主题 hook
@@ -15,6 +16,7 @@ export function useTheme() {
useEffect(() => {
document.documentElement.classList.toggle('dark', darkMode)
settingsRepository.save({ darkMode })
syncToastTheme(darkMode)
}, [darkMode])
return { darkMode, toggleDarkMode }
-45
View File
@@ -1,45 +0,0 @@
import { useState, useCallback } from 'react'
import type { ToastItem, ToastType } from '../components/Toast/Toast'
/** 默认自动消失时间 (ms) */
const DEFAULT_DURATION = 3000
/** 最大同时显示数量 */
const MAX_TOASTS = 5
// Module-level counter survives component remounts
let toastCounter = 0
/**
* UX-05: 升级版 Toast 状态管理 hook
* 支持多条堆叠、类型区分、自动消失
*/
export function useToast() {
const [toasts, setToasts] = useState<ToastItem[]>([])
const showToast = useCallback((msg: string, type: ToastType = 'info', duration = DEFAULT_DURATION) => {
const id = `toast-${++toastCounter}`
const newToast: ToastItem = { id, message: msg, type, duration }
setToasts(prev => {
const next = [...prev, newToast]
// 超过最大数量时移除最旧的
if (next.length > MAX_TOASTS) {
return next.slice(next.length - MAX_TOASTS)
}
return next
})
return id
}, [])
const dismissToast = useCallback((id: string) => {
setToasts(prev => prev.filter(t => t.id !== id))
}, [])
const cleanup = useCallback(() => {
setToasts([])
}, [])
return { toasts, showToast, dismissToast, cleanup }
}