Multiple feature additions and improvements across the application: - NextGen Editor: drag handles, smart paste, block actions - Structured views: Kanban and table layouts for notes - Architectural Grid: new brainstorming/agent interface prototype - Flashcards: SM-2 revision algorithm with AI generation - MCP server: robustness improvements - Graph/PDF chat: fix click propagation and copy behavior - Various UI/UX enhancements and bug fixes Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
91 lines
3.1 KiB
TypeScript
91 lines
3.1 KiB
TypeScript
'use client'
|
|
|
|
import { useState, useEffect, useCallback, type ReactNode } from 'react'
|
|
import { AnimatePresence } from 'framer-motion'
|
|
import { useRouter, useSearchParams } from 'next/navigation'
|
|
import { toast } from 'sonner'
|
|
import type { Note } from '@/lib/types'
|
|
import { getNoteById } from '@/app/actions/notes'
|
|
import { NOTE_REQUEST_SAVE_EVENT } from '@/lib/note-change-sync'
|
|
import {
|
|
NOTE_PEEK_OPEN_EVENT,
|
|
NOTE_PEEK_CLOSE_EVENT,
|
|
type NotePeekOpenDetail,
|
|
} from '@/lib/note-peek-sync'
|
|
import { NoteEditorSplitPeek } from './note-editor-split-peek'
|
|
import { useLanguage } from '@/lib/i18n'
|
|
|
|
interface NoteEditorPeekHostProps {
|
|
noteId: string
|
|
fullPage?: boolean
|
|
children: ReactNode
|
|
}
|
|
|
|
export function NoteEditorPeekHost({ noteId, fullPage, children }: NoteEditorPeekHostProps) {
|
|
const router = useRouter()
|
|
const searchParams = useSearchParams()
|
|
const { t, language } = useLanguage()
|
|
const isRtl = language === 'fa' || language === 'ar'
|
|
const [peekState, setPeekState] = useState<{ note: Note; blockId?: string } | null>(null)
|
|
|
|
useEffect(() => {
|
|
const onOpenPeek = (event: Event) => {
|
|
const detail = (event as CustomEvent<NotePeekOpenDetail>).detail
|
|
if (!detail?.noteId) return
|
|
if (detail.noteId === noteId) return
|
|
|
|
void getNoteById(detail.noteId).then((fetched) => {
|
|
if (fetched) {
|
|
setPeekState({ note: fetched, blockId: detail.blockId })
|
|
} else {
|
|
toast.error(t('notePeek.loadFailed'))
|
|
}
|
|
})
|
|
}
|
|
const onClosePeek = () => setPeekState(null)
|
|
|
|
window.addEventListener(NOTE_PEEK_OPEN_EVENT, onOpenPeek)
|
|
window.addEventListener(NOTE_PEEK_CLOSE_EVENT, onClosePeek)
|
|
return () => {
|
|
window.removeEventListener(NOTE_PEEK_OPEN_EVENT, onOpenPeek)
|
|
window.removeEventListener(NOTE_PEEK_CLOSE_EVENT, onClosePeek)
|
|
}
|
|
}, [noteId, t])
|
|
|
|
const handleClosePeek = useCallback(() => {
|
|
setPeekState(null)
|
|
}, [])
|
|
|
|
const handleOpenPeekFully = useCallback(() => {
|
|
if (!peekState) return
|
|
window.dispatchEvent(new CustomEvent(NOTE_REQUEST_SAVE_EVENT, {
|
|
detail: { noteId, reason: 'before-peek-full-open' },
|
|
}))
|
|
const params = new URLSearchParams(searchParams.toString())
|
|
params.set('openNote', peekState.note.id)
|
|
router.replace(params.toString() ? `/home?${params.toString()}` : '/home', { scroll: false })
|
|
setPeekState(null)
|
|
}, [noteId, peekState, router, searchParams])
|
|
|
|
const shellClass = fullPage
|
|
? 'flex flex-1 min-h-0 h-full w-full items-stretch overflow-hidden'
|
|
: 'relative flex min-h-0 flex-1 flex-col overflow-hidden'
|
|
|
|
return (
|
|
<div className={`${shellClass}${peekState ? (isRtl ? ' flex-row-reverse' : ' flex-row') : ''}`}>
|
|
<div className="flex-1 min-w-0 flex flex-col overflow-hidden">{children}</div>
|
|
<AnimatePresence initial={false}>
|
|
{peekState && (
|
|
<NoteEditorSplitPeek
|
|
key={peekState.note.id}
|
|
note={peekState.note}
|
|
blockId={peekState.blockId}
|
|
onClose={handleClosePeek}
|
|
onOpenFully={handleOpenPeekFully}
|
|
/>
|
|
)}
|
|
</AnimatePresence>
|
|
</div>
|
|
)
|
|
}
|