Files
tolaria/src/components/Editor.tsx
Test a6b2454184 refactor: remove theming system entirely
Remove all theme switching, creation, and management functionality
from the app — backend (Rust theme module, Tauri commands, menu items,
startup tasks, vault seeding), frontend (useThemeManager, ThemePropertyEditor,
themeSchema, command palette Appearance group, settings panel appearance
section, isDarkTheme prop chain), mock data, and all related tests.

Editor styling via theme.json/EditorTheme.css is preserved (not user theming).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-23 19:57:58 +01:00

334 lines
13 KiB
TypeScript

import { useRef, useEffect, useCallback, memo } from 'react'
import { useEditorTabSwap } from '../hooks/useEditorTabSwap'
import { useCreateBlockNote } from '@blocknote/react'
import '@blocknote/mantine/style.css'
import { uploadImageFile } from '../hooks/useImageDrop'
import type { VaultEntry, GitCommit, NoteStatus } from '../types'
import type { NoteListItem } from '../utils/ai-context'
import type { FrontmatterValue } from './Inspector'
import { ResizeHandle } from './ResizeHandle'
import { TabBar } from './TabBar'
import { useDiffMode } from '../hooks/useDiffMode'
import { useRawMode } from '../hooks/useRawMode'
import { useEditorFocus } from '../hooks/useEditorFocus'
import { EditorRightPanel } from './EditorRightPanel'
import { EditorContent } from './EditorContent'
import { schema } from './editorSchema'
import './Editor.css'
import './EditorTheme.css'
interface Tab {
entry: VaultEntry
content: string
}
interface EditorProps {
tabs: Tab[]
activeTabPath: string | null
entries: VaultEntry[]
onSwitchTab: (path: string) => void
onCloseTab: (path: string) => void
onReorderTabs?: (fromIndex: number, toIndex: number) => void
onNavigateWikilink: (target: string) => void
onLoadDiff?: (path: string) => Promise<string>
onLoadDiffAtCommit?: (path: string, commitHash: string) => Promise<string>
getNoteStatus?: (path: string) => NoteStatus
onCreateNote?: () => void
inspectorCollapsed: boolean
onToggleInspector: () => void
inspectorWidth: number
onInspectorResize: (delta: number) => void
inspectorEntry: VaultEntry | null
inspectorContent: string | null
gitHistory: GitCommit[]
onUpdateFrontmatter?: (path: string, key: string, value: FrontmatterValue) => Promise<void>
onDeleteProperty?: (path: string, key: string) => Promise<void>
onAddProperty?: (path: string, key: string, value: FrontmatterValue) => Promise<void>
onCreateAndOpenNote?: (title: string) => Promise<boolean>
showAIChat?: boolean
onToggleAIChat?: () => void
vaultPath?: string
noteList?: NoteListItem[]
noteListFilter?: { type: string | null; query: string }
onTrashNote?: (path: string) => void
onRestoreNote?: (path: string) => void
onDeleteNote?: (path: string) => void
onArchiveNote?: (path: string) => void
onUnarchiveNote?: (path: string) => void
onRenameTab?: (path: string, newTitle: string) => void
onContentChange?: (path: string, content: string) => void
onSave?: () => void
/** Called when the user edits the title in TitleField. */
onTitleSync?: (path: string, newTitle: string) => 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 its diff-mode toggle into, for command palette access. */
diffToggleRef?: React.MutableRefObject<() => void>
onFileCreated?: (relativePath: string) => void
onFileModified?: (relativePath: string) => void
onVaultChanged?: () => void
/** Called when user sets an emoji icon on a note. */
onSetNoteIcon?: (path: string, emoji: string) => void
/** Called when user removes an emoji icon from a note. */
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 useEditorModeExclusion({
diffMode, rawMode, handleToggleDiff, handleToggleRaw, rawToggleRef, diffToggleRef,
}: {
diffMode: boolean
rawMode: boolean
handleToggleDiff: () => void | Promise<void>
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() {
return (
<div className="flex flex-1 flex-col items-center justify-center gap-2 text-center text-muted-foreground">
<p className="m-0 text-[15px]">Select a note to start editing</p>
<span className="text-xs text-muted-foreground">Cmd+P to search &middot; Cmd+N to create</span>
</div>
)
}
interface EditorSetupParams {
tabs: Tab[]
activeTabPath: string | null
vaultPath?: string
onContentChange?: (path: string, content: string) => void
onLoadDiff?: (path: string) => Promise<string>
onLoadDiffAtCommit?: (path: string, commitHash: string) => Promise<string>
getNoteStatus?: (path: string) => NoteStatus
rawToggleRef?: React.MutableRefObject<() => void>
diffToggleRef?: React.MutableRefObject<() => void>
}
function useRawModeWithFlush(
activeTabPath: string | null,
onContentChange?: (path: string, content: string) => void,
) {
const rawLatestContentRef = useRef<string | null>(null)
const handleBeforeRawEnd = useCallback(() => {
if (rawLatestContentRef.current != null && activeTabPath) {
onContentChange?.(activeTabPath, rawLatestContentRef.current)
}
rawLatestContentRef.current = null
}, [activeTabPath, onContentChange])
const { rawMode, handleToggleRaw } = useRawMode({
activeTabPath, onBeforeRawEnd: handleBeforeRawEnd,
})
// Flush raw editor content when switching tabs while raw mode stays active.
const prevTabPathRef = useRef(activeTabPath)
const onContentChangeRef = useRef(onContentChange)
useEffect(() => { onContentChangeRef.current = onContentChange }, [onContentChange])
useEffect(() => {
const prev = prevTabPathRef.current
prevTabPathRef.current = activeTabPath
const hasUnflushedContent = prev && prev !== activeTabPath && rawMode && rawLatestContentRef.current != null
if (hasUnflushedContent) {
onContentChangeRef.current?.(prev, rawLatestContentRef.current!)
rawLatestContentRef.current = null
}
}, [activeTabPath, rawMode])
return { rawMode, handleToggleRaw, rawLatestContentRef }
}
function useEditorSetup({
tabs, activeTabPath, vaultPath, onContentChange,
onLoadDiff, onLoadDiffAtCommit, getNoteStatus,
rawToggleRef, diffToggleRef,
}: EditorSetupParams) {
const vaultPathRef = useRef(vaultPath)
useEffect(() => { vaultPathRef.current = vaultPath }, [vaultPath])
const editor = useCreateBlockNote({
schema,
uploadFile: (file: File) => uploadImageFile(file, vaultPathRef.current),
})
const activeTab = tabs.find((t) => t.entry.path === activeTabPath) ?? null
const { rawMode, handleToggleRaw, rawLatestContentRef } = useRawModeWithFlush(
activeTabPath, onContentChange,
)
const { handleEditorChange, editorMountedRef } = useEditorTabSwap({
tabs, activeTabPath, editor, onContentChange, rawMode,
})
useEditorFocus(editor, editorMountedRef)
const { diffMode, diffContent, diffLoading, handleToggleDiff, handleViewCommitDiff } = useDiffMode({
activeTabPath, onLoadDiff, onLoadDiffAtCommit,
})
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,
rawMode, diffMode, diffContent, diffLoading,
handleToggleDiffExclusive, handleToggleRawExclusive,
handleEditorChange, handleViewCommitDiff,
isLoadingNewTab, activeStatus, showDiffToggle,
}
}
export const Editor = memo(function Editor(props: EditorProps) {
const {
tabs, activeTabPath, entries, onSwitchTab, onCloseTab, onReorderTabs, onNavigateWikilink,
getNoteStatus, onCreateNote,
inspectorCollapsed, onToggleInspector, inspectorWidth, onInspectorResize,
inspectorEntry, inspectorContent, gitHistory,
onUpdateFrontmatter, onDeleteProperty, onAddProperty, onCreateAndOpenNote,
showAIChat, onToggleAIChat,
vaultPath, noteList, noteListFilter,
onTrashNote, onRestoreNote, onDeleteNote, onArchiveNote, onUnarchiveNote,
onContentChange, onSave, onTitleSync,
canGoBack, canGoForward, onGoBack, onGoForward, leftPanelsCollapsed,
onFileCreated, onFileModified, onVaultChanged,
onSetNoteIcon, onRemoveNoteIcon,
isConflicted, onKeepMine, onKeepTheirs,
} = props
const {
editor, activeTab, rawLatestContentRef,
rawMode, diffMode, diffContent, diffLoading,
handleToggleDiffExclusive, handleToggleRawExclusive,
handleEditorChange, handleViewCommitDiff,
isLoadingNewTab, activeStatus, showDiffToggle,
} = useEditorSetup({
tabs, activeTabPath, vaultPath, onContentChange,
onLoadDiff: props.onLoadDiff,
onLoadDiffAtCommit: props.onLoadDiffAtCommit, getNoteStatus,
rawToggleRef: props.rawToggleRef, diffToggleRef: props.diffToggleRef,
})
return (
<div className="editor flex flex-col min-h-0 overflow-hidden bg-background text-foreground">
<TabBar
tabs={tabs}
activeTabPath={activeTabPath}
getNoteStatus={getNoteStatus}
onSwitchTab={onSwitchTab}
onCloseTab={onCloseTab}
onCreateNote={onCreateNote}
onReorderTabs={onReorderTabs}
onRenameTab={props.onRenameTab}
canGoBack={canGoBack}
canGoForward={canGoForward}
onGoBack={onGoBack}
onGoForward={onGoForward}
leftPanelsCollapsed={leftPanelsCollapsed}
/>
<div className="flex flex-1 min-h-0">
{tabs.length === 0
? <EditorEmptyState />
: <EditorContent
activeTab={activeTab}
isLoadingNewTab={isLoadingNewTab}
entries={entries}
editor={editor}
diffMode={diffMode}
diffContent={diffContent}
diffLoading={diffLoading}
onToggleDiff={handleToggleDiffExclusive}
rawMode={rawMode}
onToggleRaw={handleToggleRawExclusive}
onRawContentChange={onContentChange}
onSave={onSave}
activeStatus={activeStatus}
showDiffToggle={showDiffToggle}
showAIChat={showAIChat}
onToggleAIChat={onToggleAIChat}
inspectorCollapsed={inspectorCollapsed}
onToggleInspector={onToggleInspector}
onNavigateWikilink={onNavigateWikilink}
onEditorChange={handleEditorChange}
onTrashNote={onTrashNote}
onRestoreNote={onRestoreNote}
onDeleteNote={onDeleteNote}
onArchiveNote={onArchiveNote}
onUnarchiveNote={onUnarchiveNote}
vaultPath={vaultPath}
rawLatestContentRef={rawLatestContentRef}
onTitleChange={onTitleSync}
onSetNoteIcon={onSetNoteIcon}
onRemoveNoteIcon={onRemoveNoteIcon}
isConflicted={isConflicted}
onKeepMine={onKeepMine}
onKeepTheirs={onKeepTheirs}
/>
}
{(showAIChat || !inspectorCollapsed) && <ResizeHandle onResize={onInspectorResize} />}
<EditorRightPanel
showAIChat={showAIChat}
inspectorCollapsed={inspectorCollapsed}
inspectorWidth={inspectorWidth}
inspectorEntry={inspectorEntry}
inspectorContent={inspectorContent}
entries={entries}
gitHistory={gitHistory}
vaultPath={vaultPath ?? ''}
openTabs={tabs.map(t => t.entry)}
noteList={noteList}
noteListFilter={noteListFilter}
onToggleInspector={onToggleInspector}
onToggleAIChat={onToggleAIChat}
onNavigateWikilink={onNavigateWikilink}
onViewCommitDiff={handleViewCommitDiff}
onUpdateFrontmatter={onUpdateFrontmatter}
onDeleteProperty={onDeleteProperty}
onAddProperty={onAddProperty}
onCreateAndOpenNote={onCreateAndOpenNote}
onOpenNote={onNavigateWikilink}
onFileCreated={onFileCreated}
onFileModified={onFileModified}
onVaultChanged={onVaultChanged}
/>
</div>
</div>
)
})