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:
@@ -10,9 +10,9 @@ export const AboutDialog = React.memo(function AboutDialog({ onClose }: AboutDia
|
||||
const handleLinkClick = (e: React.MouseEvent<HTMLAnchorElement>): void => {
|
||||
e.preventDefault()
|
||||
if (window.electronAPI?.openExternal) {
|
||||
window.electronAPI.openExternal('https://gitee.com/thzxx/MarkLite')
|
||||
window.electronAPI.openExternal('https://git.metona.cn/MetonaTeam/MarkLite')
|
||||
} else {
|
||||
window.open('https://gitee.com/thzxx/MarkLite', '_blank')
|
||||
window.open('https://git.metona.cn/MetonaTeam/MarkLite', '_blank')
|
||||
}
|
||||
}
|
||||
|
||||
@@ -41,7 +41,7 @@ export const AboutDialog = React.memo(function AboutDialog({ onClose }: AboutDia
|
||||
<div className="about-footer">
|
||||
<a className="about-link" href="#" onClick={handleLinkClick}>
|
||||
<Gitee size={16} />
|
||||
<span>gitee.com/thzxx/MarkLite</span>
|
||||
<span>git.metona.cn/MetonaTeam/MarkLite</span>
|
||||
</a>
|
||||
<p>基于 Electron + React + TypeScript 构建</p>
|
||||
<p className="about-copyright">© 2026 thzxx</p>
|
||||
|
||||
@@ -53,7 +53,7 @@ export const Editor = React.memo(function Editor({ darkMode }: EditorProps) {
|
||||
setContent(activeTab.content)
|
||||
requestAnimationFrame(() => {
|
||||
setScrollTop(activeTab.scrollTop)
|
||||
setSelection()
|
||||
setSelection(activeTab.selectionStart, activeTab.selectionEnd)
|
||||
})
|
||||
})
|
||||
}
|
||||
@@ -79,7 +79,7 @@ export const Editor = React.memo(function Editor({ darkMode }: EditorProps) {
|
||||
}
|
||||
// stable refs: getScrollTop, getSelection (useCallback([])), updateTabScroll (zustand) - never change
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [activeTabId])
|
||||
}, [activeTabId])
|
||||
|
||||
// Register getView for OutlinePanel navigation
|
||||
useEffect(() => {
|
||||
|
||||
@@ -286,13 +286,30 @@ export function useMilkdown({ content, onChange, darkMode }: UseMilkdownOptions)
|
||||
}
|
||||
}, [])
|
||||
|
||||
const setSelection = useCallback(() => {
|
||||
// ProseMirror selection setting requires the view instance
|
||||
// which we access lazily; for now we focus the editor
|
||||
const container = containerRef.current
|
||||
if (!container) return
|
||||
const pmEditor = container.querySelector('.ProseMirror') as HTMLElement | null
|
||||
pmEditor?.focus()
|
||||
const setSelection = useCallback((from?: number, to?: number) => {
|
||||
const editor = editorRef.current
|
||||
if (!editor) return
|
||||
try {
|
||||
editor.action((ctx) => {
|
||||
const view = ctx.get(editorViewCtx)
|
||||
if (!view) return
|
||||
const docSize = view.state.doc.content.size
|
||||
// 恢复保存的选区,越界时夹紧到文档末尾
|
||||
const safeFrom = Math.min(from ?? 0, docSize)
|
||||
const safeTo = Math.min(to ?? safeFrom, docSize)
|
||||
const tr = view.state.tr.setSelection(
|
||||
TextSelection.create(view.state.doc, safeFrom, safeTo)
|
||||
)
|
||||
view.dispatch(tr)
|
||||
view.focus()
|
||||
})
|
||||
} catch {
|
||||
// 编辑器未就绪或选区无效,回退到仅 focus
|
||||
const container = containerRef.current
|
||||
if (!container) return
|
||||
const pmEditor = container.querySelector('.ProseMirror') as HTMLElement | null
|
||||
pmEditor?.focus()
|
||||
}
|
||||
}, [])
|
||||
|
||||
// Get ProseMirror EditorView instance
|
||||
|
||||
@@ -254,7 +254,6 @@ export const SearchReplace = memo(function SearchReplace({
|
||||
// 重新搜索以获取匹配的最新位置
|
||||
const freshMatches = findMatches(view.state.doc, queryRef.current, caseSensitiveRef.current, useRegexRef.current)
|
||||
if (freshMatches.length === 0) return
|
||||
if (freshMatches.length === 0) return
|
||||
|
||||
// 从后往前替换以保持位置正确
|
||||
const tr = view.state.tr
|
||||
|
||||
@@ -1,103 +0,0 @@
|
||||
import React, { useState, useEffect, useCallback, useRef } from 'react'
|
||||
|
||||
/** Toast 类型 */
|
||||
export type ToastType = 'success' | 'error' | 'warning' | 'info'
|
||||
|
||||
/** 单条 Toast 数据 */
|
||||
export interface ToastItem {
|
||||
id: string
|
||||
message: string
|
||||
type: ToastType
|
||||
duration: number
|
||||
}
|
||||
|
||||
/** Toast 容器组件属性 */
|
||||
interface ToastContainerProps {
|
||||
toasts: ToastItem[]
|
||||
onDismiss: (id: string) => void
|
||||
}
|
||||
|
||||
/** 单条 Toast 组件 */
|
||||
const SingleToast = React.memo(function SingleToast({
|
||||
toast,
|
||||
onDismiss
|
||||
}: {
|
||||
toast: ToastItem
|
||||
onDismiss: (id: string) => void
|
||||
}) {
|
||||
const [visible, setVisible] = useState(false)
|
||||
const timerRef = useRef<ReturnType<typeof setTimeout> | null>(null)
|
||||
|
||||
useEffect(() => {
|
||||
// 进入动画
|
||||
requestAnimationFrame(() => setVisible(true))
|
||||
|
||||
// 自动消失
|
||||
if (toast.duration > 0) {
|
||||
timerRef.current = setTimeout(() => {
|
||||
setVisible(false)
|
||||
// 等待退出动画完成后再移除
|
||||
setTimeout(() => onDismiss(toast.id), 300)
|
||||
}, toast.duration)
|
||||
}
|
||||
|
||||
return () => {
|
||||
if (timerRef.current) clearTimeout(timerRef.current)
|
||||
}
|
||||
}, [toast.id, toast.duration, onDismiss])
|
||||
|
||||
const handleClose = useCallback(() => {
|
||||
if (timerRef.current) clearTimeout(timerRef.current)
|
||||
setVisible(false)
|
||||
setTimeout(() => onDismiss(toast.id), 300)
|
||||
}, [toast.id, onDismiss])
|
||||
|
||||
const typeIcons: Record<ToastType, string> = {
|
||||
success: '✓',
|
||||
error: '✕',
|
||||
warning: '⚠',
|
||||
info: 'ℹ'
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
className={`toast-item toast-${toast.type} ${visible ? 'toast-visible' : ''}`}
|
||||
role="alert"
|
||||
aria-live="assertive"
|
||||
>
|
||||
<span className="toast-icon" aria-hidden="true">
|
||||
{typeIcons[toast.type]}
|
||||
</span>
|
||||
<span className="toast-message">{toast.message}</span>
|
||||
<button
|
||||
className="toast-close"
|
||||
onClick={handleClose}
|
||||
aria-label="关闭通知"
|
||||
type="button"
|
||||
>
|
||||
✕
|
||||
</button>
|
||||
</div>
|
||||
)
|
||||
})
|
||||
|
||||
/**
|
||||
* UX-05: Toast 容器组件
|
||||
* 支持多条堆叠、类型区分、关闭按钮和自动消失
|
||||
*/
|
||||
export const ToastContainer = React.memo(function ToastContainer({
|
||||
toasts,
|
||||
onDismiss
|
||||
}: ToastContainerProps) {
|
||||
if (toasts.length === 0) return null
|
||||
|
||||
return (
|
||||
<div className="toast-container" aria-label="通知区域">
|
||||
{toasts.map(toast => (
|
||||
<SingleToast key={toast.id} toast={toast} onDismiss={onDismiss} />
|
||||
))}
|
||||
</div>
|
||||
)
|
||||
})
|
||||
|
||||
ToastContainer.displayName = 'ToastContainer'
|
||||
@@ -1,2 +0,0 @@
|
||||
export { ToastContainer } from './Toast'
|
||||
export type { ToastType, ToastItem } from './Toast'
|
||||
Reference in New Issue
Block a user