fix: remove legacy title section fallback
This commit is contained in:
@@ -141,26 +141,16 @@ describe('BreadcrumbBar — title in breadcrumb (always rendered, CSS-toggled)',
|
||||
const { container } = render(<BreadcrumbBar entry={baseEntry} {...defaultProps} />)
|
||||
const bar = container.querySelector('.breadcrumb-bar')!
|
||||
expect(bar).toHaveClass('border-b', 'border-transparent')
|
||||
expect(bar).not.toHaveAttribute('data-title-hidden')
|
||||
bar.setAttribute('data-title-hidden', '')
|
||||
expect(bar).toHaveAttribute('data-title-hidden')
|
||||
})
|
||||
|
||||
it('uses the active separator state when raw mode forces the title into the breadcrumb', () => {
|
||||
it('keeps the breadcrumb title visible in raw mode', () => {
|
||||
const { container } = render(
|
||||
<BreadcrumbBar entry={baseEntry} {...defaultProps} rawMode onToggleRaw={vi.fn()} />,
|
||||
)
|
||||
|
||||
expect(container.querySelector('.breadcrumb-bar')).toHaveAttribute('data-title-hidden')
|
||||
})
|
||||
|
||||
it('keeps the breadcrumb title visible when the separate title section is absent', () => {
|
||||
const { container } = render(
|
||||
<BreadcrumbBar entry={baseEntry} {...defaultProps} showTitleSection={false} />,
|
||||
)
|
||||
|
||||
expect(container.querySelector('.breadcrumb-bar')).toHaveAttribute('data-title-hidden')
|
||||
})
|
||||
})
|
||||
|
||||
describe('BreadcrumbBar — filename controls', () => {
|
||||
|
||||
@@ -43,7 +43,6 @@ interface BreadcrumbBarProps {
|
||||
onArchive?: () => void
|
||||
onUnarchive?: () => void
|
||||
onRenameFilename?: (path: string, newFilenameStem: string) => void
|
||||
showTitleSection?: boolean
|
||||
/** Ref for direct DOM manipulation — avoids re-render on scroll. */
|
||||
barRef?: React.Ref<HTMLDivElement>
|
||||
}
|
||||
@@ -515,17 +514,13 @@ export const BreadcrumbBar = memo(function BreadcrumbBar({
|
||||
entry,
|
||||
barRef,
|
||||
onRenameFilename,
|
||||
showTitleSection = true,
|
||||
...actionProps
|
||||
}: BreadcrumbBarProps) {
|
||||
// In raw/diff mode the title section is not rendered — always show title in breadcrumb.
|
||||
// Using a prop-driven attribute avoids the timing issues of DOM mutation in useEffect.
|
||||
const titleAlwaysVisible = !showTitleSection || actionProps.rawMode || actionProps.diffMode
|
||||
return (
|
||||
<div
|
||||
ref={barRef}
|
||||
data-tauri-drag-region
|
||||
{...(titleAlwaysVisible ? { 'data-title-hidden': '' } : {})}
|
||||
data-title-hidden=""
|
||||
className="breadcrumb-bar flex shrink-0 items-center border-b border-transparent"
|
||||
style={{
|
||||
height: 52,
|
||||
|
||||
@@ -204,57 +204,6 @@
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
/* --- Title Section: wraps icon + title + separator --- */
|
||||
.title-section {
|
||||
width: 100%;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.title-section__heading {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.title-section__inline-add-icon {
|
||||
position: absolute;
|
||||
left: 8px;
|
||||
top: 0;
|
||||
z-index: 1;
|
||||
opacity: 0;
|
||||
pointer-events: none;
|
||||
transition: opacity 0.15s;
|
||||
}
|
||||
|
||||
.title-section:hover .title-section__inline-add-icon,
|
||||
.title-section__inline-add-icon:focus-within {
|
||||
opacity: 1;
|
||||
pointer-events: auto;
|
||||
}
|
||||
|
||||
.title-section__row {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: flex-start;
|
||||
gap: 10px;
|
||||
padding-top: 8px;
|
||||
margin-left: 8px;
|
||||
}
|
||||
|
||||
/* No emoji: title aligns flush left (no indent for icon area) */
|
||||
.title-section__row--no-icon {
|
||||
margin-left: 0;
|
||||
}
|
||||
|
||||
/* When emoji is present, restore top padding to the row itself */
|
||||
.title-section__row:has(.note-icon-button--active) {
|
||||
padding-top: 32px;
|
||||
}
|
||||
|
||||
.title-section__separator {
|
||||
border-bottom: 1px solid var(--border-primary, rgba(0, 0, 0, 0.08));
|
||||
margin-top: 12px;
|
||||
margin-left: 8px;
|
||||
}
|
||||
|
||||
/* --- Note Icon Area --- */
|
||||
.note-icon-area {
|
||||
display: flex;
|
||||
@@ -303,7 +252,6 @@
|
||||
transition: opacity 0.15s;
|
||||
}
|
||||
|
||||
.title-section:hover .note-icon-button--add,
|
||||
.note-icon-button--add:focus-visible {
|
||||
opacity: 1;
|
||||
}
|
||||
@@ -352,56 +300,6 @@
|
||||
color: var(--destructive, #ef4444);
|
||||
}
|
||||
|
||||
/* --- Title Field --- */
|
||||
.title-field {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.title-field__input {
|
||||
display: block;
|
||||
width: 100%;
|
||||
border: none;
|
||||
outline: none;
|
||||
background: transparent;
|
||||
font-size: var(--headings-h1-font-size, 32px);
|
||||
font-weight: var(--headings-h1-font-weight, 700);
|
||||
line-height: var(--headings-h1-line-height, 1.2);
|
||||
letter-spacing: var(--headings-h1-letter-spacing, -0.015em);
|
||||
color: var(--foreground);
|
||||
padding: 0;
|
||||
resize: none;
|
||||
overflow: hidden;
|
||||
font-family: inherit;
|
||||
field-sizing: content;
|
||||
}
|
||||
|
||||
.title-field__input::placeholder {
|
||||
color: var(--text-faint, #ccc);
|
||||
}
|
||||
|
||||
.title-field__input:disabled {
|
||||
opacity: 0.6;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.title-field__filename {
|
||||
display: block;
|
||||
font-size: 11px;
|
||||
color: var(--text-faint, #999);
|
||||
margin-top: 2px;
|
||||
}
|
||||
|
||||
/* When the legacy title UI is shown, hide the first H1 heading in BlockNote
|
||||
to avoid duplicate title display. Otherwise the editor's H1 remains visible
|
||||
and serves as the title surface. */
|
||||
.title-section[data-title-ui-visible] ~ .editor__blocknote-container [data-node-type="blockContainer"]:first-child [data-content-type="heading"][data-level="1"] {
|
||||
display: none;
|
||||
}
|
||||
.title-section[data-title-ui-visible] ~ .editor__blocknote-container [data-node-type="blockContainer"]:first-child:has([data-content-type="heading"][data-level="1"]) {
|
||||
display: none;
|
||||
}
|
||||
|
||||
/* =============================================
|
||||
Disable BlockNote/Mantine editor animations
|
||||
=============================================
|
||||
|
||||
@@ -54,8 +54,6 @@ interface EditorProps {
|
||||
onUnarchiveNote?: (path: 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
|
||||
/** Called when the user explicitly renames the filename from the breadcrumb. */
|
||||
onRenameFilename?: (path: string, newFilenameStem: string) => void
|
||||
canGoBack?: boolean
|
||||
@@ -205,7 +203,7 @@ export const Editor = memo(function Editor(props: EditorProps) {
|
||||
showAIChat, onToggleAIChat,
|
||||
vaultPath, noteList, noteListFilter,
|
||||
onToggleFavorite, onToggleOrganized, onDeleteNote, onArchiveNote, onUnarchiveNote,
|
||||
onContentChange, onSave, onTitleSync, onRenameFilename,
|
||||
onContentChange, onSave, onRenameFilename,
|
||||
onFileCreated, onFileModified, onVaultChanged,
|
||||
isConflicted, onKeepMine, onKeepTheirs,
|
||||
} = props
|
||||
@@ -256,7 +254,6 @@ export const Editor = memo(function Editor(props: EditorProps) {
|
||||
onUnarchiveNote={onUnarchiveNote}
|
||||
vaultPath={vaultPath}
|
||||
rawLatestContentRef={rawLatestContentRef}
|
||||
onTitleChange={onTitleSync}
|
||||
onRenameFilename={onRenameFilename}
|
||||
isConflicted={isConflicted}
|
||||
onKeepMine={onKeepMine}
|
||||
|
||||
@@ -1,168 +0,0 @@
|
||||
import { describe, it, expect, vi } from 'vitest'
|
||||
import { render, screen, fireEvent } from '@testing-library/react'
|
||||
import { TitleField } from './TitleField'
|
||||
|
||||
describe('TitleField', () => {
|
||||
it('renders the title in the input', () => {
|
||||
render(<TitleField title="My Note" filename="my-note.md" onTitleChange={() => {}} />)
|
||||
expect(screen.getByTestId('title-field-input')).toHaveValue('My Note')
|
||||
})
|
||||
|
||||
it('calls onTitleChange on blur with new value', () => {
|
||||
const onChange = vi.fn()
|
||||
render(<TitleField title="Old Title" filename="old-title.md" onTitleChange={onChange} />)
|
||||
const input = screen.getByTestId('title-field-input')
|
||||
fireEvent.change(input, { target: { value: 'New Title' } })
|
||||
fireEvent.blur(input)
|
||||
expect(onChange).toHaveBeenCalledWith('New Title')
|
||||
})
|
||||
|
||||
it('does not call onTitleChange if title unchanged', () => {
|
||||
const onChange = vi.fn()
|
||||
render(<TitleField title="Same Title" filename="same-title.md" onTitleChange={onChange} />)
|
||||
const input = screen.getByTestId('title-field-input')
|
||||
fireEvent.focus(input)
|
||||
fireEvent.blur(input)
|
||||
expect(onChange).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('reverts to original title if input is emptied', () => {
|
||||
const onChange = vi.fn()
|
||||
render(<TitleField title="Keep This" filename="keep-this.md" onTitleChange={onChange} />)
|
||||
const input = screen.getByTestId('title-field-input')
|
||||
fireEvent.change(input, { target: { value: '' } })
|
||||
fireEvent.blur(input)
|
||||
expect(onChange).not.toHaveBeenCalled()
|
||||
expect(input).toHaveValue('Keep This')
|
||||
})
|
||||
|
||||
it('shows filename indicator when slug differs and title is focused', () => {
|
||||
render(<TitleField title="My Note" filename="wrong-name.md" onTitleChange={() => {}} />)
|
||||
// Not shown when unfocused
|
||||
expect(screen.queryByTestId('title-field-filename')).not.toBeInTheDocument()
|
||||
// Shown when focused
|
||||
fireEvent.focus(screen.getByTestId('title-field-input'))
|
||||
expect(screen.getByTestId('title-field-filename')).toHaveTextContent('my-note.md')
|
||||
})
|
||||
|
||||
it('does not show filename when slug matches and not editing', () => {
|
||||
render(<TitleField title="My Note" filename="my-note.md" onTitleChange={() => {}} />)
|
||||
expect(screen.queryByTestId('title-field-filename')).not.toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('disables input when editable is false', () => {
|
||||
render(<TitleField title="Read Only" filename="read-only.md" editable={false} onTitleChange={() => {}} />)
|
||||
expect(screen.getByTestId('title-field-input')).toBeDisabled()
|
||||
})
|
||||
|
||||
it('commits title on Enter key', () => {
|
||||
const onChange = vi.fn()
|
||||
render(<TitleField title="Before" filename="before.md" onTitleChange={onChange} />)
|
||||
const input = screen.getByTestId('title-field-input')
|
||||
fireEvent.change(input, { target: { value: 'After' } })
|
||||
fireEvent.keyDown(input, { key: 'Enter' })
|
||||
// In jsdom, blur() after keyDown needs explicit blur event
|
||||
fireEvent.blur(input)
|
||||
expect(onChange).toHaveBeenCalledWith('After')
|
||||
})
|
||||
|
||||
it('reverts on Escape key', () => {
|
||||
const onChange = vi.fn()
|
||||
render(<TitleField title="Original" filename="original.md" onTitleChange={onChange} />)
|
||||
const input = screen.getByTestId('title-field-input')
|
||||
fireEvent.change(input, { target: { value: 'Changed' } })
|
||||
fireEvent.keyDown(input, { key: 'Escape' })
|
||||
// Escape reverts value and blurs
|
||||
expect(input).toHaveValue('Original')
|
||||
})
|
||||
|
||||
it('shows new title optimistically after commit (before prop updates)', () => {
|
||||
const onChange = vi.fn()
|
||||
const { rerender } = render(<TitleField title="Old Title" filename="old-title.md" onTitleChange={onChange} />)
|
||||
const input = screen.getByTestId('title-field-input')
|
||||
fireEvent.focus(input)
|
||||
fireEvent.change(input, { target: { value: 'New Title' } })
|
||||
fireEvent.blur(input)
|
||||
// After commit, should show new title even though prop is still "Old Title"
|
||||
expect(input).toHaveValue('New Title')
|
||||
// After prop updates to match, should still show new title
|
||||
rerender(<TitleField title="New Title" filename="new-title.md" onTitleChange={onChange} />)
|
||||
expect(input).toHaveValue('New Title')
|
||||
})
|
||||
|
||||
it('resets optimistic title when prop changes from external source', () => {
|
||||
const onChange = vi.fn()
|
||||
const { rerender } = render(<TitleField title="Title A" filename="title-a.md" onTitleChange={onChange} />)
|
||||
const input = screen.getByTestId('title-field-input')
|
||||
// Simulate external title change (e.g., tab switch)
|
||||
rerender(<TitleField title="Title B" filename="title-b.md" onTitleChange={onChange} />)
|
||||
expect(input).toHaveValue('Title B')
|
||||
})
|
||||
|
||||
it('responds to laputa:focus-editor event with selectTitle', () => {
|
||||
render(<TitleField title="Focus Me" filename="focus-me.md" onTitleChange={() => {}} />)
|
||||
const input = screen.getByTestId('title-field-input')
|
||||
window.dispatchEvent(new CustomEvent('laputa:focus-editor', { detail: { selectTitle: true } }))
|
||||
expect(document.activeElement).toBe(input)
|
||||
})
|
||||
|
||||
it('resets stale localValue when title prop changes after focus', () => {
|
||||
// Regression: creating a new note fires focus-editor before React re-renders,
|
||||
// so handleFocus captures the OLD note's title into localValue.
|
||||
// When React re-renders with the new title, localValue should be cleared.
|
||||
const onChange = vi.fn()
|
||||
const { rerender } = render(<TitleField title="Old Note" filename="old-note.md" onTitleChange={onChange} />)
|
||||
const input = screen.getByTestId('title-field-input')
|
||||
// Simulate: focus fires while title prop is still "Old Note"
|
||||
fireEvent.focus(input)
|
||||
expect(input).toHaveValue('Old Note')
|
||||
// React re-renders with new note's title (tab switched)
|
||||
rerender(<TitleField title="Untitled note" filename="untitled-note.md" onTitleChange={onChange} />)
|
||||
expect(input).toHaveValue('Untitled note')
|
||||
})
|
||||
|
||||
it('shows vault-relative path (without .md) only when title is focused', () => {
|
||||
render(<TitleField title="ADR" filename="0001-tauri-stack.md" notePath="/Users/luca/Laputa/docs/adr/0001-tauri-stack.md" vaultPath="/Users/luca/Laputa" onTitleChange={() => {}} />)
|
||||
// Path hidden by default
|
||||
expect(screen.queryByTestId('title-field-path')).not.toBeInTheDocument()
|
||||
// Focus title → path appears
|
||||
fireEvent.focus(screen.getByTestId('title-field-input'))
|
||||
expect(screen.getByTestId('title-field-path')).toHaveTextContent('docs/adr/0001-tauri-stack')
|
||||
// No bare filename shown when path is visible
|
||||
expect(screen.queryByTestId('title-field-filename')).not.toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('hides path on blur', () => {
|
||||
render(<TitleField title="ADR" filename="0001-tauri-stack.md" notePath="/Users/luca/Laputa/docs/adr/0001-tauri-stack.md" vaultPath="/Users/luca/Laputa" onTitleChange={() => {}} />)
|
||||
const input = screen.getByTestId('title-field-input')
|
||||
fireEvent.focus(input)
|
||||
expect(screen.getByTestId('title-field-path')).toBeInTheDocument()
|
||||
fireEvent.blur(input)
|
||||
expect(screen.queryByTestId('title-field-path')).not.toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('hides path for notes at vault root even when focused', () => {
|
||||
render(<TitleField title="Root Note" filename="root-note.md" notePath="/Users/luca/Laputa/root-note.md" vaultPath="/Users/luca/Laputa" onTitleChange={() => {}} />)
|
||||
fireEvent.focus(screen.getByTestId('title-field-input'))
|
||||
expect(screen.queryByTestId('title-field-path')).not.toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('hides path when vaultPath is not provided', () => {
|
||||
render(<TitleField title="Note" filename="note.md" onTitleChange={() => {}} />)
|
||||
fireEvent.focus(screen.getByTestId('title-field-input'))
|
||||
expect(screen.queryByTestId('title-field-path')).not.toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('resolves vault-relative path when paths differ by symlink prefix', () => {
|
||||
// vaultPath uses symlink /Users/luca/... but notePath is canonical /Volumes/Jupiter/...
|
||||
render(<TitleField title="ADR" filename="0001-tauri-stack.md" notePath="/Volumes/Jupiter/Workspace/laputa-app/demo-vault-v2/docs/adr/0001-tauri-stack.md" vaultPath="/Users/luca/Workspace/laputa-app/demo-vault-v2" onTitleChange={() => {}} />)
|
||||
fireEvent.focus(screen.getByTestId('title-field-input'))
|
||||
expect(screen.getByTestId('title-field-path')).toHaveTextContent('docs/adr/0001-tauri-stack')
|
||||
})
|
||||
|
||||
it('handles vaultPath with trailing slash', () => {
|
||||
render(<TitleField title="ADR" filename="0001-tauri-stack.md" notePath="/Users/luca/Laputa/docs/adr/0001-tauri-stack.md" vaultPath="/Users/luca/Laputa/" onTitleChange={() => {}} />)
|
||||
fireEvent.focus(screen.getByTestId('title-field-input'))
|
||||
expect(screen.getByTestId('title-field-path')).toHaveTextContent('docs/adr/0001-tauri-stack')
|
||||
})
|
||||
})
|
||||
@@ -1,170 +0,0 @@
|
||||
import { useState, useCallback, useRef, useEffect } from 'react'
|
||||
import { slugify } from '../hooks/useNoteCreation'
|
||||
|
||||
interface TitleFieldProps {
|
||||
title: string
|
||||
filename: string
|
||||
editable?: boolean
|
||||
/** Absolute path of the note file. */
|
||||
notePath?: string
|
||||
/** Absolute path of the vault root. */
|
||||
vaultPath?: string
|
||||
/** Called when the user finishes editing the title (blur or Enter). */
|
||||
onTitleChange: (newTitle: string) => void
|
||||
}
|
||||
|
||||
/** Manages local edit + optimistic title state for TitleField. */
|
||||
function useOptimisticTitle(title: string, onTitleChange: (t: string) => void) {
|
||||
const [localValue, setLocalValue] = useState<string | null>(null)
|
||||
// [optimisticTitle, forPropTitle]: shown after commit until title prop catches up
|
||||
const [optimistic, setOptimistic] = useState<[string, string] | null>(null)
|
||||
const isFocusedRef = useRef(false)
|
||||
const [prevTitle, setPrevTitle] = useState(title)
|
||||
|
||||
// Reset local edit when the title prop changes (e.g. note switch).
|
||||
// This prevents a stale handleFocus closure from locking in the old note's title
|
||||
// when focus-editor fires before React re-renders with the new tab.
|
||||
if (prevTitle !== title) {
|
||||
setPrevTitle(title)
|
||||
if (localValue !== null) setLocalValue(null)
|
||||
}
|
||||
|
||||
// Clear optimistic once the prop changes (rename completed or tab switched)
|
||||
const optimisticValue = optimistic && optimistic[1] === title ? optimistic[0] : null
|
||||
const value = localValue ?? optimisticValue ?? title
|
||||
const isEditing = localValue !== null || optimisticValue !== null
|
||||
|
||||
const handleFocus = useCallback(() => {
|
||||
isFocusedRef.current = true
|
||||
setLocalValue(title)
|
||||
}, [title])
|
||||
|
||||
const commitTitle = useCallback(() => {
|
||||
isFocusedRef.current = false
|
||||
const trimmed = (localValue ?? '').trim()
|
||||
if (trimmed && trimmed !== title) {
|
||||
setLocalValue(null)
|
||||
setOptimistic([trimmed, title])
|
||||
onTitleChange(trimmed)
|
||||
} else {
|
||||
setLocalValue(null)
|
||||
}
|
||||
}, [localValue, title, onTitleChange])
|
||||
|
||||
const revert = useCallback(() => setLocalValue(null), [])
|
||||
const setEdit = useCallback((v: string) => setLocalValue(v), [])
|
||||
|
||||
return { value, isEditing, handleFocus, commitTitle, revert, setEdit }
|
||||
}
|
||||
|
||||
/**
|
||||
* Dedicated title input field above the editor.
|
||||
* Displays the title as an editable field and shows the resulting filename below.
|
||||
*/
|
||||
export function TitleField({ title, filename, editable = true, notePath, vaultPath, onTitleChange }: TitleFieldProps) {
|
||||
const inputRef = useRef<HTMLTextAreaElement>(null)
|
||||
const [isFocused, setIsFocused] = useState(false)
|
||||
const { value, isEditing, handleFocus, commitTitle, revert, setEdit } =
|
||||
useOptimisticTitle(title, onTitleChange)
|
||||
|
||||
// Auto-resize textarea to fit content (fallback for browsers without field-sizing: content)
|
||||
const autoResize = useCallback(() => {
|
||||
const el = inputRef.current
|
||||
if (!el) return
|
||||
el.style.height = 'auto'
|
||||
el.style.height = el.scrollHeight + 'px'
|
||||
}, [])
|
||||
|
||||
useEffect(() => {
|
||||
autoResize()
|
||||
// Schedule a second measurement after the browser has painted, to catch
|
||||
// cases where scrollHeight is stale on the first read (e.g. tab switch).
|
||||
const id = requestAnimationFrame(autoResize)
|
||||
return () => cancelAnimationFrame(id)
|
||||
}, [value, autoResize])
|
||||
|
||||
// Re-measure when container width changes (text may re-wrap)
|
||||
useEffect(() => {
|
||||
const el = inputRef.current
|
||||
if (!el) return
|
||||
const ro = new ResizeObserver(autoResize)
|
||||
ro.observe(el)
|
||||
return () => ro.disconnect()
|
||||
}, [autoResize])
|
||||
|
||||
useEffect(() => {
|
||||
const handler = (e: Event) => {
|
||||
const detail = (e as CustomEvent).detail
|
||||
if (detail?.selectTitle && inputRef.current) {
|
||||
inputRef.current.focus()
|
||||
inputRef.current.select()
|
||||
}
|
||||
}
|
||||
window.addEventListener('laputa:focus-editor', handler)
|
||||
return () => window.removeEventListener('laputa:focus-editor', handler)
|
||||
}, [])
|
||||
|
||||
const handleKeyDown = useCallback((e: React.KeyboardEvent) => {
|
||||
if (e.key === 'Enter') {
|
||||
e.preventDefault()
|
||||
inputRef.current?.blur()
|
||||
}
|
||||
if (e.key === 'Escape') {
|
||||
revert()
|
||||
inputRef.current?.blur()
|
||||
}
|
||||
}, [revert])
|
||||
|
||||
const expectedSlug = slugify(value.trim() || title)
|
||||
const currentStem = filename.replace(/\.md$/, '')
|
||||
|
||||
// Compute vault-relative path (only for notes in subdirectories)
|
||||
const relativePath = (() => {
|
||||
if (!notePath || !vaultPath) return null
|
||||
const vp = vaultPath.replace(/\/+$/, '')
|
||||
const np = notePath.replace(/\.md$/, '')
|
||||
if (np.startsWith(vp + '/')) return np.slice(vp.length + 1)
|
||||
// Fallback: match by vault directory name for symlink-resolved paths
|
||||
const vaultName = vp.split('/').pop()
|
||||
if (!vaultName) return null
|
||||
const segments = np.split('/')
|
||||
const idx = segments.lastIndexOf(vaultName)
|
||||
if (idx >= 0) return segments.slice(idx + 1).join('/')
|
||||
return null
|
||||
})()
|
||||
const isSubdirectory = relativePath != null && relativePath.includes('/')
|
||||
|
||||
// Show path only when title is focused and note is in a subdirectory
|
||||
const showRelativePath = isFocused && isSubdirectory
|
||||
// Show filename hint when slug differs, but only when focused (not always)
|
||||
const showFilename = isFocused && !showRelativePath && (isEditing || currentStem !== expectedSlug)
|
||||
|
||||
return (
|
||||
<div className="title-field" data-testid="title-field">
|
||||
<textarea
|
||||
ref={inputRef}
|
||||
className="title-field__input"
|
||||
value={value}
|
||||
rows={1}
|
||||
onChange={e => { setEdit(e.target.value); autoResize() }}
|
||||
onFocus={() => { setIsFocused(true); handleFocus() }}
|
||||
onBlur={() => { setIsFocused(false); commitTitle() }}
|
||||
onKeyDown={handleKeyDown}
|
||||
disabled={!editable}
|
||||
placeholder="Untitled"
|
||||
spellCheck={false}
|
||||
data-testid="title-field-input"
|
||||
/>
|
||||
{showFilename && (
|
||||
<span className="title-field__filename" data-testid="title-field-filename">
|
||||
{expectedSlug}.md
|
||||
</span>
|
||||
)}
|
||||
{showRelativePath && (
|
||||
<span className="title-field__path" data-testid="title-field-path" style={{ display: 'block', fontSize: 11, color: 'var(--muted-foreground)', marginTop: 2 }}>
|
||||
{relativePath}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -7,18 +7,6 @@ vi.mock('../BreadcrumbBar', () => ({
|
||||
BreadcrumbBar: () => <div data-testid="breadcrumb-bar" />,
|
||||
}))
|
||||
|
||||
vi.mock('../TitleField', () => ({
|
||||
TitleField: () => <div data-testid="title-field-input" />,
|
||||
}))
|
||||
|
||||
vi.mock('../NoteIcon', () => ({
|
||||
NoteIcon: ({ icon }: { icon: string | null }) => (
|
||||
icon
|
||||
? <button type="button" data-testid="note-icon-display" />
|
||||
: <button type="button" data-testid="note-icon-add" />
|
||||
),
|
||||
}))
|
||||
|
||||
vi.mock('../ArchivedNoteBanner', () => ({
|
||||
ArchivedNoteBanner: () => <div data-testid="archived-banner" />,
|
||||
}))
|
||||
@@ -69,12 +57,7 @@ function createModel(overrides: Record<string, unknown> = {}) {
|
||||
onKeepTheirs: vi.fn(),
|
||||
breadcrumbBarRef: createRef<HTMLDivElement>(),
|
||||
wordCount: 12,
|
||||
titleSectionRef: createRef<HTMLDivElement>(),
|
||||
showTitleSection: true,
|
||||
hasDisplayIcon: false,
|
||||
entryIcon: null,
|
||||
vaultPath: '/vault',
|
||||
onTitleChange: vi.fn(),
|
||||
cssVars: {},
|
||||
onNavigateWikilink: vi.fn(),
|
||||
onEditorChange: vi.fn(),
|
||||
@@ -95,27 +78,12 @@ function createModel(overrides: Record<string, unknown> = {}) {
|
||||
}
|
||||
|
||||
describe('EditorContentLayout', () => {
|
||||
it('does not render a standalone add-icon row when the note has no icon', () => {
|
||||
it('never renders the legacy title section', () => {
|
||||
const { container } = render(<EditorContentLayout {...createModel()} />)
|
||||
|
||||
expect(container.querySelector('.title-section__add-icon')).toBeNull()
|
||||
expect(container.querySelector('.title-section__inline-add-icon')).not.toBeNull()
|
||||
expect(screen.getByTestId('note-icon-add')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('keeps the existing icon and title inside the same title row', () => {
|
||||
const { container } = render(
|
||||
<EditorContentLayout
|
||||
{...createModel({
|
||||
hasDisplayIcon: true,
|
||||
entryIcon: 'rocket',
|
||||
})}
|
||||
/>,
|
||||
)
|
||||
|
||||
const titleRow = container.querySelector('.title-section__row')
|
||||
expect(titleRow?.querySelector('[data-testid="note-icon-display"]')).not.toBeNull()
|
||||
expect(titleRow?.querySelector('[data-testid="title-field-input"]')).not.toBeNull()
|
||||
expect(container.querySelector('.title-section')).toBeNull()
|
||||
expect(screen.queryByTestId('title-field-input')).not.toBeInTheDocument()
|
||||
expect(screen.getByTestId('single-editor-view')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('shows the loading skeleton instead of stale editor chrome while switching tabs', () => {
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
import type React from 'react'
|
||||
import { DiffView } from '../DiffView'
|
||||
import { BreadcrumbBar } from '../BreadcrumbBar'
|
||||
import { TitleField } from '../TitleField'
|
||||
import { NoteIcon } from '../NoteIcon'
|
||||
import { ArchivedNoteBanner } from '../ArchivedNoteBanner'
|
||||
import { ConflictNoteBanner } from '../ConflictNoteBanner'
|
||||
import { RawEditorView } from '../RawEditorView'
|
||||
@@ -99,14 +97,12 @@ function ActiveTabBreadcrumb({
|
||||
barRef,
|
||||
wordCount,
|
||||
path,
|
||||
showTitleSection,
|
||||
actions,
|
||||
}: {
|
||||
activeTab: NonNullable<EditorContentModel['activeTab']>
|
||||
barRef: React.RefObject<HTMLDivElement | null>
|
||||
wordCount: number
|
||||
path: string
|
||||
showTitleSection: boolean
|
||||
actions: BreadcrumbActions
|
||||
}) {
|
||||
return (
|
||||
@@ -114,7 +110,6 @@ function ActiveTabBreadcrumb({
|
||||
entry={activeTab.entry}
|
||||
wordCount={wordCount}
|
||||
barRef={barRef}
|
||||
showTitleSection={showTitleSection}
|
||||
showDiffToggle={actions.showDiffToggle}
|
||||
diffMode={actions.diffMode}
|
||||
diffLoading={actions.diffLoading}
|
||||
@@ -136,50 +131,6 @@ function ActiveTabBreadcrumb({
|
||||
)
|
||||
}
|
||||
|
||||
function TitleSection({
|
||||
activeTab,
|
||||
entryIcon,
|
||||
hasDisplayIcon,
|
||||
path,
|
||||
showTitleSection,
|
||||
titleSectionRef,
|
||||
vaultPath,
|
||||
onTitleChange,
|
||||
}: Pick<
|
||||
EditorContentModel,
|
||||
'activeTab' | 'entryIcon' | 'hasDisplayIcon' | 'path' | 'showTitleSection' | 'titleSectionRef' | 'vaultPath' | 'onTitleChange'
|
||||
>) {
|
||||
if (!activeTab) return null
|
||||
|
||||
return (
|
||||
<div ref={titleSectionRef} className="title-section" data-title-ui-visible={showTitleSection || undefined}>
|
||||
{showTitleSection && (
|
||||
<>
|
||||
<div className="title-section__heading">
|
||||
{!hasDisplayIcon && (
|
||||
<div className="title-section__inline-add-icon">
|
||||
<NoteIcon icon={null} editable />
|
||||
</div>
|
||||
)}
|
||||
<div className={`title-section__row${hasDisplayIcon ? '' : ' title-section__row--no-icon'}`}>
|
||||
{hasDisplayIcon && <NoteIcon icon={entryIcon} editable />}
|
||||
<TitleField
|
||||
title={activeTab.entry.title}
|
||||
filename={activeTab.entry.filename}
|
||||
editable
|
||||
notePath={path}
|
||||
vaultPath={vaultPath}
|
||||
onTitleChange={(newTitle) => onTitleChange?.(path, newTitle)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="title-section__separator" />
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function EditorChrome({
|
||||
isArchived,
|
||||
onUnarchiveNote,
|
||||
@@ -213,52 +164,28 @@ function EditorChrome({
|
||||
function EditorCanvas({
|
||||
showEditor,
|
||||
cssVars,
|
||||
activeTab,
|
||||
entryIcon,
|
||||
hasDisplayIcon,
|
||||
path,
|
||||
showTitleSection,
|
||||
titleSectionRef,
|
||||
vaultPath,
|
||||
onTitleChange,
|
||||
editor,
|
||||
entries,
|
||||
onNavigateWikilink,
|
||||
onEditorChange,
|
||||
isDeletedPreview,
|
||||
vaultPath,
|
||||
}: Pick<
|
||||
EditorContentModel,
|
||||
| 'showEditor'
|
||||
| 'cssVars'
|
||||
| 'activeTab'
|
||||
| 'entryIcon'
|
||||
| 'hasDisplayIcon'
|
||||
| 'path'
|
||||
| 'showTitleSection'
|
||||
| 'titleSectionRef'
|
||||
| 'vaultPath'
|
||||
| 'onTitleChange'
|
||||
| 'editor'
|
||||
| 'entries'
|
||||
| 'onNavigateWikilink'
|
||||
| 'onEditorChange'
|
||||
| 'isDeletedPreview'
|
||||
| 'vaultPath'
|
||||
>) {
|
||||
if (!showEditor) return null
|
||||
|
||||
return (
|
||||
<div className="editor-scroll-area" style={cssVars as React.CSSProperties}>
|
||||
<div className="editor-content-wrapper">
|
||||
<TitleSection
|
||||
activeTab={activeTab}
|
||||
entryIcon={entryIcon}
|
||||
hasDisplayIcon={hasDisplayIcon}
|
||||
path={path}
|
||||
showTitleSection={showTitleSection}
|
||||
titleSectionRef={titleSectionRef}
|
||||
vaultPath={vaultPath}
|
||||
onTitleChange={onTitleChange}
|
||||
/>
|
||||
<SingleEditorView
|
||||
editor={editor}
|
||||
entries={entries}
|
||||
@@ -293,12 +220,7 @@ export function EditorContentLayout(model: EditorContentModel) {
|
||||
onKeepTheirs,
|
||||
breadcrumbBarRef,
|
||||
wordCount,
|
||||
titleSectionRef,
|
||||
showTitleSection,
|
||||
hasDisplayIcon,
|
||||
entryIcon,
|
||||
vaultPath,
|
||||
onTitleChange,
|
||||
cssVars,
|
||||
onNavigateWikilink,
|
||||
onEditorChange,
|
||||
@@ -321,7 +243,6 @@ export function EditorContentLayout(model: EditorContentModel) {
|
||||
barRef={breadcrumbBarRef}
|
||||
wordCount={wordCount}
|
||||
path={path}
|
||||
showTitleSection={showTitleSection}
|
||||
actions={{
|
||||
diffMode: model.diffMode,
|
||||
diffLoading: model.diffLoading,
|
||||
@@ -365,14 +286,7 @@ export function EditorContentLayout(model: EditorContentModel) {
|
||||
<EditorCanvas
|
||||
showEditor={showEditor}
|
||||
cssVars={cssVars}
|
||||
activeTab={activeTab}
|
||||
entryIcon={entryIcon}
|
||||
hasDisplayIcon={hasDisplayIcon}
|
||||
path={path}
|
||||
showTitleSection={showTitleSection}
|
||||
titleSectionRef={titleSectionRef}
|
||||
vaultPath={vaultPath}
|
||||
onTitleChange={onTitleChange}
|
||||
editor={editor}
|
||||
entries={entries}
|
||||
onNavigateWikilink={onNavigateWikilink}
|
||||
|
||||
@@ -46,47 +46,47 @@ function deriveState(tab: EditorContentTab | null, overrides?: Partial<VaultEntr
|
||||
}
|
||||
|
||||
describe('deriveEditorContentState', () => {
|
||||
it('hides the legacy title section when loaded content contains a top-level H1', () => {
|
||||
it('marks loaded content with a top-level H1 as titled', () => {
|
||||
const state = deriveState({
|
||||
entry: baseEntry,
|
||||
content: '---\ntitle: Legacy Project\n---\n# Legacy Project\n\nBody',
|
||||
})
|
||||
|
||||
expect(state.hasH1).toBe(true)
|
||||
expect(state.showTitleSection).toBe(false)
|
||||
expect(state.showEditor).toBe(true)
|
||||
})
|
||||
|
||||
it('keeps the title section for notes without an H1', () => {
|
||||
it('keeps editor content visible for notes without an H1', () => {
|
||||
const state = deriveState({
|
||||
entry: baseEntry,
|
||||
content: '---\ntitle: Legacy Project\n---\nBody without a heading',
|
||||
})
|
||||
|
||||
expect(state.hasH1).toBe(false)
|
||||
expect(state.showTitleSection).toBe(false)
|
||||
expect(state.showEditor).toBe(true)
|
||||
})
|
||||
|
||||
it('hides the legacy title section when a frontmatter title drives the display title', () => {
|
||||
it('keeps editor content visible when a legacy frontmatter title exists', () => {
|
||||
const state = deriveState({
|
||||
entry: baseEntry,
|
||||
content: '---\ntitle: Spring 2026\nstatus: Active\n---\n## Goals',
|
||||
})
|
||||
|
||||
expect(state.hasH1).toBe(false)
|
||||
expect(state.showTitleSection).toBe(false)
|
||||
expect(state.showEditor).toBe(true)
|
||||
})
|
||||
|
||||
it('keeps the title section when the document title still comes from the filename', () => {
|
||||
it('does not fall back to a separate title section when the filename drives the display title', () => {
|
||||
const state = deriveState({
|
||||
entry: baseEntry,
|
||||
content: '---\nstatus: Active\n---\nBody without a heading',
|
||||
})
|
||||
|
||||
expect(state.hasH1).toBe(false)
|
||||
expect(state.showTitleSection).toBe(true)
|
||||
expect(state.showEditor).toBe(true)
|
||||
})
|
||||
|
||||
it('hides the title section for untitled drafts before they get an H1', () => {
|
||||
it('keeps untitled drafts in the editor even before they get an H1', () => {
|
||||
const draftEntry = {
|
||||
...baseEntry,
|
||||
path: '/vault/untitled-note-1700000000.md',
|
||||
@@ -105,6 +105,6 @@ describe('deriveEditorContentState', () => {
|
||||
})
|
||||
|
||||
expect(state.hasH1).toBe(false)
|
||||
expect(state.showTitleSection).toBe(false)
|
||||
expect(state.showEditor).toBe(true)
|
||||
})
|
||||
})
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import type { NoteStatus, VaultEntry } from '../../types'
|
||||
import { contentDefinesDisplayTitle, extractH1TitleFromContent } from '../../utils/noteTitle'
|
||||
import { extractH1TitleFromContent } from '../../utils/noteTitle'
|
||||
import { countWords } from '../../utils/wikilinks'
|
||||
|
||||
export interface EditorContentTab {
|
||||
@@ -14,17 +14,11 @@ interface EditorContentStateInput {
|
||||
activeStatus: NoteStatus
|
||||
}
|
||||
|
||||
interface TitleSectionState {
|
||||
hasDisplayTitle: boolean
|
||||
hasH1: boolean
|
||||
}
|
||||
|
||||
interface VisibilityState {
|
||||
effectiveRawMode: boolean
|
||||
isDeletedPreview: boolean
|
||||
isNonMarkdownText: boolean
|
||||
showEditor: boolean
|
||||
showTitleSection: boolean
|
||||
}
|
||||
|
||||
export interface EditorContentState {
|
||||
@@ -35,7 +29,6 @@ export interface EditorContentState {
|
||||
isNonMarkdownText: boolean
|
||||
effectiveRawMode: boolean
|
||||
showEditor: boolean
|
||||
showTitleSection: boolean
|
||||
path: string
|
||||
wordCount: number
|
||||
}
|
||||
@@ -49,38 +42,18 @@ function contentHasTopLevelH1(activeTab: EditorContentTab | null): boolean {
|
||||
return activeTab ? extractH1TitleFromContent(activeTab.content) !== null : false
|
||||
}
|
||||
|
||||
function contentDefinesTitle(activeTab: EditorContentTab | null): boolean {
|
||||
return activeTab ? contentDefinesDisplayTitle(activeTab.content) : false
|
||||
}
|
||||
|
||||
function resolveHasH1(activeTab: EditorContentTab | null, freshEntry: VaultEntry | undefined): boolean {
|
||||
return contentHasTopLevelH1(activeTab) || freshEntry?.hasH1 === true || activeTab?.entry.hasH1 === true
|
||||
}
|
||||
|
||||
function resolveHasDisplayTitle(activeTab: EditorContentTab | null, hasH1: boolean): boolean {
|
||||
return hasH1 || contentDefinesTitle(activeTab)
|
||||
}
|
||||
|
||||
function deriveTitleSectionState(activeTab: EditorContentTab | null, freshEntry: VaultEntry | undefined): TitleSectionState {
|
||||
const hasH1 = resolveHasH1(activeTab, freshEntry)
|
||||
return {
|
||||
hasDisplayTitle: resolveHasDisplayTitle(activeTab, hasH1),
|
||||
hasH1,
|
||||
}
|
||||
}
|
||||
|
||||
function deriveVisibilityState(input: {
|
||||
activeStatus: NoteStatus
|
||||
activeTab: EditorContentTab | null
|
||||
freshEntry: VaultEntry | undefined
|
||||
hasDisplayTitle: boolean
|
||||
rawMode: boolean
|
||||
}): VisibilityState {
|
||||
const {
|
||||
activeStatus,
|
||||
activeTab,
|
||||
freshEntry,
|
||||
hasDisplayTitle,
|
||||
rawMode,
|
||||
} = input
|
||||
const isDeletedPreview = !!activeTab && !freshEntry
|
||||
@@ -92,36 +65,23 @@ function deriveVisibilityState(input: {
|
||||
isNonMarkdownText,
|
||||
effectiveRawMode,
|
||||
showEditor: !effectiveRawMode,
|
||||
showTitleSection: !isDeletedPreview && !hasDisplayTitle && !isUnsavedUntitledDraft(activeTab, activeStatus),
|
||||
}
|
||||
}
|
||||
|
||||
function isUnsavedUntitledDraft(activeTab: EditorContentTab | null, activeStatus: NoteStatus): boolean {
|
||||
if (!activeTab) return false
|
||||
if (!activeTab.entry.filename.startsWith('untitled-')) return false
|
||||
return activeStatus === 'new' || activeStatus === 'unsaved' || activeStatus === 'pendingSave'
|
||||
}
|
||||
|
||||
export function deriveEditorContentState({
|
||||
activeTab,
|
||||
entries,
|
||||
rawMode,
|
||||
activeStatus,
|
||||
}: EditorContentStateInput): EditorContentState {
|
||||
export function deriveEditorContentState(input: EditorContentStateInput): EditorContentState {
|
||||
const { activeTab, entries, rawMode } = input
|
||||
const freshEntry = findFreshEntry(activeTab, entries)
|
||||
const titleState = deriveTitleSectionState(activeTab, freshEntry)
|
||||
const hasH1 = resolveHasH1(activeTab, freshEntry)
|
||||
const visibilityState = deriveVisibilityState({
|
||||
activeStatus,
|
||||
activeTab,
|
||||
freshEntry,
|
||||
hasDisplayTitle: titleState.hasDisplayTitle,
|
||||
rawMode,
|
||||
})
|
||||
|
||||
return {
|
||||
freshEntry,
|
||||
isArchived: freshEntry?.archived ?? activeTab?.entry.archived ?? false,
|
||||
hasH1: titleState.hasH1,
|
||||
hasH1,
|
||||
...visibilityState,
|
||||
path: activeTab?.entry.path ?? '',
|
||||
wordCount: activeTab ? countWords(activeTab.content) : 0,
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
import type React from 'react'
|
||||
import { useEffect, useRef } from 'react'
|
||||
import { useRef } from 'react'
|
||||
import type { useCreateBlockNote } from '@blocknote/react'
|
||||
import type { NoteStatus, VaultEntry } from '../../types'
|
||||
import { useEditorTheme } from '../../hooks/useTheme'
|
||||
import { resolveNoteIcon } from '../../utils/noteIcon'
|
||||
import { deriveEditorContentState } from './editorContentState'
|
||||
|
||||
export interface Tab {
|
||||
@@ -39,61 +38,17 @@ export interface EditorContentProps {
|
||||
onUnarchiveNote?: (path: string) => void
|
||||
vaultPath?: string
|
||||
rawLatestContentRef?: React.MutableRefObject<string | null>
|
||||
onTitleChange?: (path: string, newTitle: string) => void
|
||||
onRenameFilename?: (path: string, newFilenameStem: string) => void
|
||||
isConflicted?: boolean
|
||||
onKeepMine?: (path: string) => void
|
||||
onKeepTheirs?: (path: string) => void
|
||||
}
|
||||
|
||||
function useBreadcrumbTitleVisibility({
|
||||
showEditor,
|
||||
showTitleSection,
|
||||
path,
|
||||
breadcrumbBarRef,
|
||||
titleSectionRef,
|
||||
}: {
|
||||
showEditor: boolean
|
||||
showTitleSection: boolean
|
||||
path: string
|
||||
breadcrumbBarRef: React.RefObject<HTMLDivElement | null>
|
||||
titleSectionRef: React.RefObject<HTMLDivElement | null>
|
||||
}) {
|
||||
useEffect(() => {
|
||||
if (!showEditor) return
|
||||
|
||||
const bar = breadcrumbBarRef.current
|
||||
const titleSection = titleSectionRef.current
|
||||
if (!bar || !titleSection) return
|
||||
|
||||
if (!showTitleSection) {
|
||||
bar.setAttribute('data-title-hidden', '')
|
||||
return () => {
|
||||
bar.removeAttribute('data-title-hidden')
|
||||
}
|
||||
}
|
||||
|
||||
const observer = new IntersectionObserver(
|
||||
([entry]) => {
|
||||
if (entry.isIntersecting) bar.removeAttribute('data-title-hidden')
|
||||
else bar.setAttribute('data-title-hidden', '')
|
||||
},
|
||||
{ threshold: 0 },
|
||||
)
|
||||
observer.observe(titleSection)
|
||||
return () => {
|
||||
observer.disconnect()
|
||||
bar.removeAttribute('data-title-hidden')
|
||||
}
|
||||
}, [path, showEditor, showTitleSection, breadcrumbBarRef, titleSectionRef])
|
||||
}
|
||||
|
||||
export function useEditorContentModel(props: EditorContentProps) {
|
||||
const {
|
||||
activeTab,
|
||||
entries,
|
||||
rawMode,
|
||||
activeStatus,
|
||||
diffMode,
|
||||
} = props
|
||||
|
||||
@@ -105,29 +60,17 @@ export function useEditorContentModel(props: EditorContentProps) {
|
||||
effectiveRawMode,
|
||||
showEditor: showContentEditor,
|
||||
path,
|
||||
showTitleSection,
|
||||
wordCount,
|
||||
} = deriveEditorContentState({
|
||||
activeTab,
|
||||
entries,
|
||||
rawMode,
|
||||
activeStatus,
|
||||
activeStatus: props.activeStatus,
|
||||
})
|
||||
const showEditor = !diffMode && showContentEditor
|
||||
const entryIcon = activeTab?.entry.icon ?? null
|
||||
const hasDisplayIcon = resolveNoteIcon(entryIcon).kind !== 'none'
|
||||
|
||||
const titleSectionRef = useRef<HTMLDivElement | null>(null)
|
||||
const breadcrumbBarRef = useRef<HTMLDivElement | null>(null)
|
||||
|
||||
useBreadcrumbTitleVisibility({
|
||||
showEditor,
|
||||
showTitleSection,
|
||||
path,
|
||||
breadcrumbBarRef,
|
||||
titleSectionRef,
|
||||
})
|
||||
|
||||
return {
|
||||
...props,
|
||||
cssVars,
|
||||
@@ -136,11 +79,7 @@ export function useEditorContentModel(props: EditorContentProps) {
|
||||
effectiveRawMode,
|
||||
forceRawMode: isNonMarkdownText || isDeletedPreview,
|
||||
showEditor,
|
||||
entryIcon,
|
||||
hasDisplayIcon,
|
||||
path,
|
||||
showTitleSection,
|
||||
titleSectionRef,
|
||||
breadcrumbBarRef,
|
||||
wordCount,
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user