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