import type React from 'react' import { useCallback, useRef, useState, useEffect } from 'react' import type { VaultEntry, NoteStatus } from '../types' import type { useCreateBlockNote } from '@blocknote/react' import { DiffView } from './DiffView' import { BreadcrumbBar } from './BreadcrumbBar' import { TitleField } from './TitleField' import { NoteIcon } from './NoteIcon' import { TrashedNoteBanner } from './TrashedNoteBanner' import { ArchivedNoteBanner } from './ArchivedNoteBanner' import { ConflictNoteBanner } from './ConflictNoteBanner' import { RawEditorView } from './RawEditorView' import { countWords } from '../utils/wikilinks' import { SingleEditorView } from './SingleEditorView' import { isEmoji } from '../utils/emoji' import { useEditorTheme } from '../hooks/useTheme' interface Tab { entry: VaultEntry content: string } interface EditorContentProps { activeTab: Tab | null isLoadingNewTab: boolean entries: VaultEntry[] editor: ReturnType diffMode: boolean diffContent: string | null diffLoading: boolean onToggleDiff: () => void rawMode: boolean onToggleRaw: () => void onRawContentChange?: (path: string, content: string) => void onSave?: () => void activeStatus: NoteStatus showDiffToggle: boolean showAIChat?: boolean onToggleAIChat?: () => void inspectorCollapsed: boolean onToggleInspector: () => void onNavigateWikilink: (target: string) => void onEditorChange?: () => void onTrashNote?: (path: string) => void onRestoreNote?: (path: string) => void onDeleteNote?: (path: string) => void onArchiveNote?: (path: string) => void onUnarchiveNote?: (path: string) => void vaultPath?: string /** Ref updated by RawEditorView on every keystroke with the latest doc. */ rawLatestContentRef?: React.MutableRefObject /** Called when the user edits the dedicated title field. */ onTitleChange?: (path: string, newTitle: string) => void /** Called when user sets or changes an emoji icon via the picker. */ onSetNoteIcon?: (path: string, emoji: string) => void /** Called when user removes an emoji icon. */ onRemoveNoteIcon?: (path: string) => void /** Whether the active note has a merge conflict. */ isConflicted?: boolean /** Resolve conflict by keeping the local version. */ onKeepMine?: (path: string) => void /** Resolve conflict by keeping the remote version. */ onKeepTheirs?: (path: string) => void } function EditorLoadingSkeleton() { return (
) } function DiffModeView({ diffContent, onToggleDiff }: { diffContent: string | null; onToggleDiff: () => void }) { return (
) } function RawModeEditorSection({ rawMode, activeTab, entries, onContentChange, onSave, latestContentRef, }: { rawMode: boolean activeTab: Tab | null entries: VaultEntry[] onContentChange?: (path: string, content: string) => void onSave?: () => void latestContentRef?: React.MutableRefObject }) { if (!rawMode || !activeTab) return null return ( {})} onSave={onSave ?? (() => {})} latestContentRef={latestContentRef} /> ) } /** Bind an optional callback to a path, returning undefined if callback is absent */ function bindPath(cb: ((path: string) => void) | undefined, path: string) { return cb ? () => cb(path) : undefined } function ActiveTabBreadcrumb({ activeTab, titleHidden, props }: { activeTab: Tab titleHidden: boolean props: Omit }) { const wordCount = countWords(activeTab.content) const path = activeTab.entry.path return ( ) } export function EditorContent({ activeTab, isLoadingNewTab, entries, editor, diffMode, diffContent, onToggleDiff, rawMode, onToggleRaw, onRawContentChange, onSave, onNavigateWikilink, onEditorChange, vaultPath, onDeleteNote, rawLatestContentRef, onTitleChange, onSetNoteIcon, onRemoveNoteIcon, isConflicted, onKeepMine, onKeepTheirs, ...breadcrumbProps }: EditorContentProps) { // Look up trashed/archived from the latest vault entries, not the tab snapshot, // so the banner appears regardless of navigation context. const { cssVars } = useEditorTheme() const freshEntry = activeTab ? entries.find(e => e.path === activeTab.entry.path) : undefined const isTrashed = freshEntry?.trashed ?? activeTab?.entry.trashed ?? false const isArchived = freshEntry?.archived ?? activeTab?.entry.archived ?? false const showEditor = !diffMode && !rawMode const entryIcon = activeTab?.entry.icon ?? null const emojiIcon = entryIcon && isEmoji(entryIcon) ? entryIcon : null const titleSectionRef = useRef(null) const [titleScrolledAway, setTitleScrolledAway] = useState(false) const titleHidden = showEditor && titleScrolledAway useEffect(() => { const el = titleSectionRef.current if (!el) return const observer = new IntersectionObserver( ([e]) => setTitleScrolledAway(!e.isIntersecting), { threshold: 0 }, ) observer.observe(el) return () => observer.disconnect() }, [activeTab?.entry.path, showEditor]) const handleSetIcon = useCallback((emoji: string) => { if (activeTab) onSetNoteIcon?.(activeTab.entry.path, emoji) }, [activeTab, onSetNoteIcon]) const handleRemoveIcon = useCallback(() => { if (activeTab) onRemoveNoteIcon?.(activeTab.entry.path) }, [activeTab, onRemoveNoteIcon]) return (
{activeTab && ( )} {activeTab && isTrashed && ( breadcrumbProps.onRestoreNote?.(activeTab.entry.path)} onDeletePermanently={() => onDeleteNote?.(activeTab.entry.path)} /> )} {activeTab && isArchived && breadcrumbProps.onUnarchiveNote && ( breadcrumbProps.onUnarchiveNote!(activeTab.entry.path)} /> )} {activeTab && isConflicted && ( onKeepMine?.(activeTab.entry.path)} onKeepTheirs={() => onKeepTheirs?.(activeTab.entry.path)} /> )} {diffMode && } {showEditor && activeTab && (
{!emojiIcon && (
)}
{emojiIcon && ( )} onTitleChange?.(activeTab.entry.path, newTitle)} />
)} {isLoadingNewTab && showEditor && }
) }