feat: deliver P2 v0.2.0 with unified editor, search, and snapshots

Implement schema v2 migration, outline/setting/inspiration CRUD, unified TipTap editing, reference panel with mentions, FTS global search, chapter version history, writing landmarks, word frequency analysis, light theme contrast fixes, and full unit/E2E test coverage.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-07-06 14:13:27 +08:00
parent 91c93954df
commit aac51bf183
72 changed files with 5790 additions and 203 deletions
@@ -0,0 +1,72 @@
import { useEffect, useState } from 'react'
import { useTranslation } from 'react-i18next'
import type { Bookmark } from '@shared/types'
import { useBookStore } from '@renderer/stores/useBookStore'
import { useEditStore } from '@renderer/stores/useEditStore'
import { ipcCall } from '@renderer/lib/ipc-client'
interface LandmarkModalProps {
open: boolean
onClose: () => void
}
export function LandmarkModal({ open, onClose }: LandmarkModalProps): React.JSX.Element | null {
const { t } = useTranslation()
const bookId = useBookStore((s) => s.currentBookId)
const chapters = useBookStore((s) => s.chapters)
const setSelectedChapter = useBookStore((s) => s.setSelectedChapter)
const switchTarget = useEditStore((s) => s.switchTarget)
const [bookmarks, setBookmarks] = useState<Bookmark[]>([])
useEffect(() => {
if (!open || !bookId) return
void ipcCall(() => window.electronAPI.bookmark.list(bookId)).then(setBookmarks)
}, [open, bookId])
const jumpTo = async (bm: Bookmark): Promise<void> => {
setSelectedChapter(bm.chapterId)
await switchTarget({ kind: 'chapter', id: bm.chapterId })
onClose()
}
if (!open) return null
return (
<div className="modal-overlay" id="landmarksModal" data-testid="landmarks-modal">
<div className="modal-content">
<div className="modal-header">
<h3>{t('landmark.title')}</h3>
<button type="button" className="modal-close" onClick={onClose}>
</button>
</div>
<div className="search-results">
{bookmarks.map((bm) => {
const ch = chapters.find((c) => c.id === bm.chapterId)
return (
<div
key={bm.id}
className="search-result"
onClick={() => void jumpTo(bm)}
role="button"
tabIndex={0}
data-testid={`landmark-item-${bm.id}`}
>
<div className="sr-type">
{bm.landmarkType} · {ch?.title ?? bm.chapterId}
</div>
<div>{bm.label}</div>
</div>
)
})}
{bookmarks.length === 0 && <div className="sidebar-empty">{t('landmark.empty')}</div>}
</div>
<div className="modal-footer">
<button type="button" className="btn" onClick={onClose}>
{t('dialog.cancel')}
</button>
</div>
</div>
</div>
)
}