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
+23
View File
@@ -0,0 +1,23 @@
export interface LandmarkInsert {
label: string
landmarkType?: string
}
let insertLandmarkFn: ((payload: LandmarkInsert) => void) | null = null
let highlightTokenFn: ((token: string) => void) | null = null
export function registerInsertLandmark(fn: (payload: LandmarkInsert) => void): void {
insertLandmarkFn = fn
}
export function registerHighlightToken(fn: (token: string) => void): void {
highlightTokenFn = fn
}
export function insertLandmarkInEditor(payload: LandmarkInsert): void {
insertLandmarkFn?.(payload)
}
export function highlightTokenInEditor(token: string): void {
highlightTokenFn?.(token)
}
+36
View File
@@ -0,0 +1,36 @@
import type { OutlineItem } from '@shared/types'
export function buildOutlineTree(items: OutlineItem[]): OutlineItem[] {
const byParent = new Map<string | null, OutlineItem[]>()
for (const item of items) {
const key = item.parentId
if (!byParent.has(key)) byParent.set(key, [])
byParent.get(key)!.push({ ...item, children: [] })
}
const attach = (parentId: string | null): OutlineItem[] => {
const nodes = byParent.get(parentId) ?? []
nodes.sort((a, b) => a.sortOrder - b.sortOrder)
for (const node of nodes) {
node.children = attach(node.id)
}
return nodes
}
return attach(null)
}
export function flattenOutlineTree(items: OutlineItem[]): OutlineItem[] {
const result: OutlineItem[] = []
const walk = (nodes: OutlineItem[]): void => {
for (const node of nodes) {
result.push(node)
if (node.children?.length) walk(node.children)
}
}
walk(items)
return result
}
export function calcDeviation(expected: number | null, actual: number): number | null {
if (!expected || expected <= 0) return null
return Math.round(((actual - expected) / expected) * 100)
}