import { useRef, useEffect, useCallback, memo, useState, type ReactNode } from 'react' import { useEditorTabSwap } from '../hooks/useEditorTabSwap' import { useCreateBlockNote } from '@blocknote/react' import '@blocknote/mantine/style.css' import 'katex/dist/katex.min.css' import { emptyImageUploadResult, isUnsupportedImageFormatError, uploadImageFile, type ImageImportError, type UploadImageFileResult, } from '../hooks/useImageDrop' import { DEFAULT_AI_AGENT, type AiAgentId, type AiAgentReadiness } from '../lib/aiAgents' import type { AiTarget } from '../lib/aiTargets' import { translate, type AppLocale } from '../lib/i18n' import { RUNTIME_STYLE_NONCE } from '../lib/runtimeStyleNonce' import type { VaultEntry, GitCommit, NoteWidthMode, NoteStatus, WorkspaceIdentity } from '../types' import type { NoteListItem } from '../utils/ai-context' import type { FrontmatterValue } from './Inspector' import type { FrontmatterOpOptions } from '../hooks/frontmatterOps' import { ResizeHandle } from './ResizeHandle' import { useDiffMode, type CommitDiffRequest } from '../hooks/useDiffMode' import { useEditorFocus } from '../hooks/useEditorFocus' import { useDragRegion } from '../hooks/useDragRegion' import { formatShortcutDisplay } from '../hooks/appCommandCatalog' import { EditorRightPanel } from './EditorRightPanel' import { EditorContent } from './EditorContent' import { EditorMemoryProbe } from './EditorMemoryProbe' import { FilePreview } from './FilePreview' import { schema } from './editorSchema' import { useRightPanelExclusion } from './useRightPanelExclusion' import type { RawEditorFindRequest } from './RawEditorFindBar' import { resolvePendingRawExitContent, resolveRawModeContent, } from './editorRawModeSync' import { useRegisterEditorContentFlushes } from './editorContentFlushRegistration' import { useRawModeWithFlush } from './useRawModeWithFlush' import { createImeCompositionKeyGuardExtension } from './imeCompositionKeyGuardExtension' import { createMarkdownHighlightShortcutExtension } from './markdownHighlightShortcutExtension' import { handleRichEditorPaste } from './richEditorPaste' import { createRichEditorMarkdownInputTransformExtension } from './richEditorInputTransformExtension' import { createRichEditorTextDirectionExtension } from './richEditorTextDirection' import { createRichEditorTransformErrorRecoveryExtension } from './richEditorTransformErrorRecoveryExtension' import { createRichEditorBlockSelectionExtension } from './richEditorBlockSelectionExtension' import { useFilenameAutolinkGuard } from './useFilenameAutolinkGuard' import { useEditorPdfExport } from './useEditorPdfExport' import type { NotePdfExportSource } from '../utils/notePdfExport' import type { RichEditorBlockTypeDefinition } from '../utils/richEditorBlockTypes' import { useRichEditorContentReadiness, useRichEditorSheetSwapState, } from './useRichEditorSheetTransition' import { installBlockNoteDirectMarkdown, type DirectMarkdownCapableSerializer, } from '../utils/blockNoteDirectMarkdown' import { installRichEditorDispatchPerformanceProbe } from './richEditorDispatchPerformance' import { RICH_EDITOR_BLOCKNOTE_PERFORMANCE_OPTIONS } from './richEditorBlockNoteOptions' import { useTurnCurrentBlockIntoCommand } from './useTurnCurrentBlockIntoCommand' import './Editor.css' import './EditorTheme.css' const RICH_EDITOR_BIDI_DOM_ATTRIBUTES = { blockContent: { dir: 'auto' }, inlineContent: { dir: 'auto' }, } interface Tab { entry: VaultEntry content: string } interface EditorProps { tabs: Tab[] activeTabPath: string | null isVaultLoading?: boolean entries: VaultEntry[] onNavigateWikilink: (target: string) => void onUnsupportedAiPaste?: (message: string) => void onLoadDiff?: (path: string) => Promise onLoadDiffAtCommit?: (path: string, commitHash: string) => Promise pendingCommitDiffRequest?: CommitDiffRequest | null onPendingCommitDiffHandled?: (requestId: number) => void getNoteStatus?: (path: string) => NoteStatus onCreateNote?: () => void inspectorCollapsed: boolean onToggleInspector: () => void inspectorWidth: number defaultAiAgent?: AiAgentId defaultAiTarget?: AiTarget defaultAiAgentReadiness?: AiAgentReadiness defaultAiAgentReady?: boolean onInspectorResize: (delta: number) => void inspectorEntry: VaultEntry | null inspectorContent: string | null gitHistory: GitCommit[] onUpdateFrontmatter?: (path: string, key: string, value: FrontmatterValue, options?: FrontmatterOpOptions) => Promise onDeleteProperty?: (path: string, key: string, options?: FrontmatterOpOptions) => Promise onAddProperty?: (path: string, key: string, value: FrontmatterValue, options?: FrontmatterOpOptions) => Promise onCreateMissingType?: (path: string, missingType: string, nextTypeName: string) => Promise onCreateAndOpenNote?: (title: string) => Promise onChangeWorkspace?: (entry: VaultEntry, workspace: WorkspaceIdentity) => Promise | void onInitializeProperties?: (path: string) => void showAIChat?: boolean onToggleAIChat?: () => void aiWorkspaceSurface?: ReactNode vaultPath?: string vaultPaths?: string[] noteList?: NoteListItem[] noteListFilter?: { type: string | null; query: string } onToggleFavorite?: (path: string) => void onToggleOrganized?: (path: string) => void onEnterNeighborhood?: (entry: VaultEntry) => void onRevealFile?: (path: string) => void onCopyFilePath?: (path: string) => void onCopyDeepLink?: (entry: VaultEntry) => void onCopyGitUrl?: (entry: VaultEntry) => void onOpenExternalFile?: (path: string) => void onDeleteNote?: (path: string) => void onArchiveNote?: (path: string) => void onUnarchiveNote?: (path: string) => void onContentChange?: (path: string, content: string) => void onSave?: () => void /** Called when the user explicitly renames the filename from the breadcrumb. */ onRenameFilename?: (path: string, newFilenameStem: string) => void noteWidth?: NoteWidthMode onToggleNoteWidth?: () => void canGoBack?: boolean canGoForward?: boolean onGoBack?: () => void onGoForward?: () => void leftPanelsCollapsed?: boolean /** Mutable ref that Editor registers its raw-mode toggle into, for command palette access. */ rawToggleRef?: React.MutableRefObject<() => void> /** Mutable ref that Editor registers editor find commands into, for shortcuts and menus. */ findInNoteRef?: React.MutableRefObject<((options?: { replace?: boolean }) => void) | null> /** Mutable ref that Editor registers its diff-mode toggle into, for command palette access. */ diffToggleRef?: React.MutableRefObject<() => void> /** Mutable ref that Editor registers its table-of-contents toggle into, for app shortcuts and menus. */ tableOfContentsToggleRef?: React.MutableRefObject<() => void> /** Mutable ref that Editor registers the PDF export command into, for command palette and native menu access. */ pdfExportRef?: React.MutableRefObject<((source?: NotePdfExportSource) => void) | null> /** Mutable ref that Editor registers focused-block type changes into, for command palette access. */ turnCurrentBlockIntoRef?: React.MutableRefObject<((target: RichEditorBlockTypeDefinition) => void) | null> /** Emits short user-visible messages for editor actions. */ onToast?: (message: string | null) => void onFileCreated?: (relativePath: string) => void onFileModified?: (relativePath: string) => void onVaultChanged?: () => void workspaces?: WorkspaceIdentity[] /** 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 /** Registers a hook that flushes pending rich-editor changes into app state before external actions. */ flushPendingEditorContentRef?: React.MutableRefObject<((path: string) => void) | null> /** Registers a hook that flushes the raw editor buffer into app state before external actions. */ flushPendingRawContentRef?: React.MutableRefObject<((path: string) => void) | null> locale?: AppLocale } type ImageImportErrorHandler = (error: ImageImportError) => void function useEditorModeExclusion({ diffMode, rawMode, handleToggleDiff, handleToggleRaw, rawToggleRef, diffToggleRef, }: { diffMode: boolean rawMode: boolean handleToggleDiff: () => void | Promise handleToggleRaw: () => void rawToggleRef?: React.MutableRefObject<() => void> diffToggleRef?: React.MutableRefObject<() => void> }) { const handleToggleDiffExclusive = useCallback(async () => { if (!diffMode && rawMode) handleToggleRaw() await handleToggleDiff() }, [diffMode, rawMode, handleToggleDiff, handleToggleRaw]) const handleToggleRawExclusive = useCallback(() => { if (!rawMode && diffMode) handleToggleDiff() handleToggleRaw() }, [rawMode, diffMode, handleToggleDiff, handleToggleRaw]) useEffect(() => { if (rawToggleRef) rawToggleRef.current = handleToggleRawExclusive }, [rawToggleRef, handleToggleRawExclusive]) useEffect(() => { if (diffToggleRef) diffToggleRef.current = handleToggleDiffExclusive }, [diffToggleRef, handleToggleDiffExclusive]) return { handleToggleDiffExclusive, handleToggleRawExclusive } } function EditorEmptyState({ locale = 'en' }: { locale?: AppLocale }) { const breadcrumbBarHeight = 52 const { onMouseDown } = useDragRegion() const quickOpenShortcut = formatShortcutDisplay({ display: '⌘P / ⌘O' }) const newNoteShortcut = formatShortcutDisplay({ display: '⌘N' }) return (
) } interface EditorSetupParams { tabs: Tab[] activeTabPath: string | null vaultPath?: string onContentChange?: (path: string, content: string) => void onLoadDiff?: (path: string) => Promise onLoadDiffAtCommit?: (path: string, commitHash: string) => Promise pendingCommitDiffRequest?: CommitDiffRequest | null onPendingCommitDiffHandled?: (requestId: number) => void getNoteStatus?: (path: string) => NoteStatus rawToggleRef?: React.MutableRefObject<() => void> diffToggleRef?: React.MutableRefObject<() => void> onImageImportError?: ImageImportErrorHandler } function imageImportErrorMessage(error: ImageImportError, locale: AppLocale | undefined): string { if (error.kind === 'unsupported-heic') { return translate(locale ?? 'en', 'editor.imageImport.unsupportedHeic', { filename: error.fileName }) } return translate(locale ?? 'en', 'editor.imageImport.unsupported', { filename: error.fileName, format: error.format }) } function handleEditorImageUploadFailure( file: File, error: unknown, onImageImportError: ImageImportErrorHandler | undefined, ): UploadImageFileResult { if (!isUnsupportedImageFormatError(error)) throw error onImageImportError?.(error) return emptyImageUploadResult(file) } function installDirectMarkdownForRealEditor(editor: ReturnType) { if (!('pmSchema' in editor) || !('_tiptapEditor' in editor)) return installBlockNoteDirectMarkdown(editor as DirectMarkdownCapableSerializer) } function useEditorSetup({ tabs, activeTabPath, vaultPath, onContentChange, onLoadDiff, onLoadDiffAtCommit, pendingCommitDiffRequest, onPendingCommitDiffHandled, getNoteStatus, rawToggleRef, diffToggleRef, onImageImportError, }: EditorSetupParams) { const vaultPathRef = useRef(vaultPath) const activeTabPathRef = useRef(activeTabPath) const onImageImportErrorRef = useRef(onImageImportError) const flushPendingEditorChangeRef = useRef<(() => boolean) | null>(null) const sheetFlushRef = useRef<((path: string) => void) | null>(null) useEffect(() => { vaultPathRef.current = vaultPath }, [vaultPath]) useEffect(() => { activeTabPathRef.current = activeTabPath }, [activeTabPath]) useEffect(() => { onImageImportErrorRef.current = onImageImportError }, [onImageImportError]) const editor = useCreateBlockNote({ ...RICH_EDITOR_BLOCKNOTE_PERFORMANCE_OPTIONS, schema, domAttributes: RICH_EDITOR_BIDI_DOM_ATTRIBUTES, uploadFile: async (file: File) => { try { return await uploadImageFile(file, vaultPathRef.current) } catch (error) { return handleEditorImageUploadFailure(file, error, onImageImportErrorRef.current) } }, pasteHandler: handleRichEditorPaste, _tiptapOptions: { injectNonce: RUNTIME_STYLE_NONCE }, extensions: [ createRichEditorTransformErrorRecoveryExtension(), createImeCompositionKeyGuardExtension(), createMarkdownHighlightShortcutExtension(), createRichEditorMarkdownInputTransformExtension(), createRichEditorTextDirectionExtension(), createRichEditorBlockSelectionExtension(), ], }) installDirectMarkdownForRealEditor(editor) useEffect(() => { installRichEditorDispatchPerformanceProbe(editor, () => activeTabPathRef.current) }, [editor]) useFilenameAutolinkGuard(editor) const activeTab = tabs.find((t) => t.entry.path === activeTabPath) ?? null const { rawMode, handleToggleRaw, rawLatestContentRef, pendingRawExitContent, setPendingRawExitContent, rawModeContentOverride, } = useRawModeWithFlush( editor, activeTabPath, activeTab?.content ?? null, onContentChange, vaultPath, flushPendingEditorChangeRef, ) const rawModeContent = resolveRawModeContent({ activeTab, rawModeContentOverride }) useEffect(() => { setPendingRawExitContent((current) => resolvePendingRawExitContent({ activeTabPath, tabs, pendingRawExitContent: current, })) }, [activeTabPath, setPendingRawExitContent, tabs]) const { activeTabIsSheet, richEditorActiveTabPath, tabsForEditorSwap, } = useRichEditorSheetSwapState({ activeTab, activeTabPath, tabs, pendingRawExitContent, }) const { editorContentPath, handleEditorChange, flushPendingEditorChange, editorMountedRef, } = useEditorTabSwap({ tabs: tabsForEditorSwap, activeTabPath: richEditorActiveTabPath, editor, onContentChange, rawMode, vaultPath, }) const richEditorContentReady = useRichEditorContentReadiness({ activeTab, activeTabIsSheet, editorContentPath, }) useEffect(() => { flushPendingEditorChangeRef.current = flushPendingEditorChange return () => { if (flushPendingEditorChangeRef.current === flushPendingEditorChange) { flushPendingEditorChangeRef.current = null } } }, [flushPendingEditorChange]) useEditorFocus(editor, editorMountedRef) const { diffMode, diffContent, diffLoading, handleToggleDiff, handleViewCommitDiff } = useDiffMode({ activeTabPath, onLoadDiff, onLoadDiffAtCommit, pendingCommitDiffRequest, onPendingCommitDiffHandled, }) const { handleToggleDiffExclusive, handleToggleRawExclusive } = useEditorModeExclusion({ diffMode, rawMode, handleToggleDiff, handleToggleRaw, rawToggleRef, diffToggleRef, }) const isLoadingNewTab = activeTabPath !== null && !activeTab const activeStatus = activeTab ? getNoteStatus?.(activeTab.entry.path) ?? 'clean' : 'clean' const showDiffToggle = !!(activeTab && (diffMode || activeStatus === 'modified')) return { editor, activeTab, rawLatestContentRef, rawModeContent, rawMode, diffMode, diffContent, diffLoading, handleToggleDiffExclusive, handleToggleRawExclusive, handleEditorChange, flushPendingEditorChange, handleViewCommitDiff, isLoadingNewTab, activeStatus, showDiffToggle, sheetFlushRef, richEditorContentReady, } } function useEditorFindCommand({ activeTab, findInNoteRef, handleToggleRawExclusive, rawMode, }: { activeTab: Tab | null findInNoteRef?: EditorProps['findInNoteRef'] handleToggleRawExclusive: () => void rawMode: boolean }): RawEditorFindRequest | null { const [findRequest, setFindRequest] = useState(null) const handleFindInNote = useCallback((options: { replace?: boolean } = {}) => { if (!activeTab || activeTab.entry.fileKind === 'binary') return if (!rawMode) handleToggleRawExclusive() setFindRequest((current) => ({ id: (current?.id ?? 0) + 1, path: activeTab.entry.path, replace: options.replace === true, })) }, [activeTab, handleToggleRawExclusive, rawMode]) useEffect(() => { if (!findInNoteRef) return findInNoteRef.current = handleFindInNote return () => { if (findInNoteRef.current === handleFindInNote) { findInNoteRef.current = null } } }, [findInNoteRef, handleFindInNote]) return findRequest } function EditorLayout({ tabs, activeTabPath, activeTab, isLoadingNewTab, isVaultLoading, entries, editor, diffMode, diffContent, diffLoading, richEditorContentReady, handleToggleDiffExclusive, rawMode, handleToggleRawExclusive, onContentChange, onSave, activeStatus, showDiffToggle, showAIChat, onToggleAIChat, aiWorkspaceSurface, showTableOfContents, onToggleTableOfContents, inspectorCollapsed, onToggleInspector, onNavigateWikilink, handleEditorChange, onToggleFavorite, onToggleOrganized, onEnterNeighborhood, onRevealFile, onCopyFilePath, onCopyDeepLink, onCopyGitUrl, onExportPdf, onOpenExternalFile, onDeleteNote, onArchiveNote, onUnarchiveNote, vaultPath, vaultPaths, rawModeContent, findRequest, rawLatestContentRef, sheetFlushRef, onRenameFilename, noteWidth, onToggleNoteWidth, isConflicted, onKeepMine, onKeepTheirs, onInspectorResize, inspectorWidth, defaultAiAgent, defaultAiTarget, defaultAiAgentReadiness, defaultAiAgentReady, inspectorEntry, inspectorContent, gitHistory, noteList, noteListFilter, handleViewCommitDiff, onUpdateFrontmatter, onDeleteProperty, onAddProperty, onCreateMissingType, onCreateAndOpenNote, onChangeWorkspace, onInitializeProperties, onFileCreated, onFileModified, onVaultChanged, workspaces, onUnsupportedAiPaste, onImageImportError, locale, }: { tabs: Tab[] activeTabPath: string | null activeTab: Tab | null isLoadingNewTab: boolean isVaultLoading?: boolean entries: VaultEntry[] editor: ReturnType diffMode: boolean diffContent: string | null diffLoading: boolean richEditorContentReady: boolean handleToggleDiffExclusive: () => void | Promise rawMode: boolean handleToggleRawExclusive: () => void onContentChange?: (path: string, content: string) => void onSave?: () => void activeStatus: NoteStatus showDiffToggle: boolean showAIChat?: boolean onToggleAIChat?: () => void aiWorkspaceSurface?: ReactNode showTableOfContents?: boolean onToggleTableOfContents?: () => void inspectorCollapsed: boolean onToggleInspector: () => void onNavigateWikilink: (target: string) => void handleEditorChange: () => void onToggleFavorite?: (path: string) => void onToggleOrganized?: (path: string) => void onEnterNeighborhood?: (entry: VaultEntry) => void onRevealFile?: (path: string) => void onCopyFilePath?: (path: string) => void onCopyDeepLink?: (entry: VaultEntry) => void onCopyGitUrl?: (entry: VaultEntry) => void onOpenExternalFile?: (path: string) => void onDeleteNote?: (path: string) => void onArchiveNote?: (path: string) => void onUnarchiveNote?: (path: string) => void vaultPath?: string vaultPaths?: string[] rawModeContent: string | null findRequest?: RawEditorFindRequest | null rawLatestContentRef: React.MutableRefObject sheetFlushRef: React.MutableRefObject<((path: string) => void) | null> onRenameFilename?: (path: string, newFilenameStem: string) => void noteWidth?: NoteWidthMode onToggleNoteWidth?: () => void isConflicted?: boolean onKeepMine?: (path: string) => void onKeepTheirs?: (path: string) => void onInspectorResize: (delta: number) => void inspectorWidth: number defaultAiAgent: AiAgentId defaultAiTarget?: AiTarget defaultAiAgentReadiness?: AiAgentReadiness defaultAiAgentReady: boolean inspectorEntry: VaultEntry | null inspectorContent: string | null gitHistory: GitCommit[] noteList?: NoteListItem[] noteListFilter?: { type: string | null; query: string } handleViewCommitDiff: (commitHash: string) => Promise onUpdateFrontmatter?: (path: string, key: string, value: FrontmatterValue, options?: FrontmatterOpOptions) => Promise onDeleteProperty?: (path: string, key: string, options?: FrontmatterOpOptions) => Promise onAddProperty?: (path: string, key: string, value: FrontmatterValue, options?: FrontmatterOpOptions) => Promise onCreateMissingType?: (path: string, missingType: string, nextTypeName: string) => Promise onCreateAndOpenNote?: (title: string) => Promise onChangeWorkspace?: (entry: VaultEntry, workspace: WorkspaceIdentity) => Promise | void onInitializeProperties?: (path: string) => void onFileCreated?: (relativePath: string) => void onFileModified?: (relativePath: string) => void onVaultChanged?: () => void workspaces?: WorkspaceIdentity[] onUnsupportedAiPaste?: (message: string) => void onImageImportError?: ImageImportErrorHandler locale?: AppLocale onExportPdf?: (source?: NotePdfExportSource) => void }) { const activeBinaryTab = activeTab?.entry.fileKind === 'binary' ? activeTab : null const showEmptyState = tabs.length === 0 && activeTabPath === null && !isVaultLoading return (
{showEmptyState ? : activeBinaryTab ? ( ) : onExportPdf?.('breadcrumb')} onDeleteNote={onDeleteNote} onArchiveNote={onArchiveNote} onUnarchiveNote={onUnarchiveNote} vaultPath={vaultPath} rawModeContent={rawModeContent} findRequest={findRequest} rawLatestContentRef={rawLatestContentRef} sheetFlushRef={sheetFlushRef} onRenameFilename={onRenameFilename} noteWidth={noteWidth} onToggleNoteWidth={onToggleNoteWidth} isConflicted={isConflicted} onKeepMine={onKeepMine} onKeepTheirs={onKeepTheirs} onImageImportError={onImageImportError} locale={locale} /> } {(showTableOfContents || !inspectorCollapsed) && } {showAIChat && aiWorkspaceSurface}
) } type EditorRuntime = ReturnType type EditorLayoutProps = Parameters[0] function buildEditorLayoutProps( props: EditorProps, runtime: EditorRuntime, findRequest: RawEditorFindRequest | null, ): EditorLayoutProps { return { ...props, ...runtime, activeTabPath: props.activeTabPath, defaultAiAgent: props.defaultAiAgent ?? DEFAULT_AI_AGENT, defaultAiAgentReady: props.defaultAiAgentReady ?? true, findRequest, } } export const Editor = memo(function Editor(props: EditorProps) { const { locale, onToast } = props const handleImageImportError = useCallback((error: ImageImportError) => { onToast?.(imageImportErrorMessage(error, locale)) }, [locale, onToast]) const runtime = useEditorSetup({ tabs: props.tabs, activeTabPath: props.activeTabPath, vaultPath: props.vaultPath, onContentChange: props.onContentChange, onLoadDiff: props.onLoadDiff, onLoadDiffAtCommit: props.onLoadDiffAtCommit, pendingCommitDiffRequest: props.pendingCommitDiffRequest, onPendingCommitDiffHandled: props.onPendingCommitDiffHandled, getNoteStatus: props.getNoteStatus, rawToggleRef: props.rawToggleRef, diffToggleRef: props.diffToggleRef, onImageImportError: handleImageImportError, }) const findRequest = useEditorFindCommand({ activeTab: runtime.activeTab, findInNoteRef: props.findInNoteRef, handleToggleRawExclusive: runtime.handleToggleRawExclusive, rawMode: runtime.rawMode, }) useTurnCurrentBlockIntoCommand({ activeTab: runtime.activeTab, diffMode: runtime.diffMode, editor: runtime.editor, rawMode: runtime.rawMode, turnCurrentBlockIntoRef: props.turnCurrentBlockIntoRef, }) const handleExportPdf = useEditorPdfExport({ activeTab: runtime.activeTab, diffMode: runtime.diffMode, handleToggleDiffExclusive: runtime.handleToggleDiffExclusive, handleToggleRawExclusive: runtime.handleToggleRawExclusive, locale: props.locale, onToast: props.onToast, pdfExportRef: props.pdfExportRef, rawMode: runtime.rawMode, }) useRegisterEditorContentFlushes({ activeTab: runtime.activeTab, flushPendingEditorChange: runtime.flushPendingEditorChange, flushPendingEditorContentRef: props.flushPendingEditorContentRef, sheetFlushRef: runtime.sheetFlushRef, rawLatestContentRef: runtime.rawLatestContentRef, rawMode: runtime.rawMode, onContentChange: props.onContentChange, flushPendingRawContentRef: props.flushPendingRawContentRef, }) const rightPanel = useRightPanelExclusion(props) const { tableOfContentsToggleRef } = props useEffect(() => { if (tableOfContentsToggleRef) { tableOfContentsToggleRef.current = rightPanel.handleToggleTableOfContents } }, [tableOfContentsToggleRef, rightPanel.handleToggleTableOfContents]) return ( ) })