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:
@@ -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,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) => {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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 }
|
||||
|
||||
@@ -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 }
|
||||
}
|
||||
Reference in New Issue
Block a user