After createTheme(), the theme file was created and the sidebar navigated to the Theme section, but the note was never opened in the editor. Now captures the returned path and opens it via handleSelectNote. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
485 lines
24 KiB
TypeScript
485 lines
24 KiB
TypeScript
import { useCallback, useEffect, useRef, useState } from 'react'
|
|
import { Sidebar } from './components/Sidebar'
|
|
import { NoteList } from './components/NoteList'
|
|
import { Editor } from './components/Editor'
|
|
import { ResizeHandle } from './components/ResizeHandle'
|
|
import { CreateTypeDialog } from './components/CreateTypeDialog'
|
|
import { QuickOpenPalette } from './components/QuickOpenPalette'
|
|
import { CommandPalette } from './components/CommandPalette'
|
|
import { SearchPanel } from './components/SearchPanel'
|
|
import { Toast } from './components/Toast'
|
|
import { CommitDialog } from './components/CommitDialog'
|
|
import { StatusBar } from './components/StatusBar'
|
|
import { SettingsPanel } from './components/SettingsPanel'
|
|
import { GitHubVaultModal } from './components/GitHubVaultModal'
|
|
import { WelcomeScreen } from './components/WelcomeScreen'
|
|
import { useMcpRegistration } from './hooks/useMcpRegistration'
|
|
import { useVaultLoader } from './hooks/useVaultLoader'
|
|
import { useSettings } from './hooks/useSettings'
|
|
import { useNoteActions } from './hooks/useNoteActions'
|
|
import { useCommitFlow } from './hooks/useCommitFlow'
|
|
import { useViewMode } from './hooks/useViewMode'
|
|
import { useEntryActions } from './hooks/useEntryActions'
|
|
import { useAppCommands } from './hooks/useAppCommands'
|
|
import { useDialogs } from './hooks/useDialogs'
|
|
import { useVaultSwitcher } from './hooks/useVaultSwitcher'
|
|
import { useGitHistory } from './hooks/useGitHistory'
|
|
import { useUpdater, restartApp } from './hooks/useUpdater'
|
|
import { useNavigationHistory } from './hooks/useNavigationHistory'
|
|
import { useAutoSync } from './hooks/useAutoSync'
|
|
import { useConflictResolver } from './hooks/useConflictResolver'
|
|
import { useIndexing } from './hooks/useIndexing'
|
|
import { useZoom } from './hooks/useZoom'
|
|
import { useBuildNumber } from './hooks/useBuildNumber'
|
|
import { useOnboarding } from './hooks/useOnboarding'
|
|
import { useThemeManager } from './hooks/useThemeManager'
|
|
import { useEditorSaveWithLinks } from './hooks/useEditorSaveWithLinks'
|
|
import { useNavigationGestures } from './hooks/useNavigationGestures'
|
|
import { ConflictResolverModal } from './components/ConflictResolverModal'
|
|
import { UpdateBanner } from './components/UpdateBanner'
|
|
import { invoke } from '@tauri-apps/api/core'
|
|
import { isTauri, mockInvoke } from './mock-tauri'
|
|
import type { SidebarSelection } from './types'
|
|
import './App.css'
|
|
|
|
// Type declaration for mock content storage
|
|
declare global {
|
|
interface Window {
|
|
__mockContent?: Record<string, string>
|
|
}
|
|
}
|
|
|
|
const DEFAULT_SELECTION: SidebarSelection = { kind: 'filter', filter: 'all' }
|
|
|
|
function useBulkActions(
|
|
entryActions: { handleArchiveNote: (path: string) => Promise<void>; handleTrashNote: (path: string) => Promise<void> },
|
|
setToastMessage: (msg: string | null) => void,
|
|
) {
|
|
const handleBulkArchive = useCallback(async (paths: string[]) => {
|
|
for (const path of paths) await entryActions.handleArchiveNote(path)
|
|
setToastMessage(`${paths.length} note${paths.length > 1 ? 's' : ''} archived`)
|
|
}, [entryActions, setToastMessage])
|
|
|
|
const handleBulkTrash = useCallback(async (paths: string[]) => {
|
|
for (const path of paths) await entryActions.handleTrashNote(path)
|
|
setToastMessage(`${paths.length} note${paths.length > 1 ? 's' : ''} moved to trash`)
|
|
}, [entryActions, setToastMessage])
|
|
|
|
return { handleBulkArchive, handleBulkTrash }
|
|
}
|
|
|
|
function useLayoutPanels() {
|
|
const [sidebarWidth, setSidebarWidth] = useState(250)
|
|
const [noteListWidth, setNoteListWidth] = useState(300)
|
|
const [inspectorWidth, setInspectorWidth] = useState(280)
|
|
const [inspectorCollapsed, setInspectorCollapsed] = useState(false)
|
|
const handleSidebarResize = useCallback((delta: number) => setSidebarWidth((w) => Math.max(150, Math.min(400, w + delta))), [])
|
|
const handleNoteListResize = useCallback((delta: number) => setNoteListWidth((w) => Math.max(200, Math.min(500, w + delta))), [])
|
|
const handleInspectorResize = useCallback((delta: number) => setInspectorWidth((w) => Math.max(200, Math.min(500, w - delta))), [])
|
|
return { sidebarWidth, noteListWidth, inspectorWidth, inspectorCollapsed, setInspectorCollapsed, handleSidebarResize, handleNoteListResize, handleInspectorResize }
|
|
}
|
|
|
|
/** Wraps useEditorSave to also keep outgoingLinks in sync on save and on content change. */
|
|
function App() {
|
|
const [selection, setSelection] = useState<SidebarSelection>(DEFAULT_SELECTION)
|
|
const layout = useLayoutPanels()
|
|
const [toastMessage, setToastMessage] = useState<string | null>(null)
|
|
const dialogs = useDialogs()
|
|
|
|
// onSwitch closure captures `notes` declared below — safe because it's only
|
|
// called on user interaction, never during render (refs inside the hook
|
|
// guarantee the latest closure is always used).
|
|
const vaultSwitcher = useVaultSwitcher({
|
|
onSwitch: () => { setSelection(DEFAULT_SELECTION); notes.closeAllTabs() },
|
|
onToast: (msg) => setToastMessage(msg),
|
|
})
|
|
|
|
const onboarding = useOnboarding(vaultSwitcher.vaultPath)
|
|
|
|
// When onboarding resolves to a different vault path, update the switcher
|
|
const resolvedPath = onboarding.state.status === 'ready' ? onboarding.state.vaultPath : vaultSwitcher.vaultPath
|
|
const vault = useVaultLoader(resolvedPath)
|
|
const { settings, saveSettings } = useSettings()
|
|
const themeManager = useThemeManager(resolvedPath, vault.entries, vault.allContent)
|
|
|
|
useMcpRegistration(resolvedPath, setToastMessage)
|
|
|
|
const autoSync = useAutoSync({
|
|
vaultPath: resolvedPath,
|
|
intervalMinutes: settings.auto_pull_interval_minutes,
|
|
onVaultUpdated: vault.reloadVault,
|
|
onConflict: (files) => {
|
|
const names = files.map((f) => f.split('/').pop()).join(', ')
|
|
setToastMessage(`Conflict in ${names} — click to resolve`)
|
|
},
|
|
onToast: (msg) => setToastMessage(msg),
|
|
})
|
|
|
|
// Ref bridges for conflict resolution callbacks (notes declared below)
|
|
const openConflictFileRef = useRef<(relativePath: string) => void>(() => {})
|
|
|
|
const indexing = useIndexing(resolvedPath)
|
|
|
|
const conflictResolver = useConflictResolver({
|
|
vaultPath: resolvedPath,
|
|
onResolved: () => {
|
|
dialogs.closeConflictResolver()
|
|
autoSync.resumePull()
|
|
vault.reloadVault()
|
|
autoSync.triggerSync()
|
|
},
|
|
onToast: (msg) => setToastMessage(msg),
|
|
onOpenFile: (relativePath) => openConflictFileRef.current(relativePath),
|
|
})
|
|
|
|
const handleOpenConflictResolver = useCallback(() => {
|
|
if (autoSync.conflictFiles.length === 0) return
|
|
autoSync.pausePull()
|
|
conflictResolver.initFiles(autoSync.conflictFiles)
|
|
dialogs.openConflictResolver()
|
|
}, [autoSync, conflictResolver, dialogs])
|
|
|
|
const handleCloseConflictResolver = useCallback(() => {
|
|
autoSync.resumePull()
|
|
dialogs.closeConflictResolver()
|
|
}, [autoSync, dialogs])
|
|
|
|
// Ref bridges handleContentChange (created after notes) into useNoteActions.
|
|
// Read at callback time, so it's always current when user presses Cmd+N.
|
|
const contentChangeRef = useRef<(path: string, content: string) => void>(() => {})
|
|
|
|
const notes = useNoteActions({ addEntry: vault.addEntry, removeEntry: vault.removeEntry, updateContent: vault.updateContent, entries: vault.entries, setToastMessage, updateEntry: vault.updateEntry, addPendingSave: vault.addPendingSave, removePendingSave: vault.removePendingSave, trackUnsaved: vault.trackUnsaved, clearUnsaved: vault.clearUnsaved, unsavedPaths: vault.unsavedPaths, markContentPending: (path, content) => contentChangeRef.current(path, content), onNewNotePersisted: vault.loadModifiedFiles })
|
|
|
|
const navHistory = useNavigationHistory()
|
|
|
|
// Push to navigation history whenever the active tab changes (user-initiated)
|
|
const navFromHistoryRef = useRef(false)
|
|
useEffect(() => {
|
|
if (notes.activeTabPath && !navFromHistoryRef.current) {
|
|
navHistory.push(notes.activeTabPath)
|
|
}
|
|
navFromHistoryRef.current = false
|
|
}, [notes.activeTabPath]) // eslint-disable-line react-hooks/exhaustive-deps -- navHistory.push is stable
|
|
|
|
const isEntryExists = useCallback((path: string) => vault.entries.some(e => e.path === path), [vault.entries])
|
|
|
|
const handleGoBack = useCallback(() => {
|
|
const target = navHistory.goBack(isEntryExists)
|
|
if (target) {
|
|
navFromHistoryRef.current = true
|
|
if (notes.tabs.some(t => t.entry.path === target)) {
|
|
notes.handleSwitchTab(target)
|
|
} else {
|
|
const entry = vault.entries.find(e => e.path === target)
|
|
if (entry) notes.handleSelectNote(entry)
|
|
}
|
|
}
|
|
}, [navHistory, isEntryExists, vault.entries, notes])
|
|
|
|
const handleGoForward = useCallback(() => {
|
|
const target = navHistory.goForward(isEntryExists)
|
|
if (target) {
|
|
navFromHistoryRef.current = true
|
|
if (notes.tabs.some(t => t.entry.path === target)) {
|
|
notes.handleSwitchTab(target)
|
|
} else {
|
|
const entry = vault.entries.find(e => e.path === target)
|
|
if (entry) notes.handleSelectNote(entry)
|
|
}
|
|
}
|
|
}, [navHistory, isEntryExists, vault.entries, notes])
|
|
|
|
useNavigationGestures({ onGoBack: handleGoBack, onGoForward: handleGoForward })
|
|
|
|
const { triggerIncrementalIndex } = indexing
|
|
const onAfterSave = useCallback(() => {
|
|
vault.loadModifiedFiles()
|
|
triggerIncrementalIndex()
|
|
}, [vault, triggerIncrementalIndex])
|
|
|
|
const { handleSave: handleSaveRaw, handleContentChange, savePendingForPath, savePending } = useEditorSaveWithLinks({
|
|
updateContent: vault.updateContent, updateEntry: vault.updateEntry,
|
|
setTabs: notes.setTabs, setToastMessage, onAfterSave,
|
|
onNotePersisted: vault.clearUnsaved,
|
|
})
|
|
useEffect(() => { contentChangeRef.current = handleContentChange }, [handleContentChange])
|
|
|
|
// Wire conflict file opener now that notes is available
|
|
useEffect(() => {
|
|
openConflictFileRef.current = (relativePath: string) => {
|
|
const fullPath = `${resolvedPath}/${relativePath}`
|
|
const entry = vault.entries.find(e => e.path === fullPath)
|
|
if (entry) {
|
|
notes.handleSelectNote(entry)
|
|
dialogs.closeConflictResolver()
|
|
}
|
|
}
|
|
}, [resolvedPath, vault.entries, notes, dialogs])
|
|
|
|
// Wrap handleSave to also persist unsaved notes that have no pending edits (user pressed Cmd+S without typing)
|
|
const handleSave = useCallback(async () => {
|
|
const activeTab = notes.tabs.find(t => t.entry.path === notes.activeTabPath)
|
|
const fallback = activeTab && vault.unsavedPaths.has(activeTab.entry.path)
|
|
? { path: activeTab.entry.path, content: activeTab.content }
|
|
: undefined
|
|
await handleSaveRaw(fallback)
|
|
}, [handleSaveRaw, notes.tabs, notes.activeTabPath, vault.unsavedPaths])
|
|
|
|
const commitFlow = useCommitFlow({ savePending, loadModifiedFiles: vault.loadModifiedFiles, commitAndPush: vault.commitAndPush, setToastMessage })
|
|
|
|
const entryActions = useEntryActions({
|
|
entries: vault.entries, updateEntry: vault.updateEntry,
|
|
handleUpdateFrontmatter: notes.handleUpdateFrontmatter,
|
|
handleDeleteProperty: notes.handleDeleteProperty, setToastMessage,
|
|
createTypeEntry: notes.createTypeEntrySilent,
|
|
})
|
|
|
|
const handleDeleteNote = useCallback(async (path: string) => {
|
|
try {
|
|
if (isTauri()) await invoke('delete_note', { path })
|
|
else await mockInvoke('delete_note', { path })
|
|
notes.handleCloseTab(path)
|
|
vault.removeEntry(path)
|
|
setToastMessage('Note permanently deleted')
|
|
} catch (e) {
|
|
setToastMessage(`Failed to delete note: ${e}`)
|
|
}
|
|
}, [notes, vault, setToastMessage])
|
|
|
|
const gitHistory = useGitHistory(notes.activeTabPath, vault.loadGitHistory)
|
|
|
|
const handleCreateType = useCallback((name: string) => {
|
|
notes.handleCreateType(name)
|
|
setToastMessage(`Type "${name}" created`)
|
|
}, [notes])
|
|
|
|
const handleRenameTab = useCallback(async (path: string, newTitle: string) => {
|
|
await savePendingForPath(path)
|
|
await notes.handleRenameNote(path, newTitle, resolvedPath, vault.replaceEntry).then(vault.loadModifiedFiles)
|
|
}, [notes, resolvedPath, vault, savePendingForPath])
|
|
|
|
/** H1→title sync: update VaultEntry.title and tab entry in memory. */
|
|
const handleTitleSync = useCallback((path: string, newTitle: string) => {
|
|
vault.updateEntry(path, { title: newTitle })
|
|
notes.setTabs(prev => prev.map(t =>
|
|
t.entry.path === path ? { ...t, entry: { ...t.entry, title: newTitle } } : t
|
|
))
|
|
}, [vault, notes])
|
|
|
|
const bulkActions = useBulkActions(entryActions, setToastMessage)
|
|
|
|
// Raw-toggle ref: Editor registers its handleToggleRaw here so the command palette can call it
|
|
const rawToggleRef = useRef<() => void>(() => {})
|
|
// Diff-toggle ref: Editor registers its handleToggleDiff here so the command palette can call it
|
|
const diffToggleRef = useRef<() => void>(() => {})
|
|
|
|
const { setViewMode, sidebarVisible, noteListVisible } = useViewMode()
|
|
const zoom = useZoom()
|
|
const buildNumber = useBuildNumber()
|
|
|
|
const { status: updateStatus, actions: updateActions } = useUpdater()
|
|
|
|
const handleCheckForUpdates = useCallback(async () => {
|
|
if (updateStatus.state === 'downloading') {
|
|
setToastMessage('Update is downloading…')
|
|
return
|
|
}
|
|
if (updateStatus.state === 'ready') {
|
|
await restartApp()
|
|
return
|
|
}
|
|
const result = await updateActions.checkForUpdates()
|
|
if (result === 'up-to-date') {
|
|
setToastMessage("You're on the latest version")
|
|
} else if (result === 'error') {
|
|
setToastMessage('Could not check for updates')
|
|
}
|
|
// 'available' → UpdateBanner handles it automatically
|
|
}, [updateActions, updateStatus.state, setToastMessage])
|
|
|
|
const commands = useAppCommands({
|
|
activeTabPath: notes.activeTabPath, activeTabPathRef: notes.activeTabPathRef,
|
|
handleCloseTabRef: notes.handleCloseTabRef, tabs: notes.tabs,
|
|
entries: vault.entries, allContent: vault.allContent,
|
|
modifiedCount: vault.modifiedFiles.length,
|
|
activeNoteModified: vault.modifiedFiles.some(f => f.path === notes.activeTabPath),
|
|
selection,
|
|
onQuickOpen: dialogs.openQuickOpen, onCommandPalette: dialogs.openCommandPalette,
|
|
onSearch: dialogs.openSearch,
|
|
onCreateNote: notes.handleCreateNoteImmediate,
|
|
onOpenDailyNote: notes.handleOpenDailyNote,
|
|
onCreateNoteOfType: notes.handleCreateNoteImmediate,
|
|
onSave: handleSave,
|
|
onOpenSettings: dialogs.openSettings,
|
|
onTrashNote: entryActions.handleTrashNote, onRestoreNote: entryActions.handleRestoreNote,
|
|
onArchiveNote: entryActions.handleArchiveNote, onUnarchiveNote: entryActions.handleUnarchiveNote,
|
|
onCommitPush: commitFlow.openCommitDialog,
|
|
onResolveConflicts: handleOpenConflictResolver,
|
|
conflictCount: autoSync.conflictFiles.length,
|
|
onSetViewMode: setViewMode,
|
|
onToggleInspector: () => layout.setInspectorCollapsed(c => !c),
|
|
onToggleDiff: () => diffToggleRef.current(),
|
|
onToggleRawEditor: () => rawToggleRef.current(),
|
|
onZoomIn: zoom.zoomIn, onZoomOut: zoom.zoomOut, onZoomReset: zoom.zoomReset,
|
|
zoomLevel: zoom.zoomLevel,
|
|
onSelect: setSelection, onCloseTab: notes.handleCloseTab,
|
|
onSwitchTab: notes.handleSwitchTab, onReplaceActiveTab: notes.handleReplaceActiveTab,
|
|
onSelectNote: notes.handleSelectNote,
|
|
onGoBack: handleGoBack, onGoForward: handleGoForward,
|
|
canGoBack: navHistory.canGoBack, canGoForward: navHistory.canGoForward,
|
|
themes: themeManager.themes, activeThemeId: themeManager.activeThemeId,
|
|
onSwitchTheme: themeManager.switchTheme,
|
|
onCreateTheme: async () => {
|
|
const path = await themeManager.createTheme()
|
|
const freshEntries = await vault.reloadVault()
|
|
setSelection({ kind: 'sectionGroup', type: 'Theme' })
|
|
if (path) {
|
|
const entry = freshEntries.find(e => e.path === path)
|
|
if (entry) notes.handleSelectNote(entry)
|
|
}
|
|
},
|
|
onOpenTheme: (themeId: string) => {
|
|
const entry = vault.entries.find(e => e.path === themeId)
|
|
if (entry) notes.handleSelectNote(entry)
|
|
},
|
|
onOpenVault: vaultSwitcher.handleOpenLocalFolder,
|
|
onCreateType: dialogs.openCreateType,
|
|
onToggleAIChat: dialogs.toggleAIChat,
|
|
onCheckForUpdates: handleCheckForUpdates,
|
|
onRemoveActiveVault: () => vaultSwitcher.removeVault(vaultSwitcher.vaultPath),
|
|
onRestoreGettingStarted: vaultSwitcher.restoreGettingStarted,
|
|
isGettingStartedHidden: vaultSwitcher.isGettingStartedHidden,
|
|
vaultCount: vaultSwitcher.allVaults.length,
|
|
})
|
|
|
|
const activeTab = notes.tabs.find((t) => t.entry.path === notes.activeTabPath) ?? null
|
|
|
|
// Show welcome/onboarding screen when vault doesn't exist
|
|
if (onboarding.state.status === 'welcome' || onboarding.state.status === 'vault-missing') {
|
|
const defaultPath = onboarding.state.defaultPath
|
|
return (
|
|
<div className="app-shell">
|
|
<WelcomeScreen
|
|
mode={onboarding.state.status === 'welcome' ? 'welcome' : 'vault-missing'}
|
|
missingPath={onboarding.state.status === 'vault-missing' ? onboarding.state.vaultPath : undefined}
|
|
defaultVaultPath={defaultPath}
|
|
onCreateVault={onboarding.handleCreateVault}
|
|
onOpenFolder={onboarding.handleOpenFolder}
|
|
creating={onboarding.creating}
|
|
error={onboarding.error}
|
|
/>
|
|
</div>
|
|
)
|
|
}
|
|
|
|
// Show loading spinner while checking vault
|
|
if (onboarding.state.status === 'loading') {
|
|
return (
|
|
<div className="app-shell">
|
|
<div style={{ width: '100%', height: '100%', display: 'flex', alignItems: 'center', justifyContent: 'center', background: 'var(--sidebar)' }}>
|
|
<span style={{ color: 'var(--muted-foreground)', fontSize: 14 }}>Loading…</span>
|
|
</div>
|
|
</div>
|
|
)
|
|
}
|
|
|
|
return (
|
|
<div className="app-shell">
|
|
<div className="app">
|
|
{sidebarVisible && (
|
|
<>
|
|
<div className="app__sidebar" style={{ width: layout.sidebarWidth }}>
|
|
<Sidebar entries={vault.entries} selection={selection} onSelect={setSelection} onSelectNote={notes.handleSelectNote} onCreateType={notes.handleCreateNoteImmediate} onCreateNewType={dialogs.openCreateType} onCustomizeType={entryActions.handleCustomizeType} onUpdateTypeTemplate={entryActions.handleUpdateTypeTemplate} onReorderSections={entryActions.handleReorderSections} onRenameSection={entryActions.handleRenameSection} modifiedCount={vault.modifiedFiles.length} onCommitPush={commitFlow.openCommitDialog} />
|
|
</div>
|
|
<ResizeHandle onResize={layout.handleSidebarResize} />
|
|
</>
|
|
)}
|
|
{noteListVisible && (
|
|
<>
|
|
<div className="app__note-list" style={{ width: layout.noteListWidth }}>
|
|
<NoteList entries={vault.entries} selection={selection} selectedNote={activeTab?.entry ?? null} allContent={vault.allContent} modifiedFiles={vault.modifiedFiles} modifiedFilesError={vault.modifiedFilesError} getNoteStatus={vault.getNoteStatus} sidebarCollapsed={!sidebarVisible} onSelectNote={notes.handleSelectNote} onReplaceActiveTab={notes.handleReplaceActiveTab} onCreateNote={notes.handleCreateNoteImmediate} onBulkArchive={bulkActions.handleBulkArchive} onBulkTrash={bulkActions.handleBulkTrash} onUpdateTypeSort={notes.handleUpdateFrontmatter} updateEntry={vault.updateEntry} />
|
|
</div>
|
|
<ResizeHandle onResize={layout.handleNoteListResize} />
|
|
</>
|
|
)}
|
|
<div className="app__editor">
|
|
<Editor
|
|
tabs={notes.tabs}
|
|
activeTabPath={notes.activeTabPath}
|
|
entries={vault.entries}
|
|
onSwitchTab={notes.handleSwitchTab}
|
|
onCloseTab={notes.handleCloseTab}
|
|
onReorderTabs={notes.handleReorderTabs}
|
|
onNavigateWikilink={notes.handleNavigateWikilink}
|
|
onLoadDiff={vault.loadDiff}
|
|
onLoadDiffAtCommit={vault.loadDiffAtCommit}
|
|
getNoteStatus={vault.getNoteStatus}
|
|
onCreateNote={notes.handleCreateNoteImmediate}
|
|
inspectorCollapsed={layout.inspectorCollapsed}
|
|
onToggleInspector={() => layout.setInspectorCollapsed((c) => !c)}
|
|
inspectorWidth={layout.inspectorWidth}
|
|
onInspectorResize={layout.handleInspectorResize}
|
|
inspectorEntry={activeTab?.entry ?? null}
|
|
inspectorContent={activeTab?.content ?? null}
|
|
allContent={vault.allContent}
|
|
gitHistory={gitHistory}
|
|
onUpdateFrontmatter={notes.handleUpdateFrontmatter}
|
|
onDeleteProperty={notes.handleDeleteProperty}
|
|
onAddProperty={notes.handleAddProperty}
|
|
showAIChat={dialogs.showAIChat}
|
|
onToggleAIChat={dialogs.toggleAIChat}
|
|
vaultPath={resolvedPath}
|
|
onTrashNote={entryActions.handleTrashNote}
|
|
onRestoreNote={entryActions.handleRestoreNote}
|
|
onDeleteNote={handleDeleteNote}
|
|
onArchiveNote={entryActions.handleArchiveNote}
|
|
onUnarchiveNote={entryActions.handleUnarchiveNote}
|
|
onRenameTab={handleRenameTab}
|
|
onContentChange={handleContentChange}
|
|
onSave={handleSave}
|
|
onTitleSync={handleTitleSync}
|
|
rawToggleRef={rawToggleRef}
|
|
diffToggleRef={diffToggleRef}
|
|
canGoBack={navHistory.canGoBack}
|
|
canGoForward={navHistory.canGoForward}
|
|
onGoBack={handleGoBack}
|
|
onGoForward={handleGoForward}
|
|
leftPanelsCollapsed={!sidebarVisible && !noteListVisible}
|
|
isDarkTheme={themeManager.isDark}
|
|
/>
|
|
</div>
|
|
</div>
|
|
<UpdateBanner status={updateStatus} actions={updateActions} />
|
|
<StatusBar noteCount={vault.entries.length} modifiedCount={vault.modifiedFiles.length} vaultPath={vaultSwitcher.vaultPath} vaults={vaultSwitcher.allVaults} onSwitchVault={vaultSwitcher.switchVault} onOpenSettings={dialogs.openSettings} onOpenLocalFolder={vaultSwitcher.handleOpenLocalFolder} onConnectGitHub={dialogs.openGitHubVault} onClickPending={() => setSelection({ kind: 'filter', filter: 'changes' })} hasGitHub={!!settings.github_token} syncStatus={autoSync.syncStatus} lastSyncTime={autoSync.lastSyncTime} conflictCount={autoSync.conflictFiles.length} lastCommitInfo={autoSync.lastCommitInfo} onTriggerSync={autoSync.triggerSync} onOpenConflictResolver={handleOpenConflictResolver} zoomLevel={zoom.zoomLevel} onZoomReset={zoom.zoomReset} buildNumber={buildNumber} indexingProgress={indexing.progress} onRemoveVault={vaultSwitcher.removeVault} />
|
|
<Toast message={toastMessage} onDismiss={() => setToastMessage(null)} />
|
|
<QuickOpenPalette open={dialogs.showQuickOpen} entries={vault.entries} onSelect={notes.handleSelectNote} onClose={dialogs.closeQuickOpen} />
|
|
<CommandPalette open={dialogs.showCommandPalette} commands={commands} onClose={dialogs.closeCommandPalette} />
|
|
<SearchPanel open={dialogs.showSearch} vaultPath={resolvedPath} entries={vault.entries} onSelectNote={notes.handleSelectNote} onClose={dialogs.closeSearch} />
|
|
<CreateTypeDialog open={dialogs.showCreateTypeDialog} onClose={dialogs.closeCreateType} onCreate={handleCreateType} />
|
|
<CommitDialog open={commitFlow.showCommitDialog} modifiedCount={vault.modifiedFiles.length} onCommit={commitFlow.handleCommitPush} onClose={commitFlow.closeCommitDialog} />
|
|
<ConflictResolverModal
|
|
open={dialogs.showConflictResolver}
|
|
fileStates={conflictResolver.fileStates}
|
|
allResolved={conflictResolver.allResolved}
|
|
committing={conflictResolver.committing}
|
|
error={conflictResolver.error}
|
|
onResolveFile={conflictResolver.resolveFile}
|
|
onOpenInEditor={conflictResolver.openInEditor}
|
|
onCommit={conflictResolver.commitResolution}
|
|
onClose={handleCloseConflictResolver}
|
|
/>
|
|
<SettingsPanel open={dialogs.showSettings} settings={settings} onSave={saveSettings} onClose={dialogs.closeSettings} themeManager={themeManager} />
|
|
<GitHubVaultModal
|
|
open={dialogs.showGitHubVault}
|
|
githubToken={settings.github_token}
|
|
onClose={dialogs.closeGitHubVault}
|
|
onVaultCloned={vaultSwitcher.handleVaultCloned}
|
|
onOpenSettings={() => { dialogs.closeGitHubVault(); dialogs.openSettings() }}
|
|
onGitHubConnected={(token, username) => saveSettings({ ...settings, github_token: token, github_username: username })}
|
|
/>
|
|
</div>
|
|
)
|
|
}
|
|
|
|
export default App
|