Files
tolaria/src/components/editor-content/EditorContentLayout.tsx

379 lines
10 KiB
TypeScript
Raw Normal View History

import type React from 'react'
2026-04-27 10:12:13 +02:00
import { useCallback, useEffect, useRef } from 'react'
2026-04-26 04:41:18 +02:00
import { cn } from '@/lib/utils'
2026-04-26 19:37:21 +02:00
import { translate, type AppLocale } from '../../lib/i18n'
2026-04-27 10:12:13 +02:00
import { dispatchEditorFindAvailability } from '../../utils/editorFindEvents'
import { DiffView } from '../DiffView'
import { BreadcrumbBar } from '../BreadcrumbBar'
import { ArchivedNoteBanner } from '../ArchivedNoteBanner'
import { ConflictNoteBanner } from '../ConflictNoteBanner'
import { RawEditorView } from '../RawEditorView'
import { SingleEditorView } from '../SingleEditorView'
import type { useEditorContentModel } from './useEditorContentModel'
type EditorContentModel = ReturnType<typeof useEditorContentModel>
type BreadcrumbActions = Pick<
EditorContentModel,
| 'diffMode'
| 'diffLoading'
| 'onToggleDiff'
| 'effectiveRawMode'
| 'onToggleRaw'
| 'forceRawMode'
| 'showAIChat'
| 'onToggleAIChat'
| 'inspectorCollapsed'
| 'onToggleInspector'
| 'showDiffToggle'
| 'onToggleFavorite'
| 'onToggleOrganized'
2026-04-27 01:45:16 +02:00
| 'onRevealFile'
| 'onCopyFilePath'
| 'onDeleteNote'
| 'onArchiveNote'
| 'onUnarchiveNote'
| 'onRenameFilename'
2026-04-27 10:10:58 +02:00
| 'noteLayout'
| 'onToggleNoteLayout'
>
function EditorLoadingSkeleton() {
return (
<div className="flex flex-1 flex-col gap-3 p-8 animate-pulse" style={{ minHeight: 0 }}>
<div className="h-6 w-2/5 rounded bg-muted" />
<div className="h-4 w-4/5 rounded bg-muted" />
<div className="h-4 w-3/5 rounded bg-muted" />
<div className="h-4 w-4/5 rounded bg-muted" />
<div className="h-4 w-2/5 rounded bg-muted" />
</div>
)
}
2026-04-26 19:37:21 +02:00
function DiffModeView({ diffContent, locale = 'en', onToggleDiff }: { diffContent: string | null; locale?: AppLocale; onToggleDiff: () => void }) {
const label = translate(locale, 'editor.toolbar.rawReturn')
return (
<div className="flex-1 overflow-auto">
<button
className="flex items-center gap-1.5 px-4 py-2 text-xs text-primary bg-muted border-b border-border cursor-pointer hover:bg-accent transition-colors w-full border-t-0 border-l-0 border-r-0"
onClick={onToggleDiff}
2026-04-26 19:37:21 +02:00
title={label}
>
<span style={{ fontSize: 14, lineHeight: 1 }}>&larr;</span>
2026-04-26 19:37:21 +02:00
{label}
</button>
<DiffView diff={diffContent ?? ''} />
</div>
)
}
function RawModeEditorSection({
activeTab,
entries,
2026-04-27 10:12:13 +02:00
findRequest,
rawMode,
rawModeContent,
onRawContentChange,
onSave,
rawLatestContentRef,
vaultPath,
2026-04-26 19:37:21 +02:00
locale,
}: Pick<
EditorContentModel,
2026-04-27 10:12:13 +02:00
'activeTab' | 'entries' | 'findRequest' | 'onRawContentChange' | 'onSave' | 'rawLatestContentRef' | 'rawModeContent' | 'vaultPath'
> & {
rawMode: boolean
2026-04-26 19:37:21 +02:00
locale?: AppLocale
}) {
if (!rawMode || !activeTab) return null
return (
2026-04-27 10:12:13 +02:00
<EditorFindScope className="editor-scroll-area">
<div className="editor-content-wrapper editor-content-wrapper--raw">
<RawEditorView
key={activeTab.entry.path}
content={rawModeContent ?? activeTab.content}
path={activeTab.entry.path}
entries={entries}
findRequest={findRequest}
onContentChange={onRawContentChange ?? (() => {})}
onSave={onSave ?? (() => {})}
latestContentRef={rawLatestContentRef}
vaultPath={vaultPath}
locale={locale}
/>
</div>
</EditorFindScope>
)
}
function bindPath(cb: ((path: string) => void) | undefined, path: string) {
return cb ? () => cb(path) : undefined
}
function ActiveTabBreadcrumb({
activeTab,
barRef,
wordCount,
path,
actions,
2026-04-26 19:37:21 +02:00
locale,
}: {
activeTab: NonNullable<EditorContentModel['activeTab']>
barRef: React.RefObject<HTMLDivElement | null>
wordCount: number
path: string
actions: BreadcrumbActions
2026-04-26 19:37:21 +02:00
locale?: AppLocale
}) {
return (
<BreadcrumbBar
entry={activeTab.entry}
wordCount={wordCount}
barRef={barRef}
showDiffToggle={actions.showDiffToggle}
diffMode={actions.diffMode}
diffLoading={actions.diffLoading}
onToggleDiff={actions.onToggleDiff}
rawMode={actions.effectiveRawMode}
onToggleRaw={actions.onToggleRaw}
forceRawMode={actions.forceRawMode}
showAIChat={actions.showAIChat}
onToggleAIChat={actions.onToggleAIChat}
inspectorCollapsed={actions.inspectorCollapsed}
onToggleInspector={actions.onToggleInspector}
onToggleFavorite={bindPath(actions.onToggleFavorite, path)}
onToggleOrganized={bindPath(actions.onToggleOrganized, path)}
2026-04-27 01:45:16 +02:00
onRevealFile={actions.onRevealFile}
onCopyFilePath={actions.onCopyFilePath}
onDelete={bindPath(actions.onDeleteNote, path)}
onArchive={bindPath(actions.onArchiveNote, path)}
onUnarchive={bindPath(actions.onUnarchiveNote, path)}
onRenameFilename={actions.onRenameFilename}
2026-04-27 10:10:58 +02:00
noteLayout={actions.noteLayout}
onToggleNoteLayout={actions.onToggleNoteLayout}
2026-04-26 19:37:21 +02:00
locale={locale}
/>
)
}
function EditorChrome({
isArchived,
onUnarchiveNote,
path,
isConflicted,
onKeepMine,
onKeepTheirs,
diffMode,
diffContent,
onToggleDiff,
2026-04-26 19:37:21 +02:00
locale,
}: Pick<
EditorContentModel,
2026-04-26 19:37:21 +02:00
'isArchived' | 'onUnarchiveNote' | 'path' | 'isConflicted' | 'onKeepMine' | 'onKeepTheirs' | 'diffMode' | 'diffContent' | 'onToggleDiff' | 'locale'
>) {
return (
<>
{isArchived && onUnarchiveNote && (
2026-04-26 19:37:21 +02:00
<ArchivedNoteBanner onUnarchive={() => onUnarchiveNote(path)} locale={locale} />
)}
{isConflicted && (
<ConflictNoteBanner
onKeepMine={() => onKeepMine?.(path)}
onKeepTheirs={() => onKeepTheirs?.(path)}
2026-04-26 19:37:21 +02:00
locale={locale}
/>
)}
2026-04-26 19:37:21 +02:00
{diffMode && <DiffModeView diffContent={diffContent} locale={locale} onToggleDiff={onToggleDiff} />}
</>
)
}
function EditorCanvas({
showEditor,
cssVars,
editor,
entries,
onNavigateWikilink,
onEditorChange,
isDeletedPreview,
vaultPath,
}: Pick<
EditorContentModel,
| 'showEditor'
| 'cssVars'
| 'editor'
| 'entries'
| 'onNavigateWikilink'
| 'onEditorChange'
| 'isDeletedPreview'
| 'vaultPath'
>) {
if (!showEditor) return null
return (
2026-04-27 10:12:13 +02:00
<EditorFindScope className="editor-scroll-area" style={cssVars as React.CSSProperties}>
<div className="editor-content-wrapper">
<SingleEditorView
editor={editor}
entries={entries}
onNavigateWikilink={onNavigateWikilink}
onChange={onEditorChange}
vaultPath={vaultPath}
editable={!isDeletedPreview}
/>
</div>
2026-04-27 10:12:13 +02:00
</EditorFindScope>
)
}
function EditorFindScope({
children,
className,
style,
}: {
children: React.ReactNode
className?: string
style?: React.CSSProperties
}) {
const scopeRef = useRef<HTMLDivElement | null>(null)
const syncAvailability = useCallback(() => {
const activeElement = document.activeElement
const enabled = activeElement instanceof Node
&& scopeRef.current?.contains(activeElement) === true
dispatchEditorFindAvailability(enabled)
}, [])
useEffect(() => () => dispatchEditorFindAvailability(false), [])
return (
<div
ref={scopeRef}
className={className}
data-editor-find-scope="true"
onFocusCapture={() => dispatchEditorFindAvailability(true)}
onBlurCapture={() => requestAnimationFrame(syncAvailability)}
style={style}
>
{children}
</div>
)
}
export function EditorContentLayout(model: EditorContentModel) {
const {
activeTab,
isLoadingNewTab,
entries,
editor,
diffMode,
diffContent,
onToggleDiff,
effectiveRawMode,
onRawContentChange,
onSave,
showEditor,
isArchived,
onUnarchiveNote,
path,
isConflicted,
onKeepMine,
onKeepTheirs,
breadcrumbBarRef,
wordCount,
vaultPath,
cssVars,
onNavigateWikilink,
onEditorChange,
isDeletedPreview,
rawLatestContentRef,
rawModeContent,
2026-04-27 10:10:58 +02:00
noteLayout,
2026-04-27 10:12:13 +02:00
findRequest,
2026-04-26 19:37:21 +02:00
locale,
} = model
2026-04-26 04:41:18 +02:00
const rootClassName = cn(
'flex flex-1 flex-col min-w-0 min-h-0',
2026-04-27 10:10:58 +02:00
noteLayout === 'left' ? 'editor-content-layout--left' : 'editor-content-layout--centered',
2026-04-26 04:41:18 +02:00
)
if (!activeTab) {
return (
2026-04-26 04:41:18 +02:00
<div className={rootClassName}>
{isLoadingNewTab && showEditor && <EditorLoadingSkeleton />}
</div>
)
}
return (
2026-04-26 04:41:18 +02:00
<div className={rootClassName}>
<ActiveTabBreadcrumb
activeTab={activeTab}
barRef={breadcrumbBarRef}
wordCount={wordCount}
path={path}
2026-04-26 19:37:21 +02:00
locale={locale}
actions={{
diffMode: model.diffMode,
diffLoading: model.diffLoading,
onToggleDiff: model.onToggleDiff,
effectiveRawMode: model.effectiveRawMode,
onToggleRaw: model.onToggleRaw,
forceRawMode: model.forceRawMode,
showAIChat: model.showAIChat,
onToggleAIChat: model.onToggleAIChat,
inspectorCollapsed: model.inspectorCollapsed,
onToggleInspector: model.onToggleInspector,
showDiffToggle: model.showDiffToggle,
onToggleFavorite: model.onToggleFavorite,
onToggleOrganized: model.onToggleOrganized,
2026-04-27 01:45:16 +02:00
onRevealFile: model.onRevealFile,
onCopyFilePath: model.onCopyFilePath,
onDeleteNote: model.onDeleteNote,
onArchiveNote: model.onArchiveNote,
onUnarchiveNote: model.onUnarchiveNote,
onRenameFilename: model.onRenameFilename,
2026-04-27 10:10:58 +02:00
noteLayout: model.noteLayout,
onToggleNoteLayout: model.onToggleNoteLayout,
}}
/>
<EditorChrome
isArchived={isArchived}
onUnarchiveNote={onUnarchiveNote}
path={path}
isConflicted={isConflicted}
onKeepMine={onKeepMine}
onKeepTheirs={onKeepTheirs}
diffMode={diffMode}
diffContent={diffContent}
onToggleDiff={onToggleDiff}
2026-04-26 19:37:21 +02:00
locale={locale}
/>
<RawModeEditorSection
activeTab={activeTab}
entries={entries}
2026-04-27 10:12:13 +02:00
findRequest={findRequest}
rawMode={effectiveRawMode}
rawModeContent={rawModeContent}
onRawContentChange={onRawContentChange}
onSave={onSave}
rawLatestContentRef={rawLatestContentRef}
vaultPath={vaultPath}
2026-04-26 19:37:21 +02:00
locale={locale}
/>
<EditorCanvas
showEditor={showEditor}
cssVars={cssVars}
vaultPath={vaultPath}
editor={editor}
entries={entries}
onNavigateWikilink={onNavigateWikilink}
onEditorChange={onEditorChange}
isDeletedPreview={isDeletedPreview}
/>
{isLoadingNewTab && showEditor && <EditorLoadingSkeleton />}
</div>
)
}