2026-05-03 11:34:56 +02:00
|
|
|
import { useEffect, useCallback, useMemo, useRef, useContext, useState } from 'react'
|
|
|
|
|
import { invoke } from '@tauri-apps/api/core'
|
feat: implement PostHog event tracking plan
Add trackEvent() calls for all user actions in the tracking plan:
- Vault: vault_opened (with has_git, note_count), vault_switched
- Notes: note_created (with has_type, creation_path), note_deleted,
note_trashed, note_archived, note_favorited, note_unfavorited
- Git: commit_made, sync_triggered
- Search: search_used
- Editor: raw_mode_toggled, wikilink_inserted
- Types & Views: type_created, view_created
- Telemetry: telemetry_opted_in, telemetry_opted_out
All events gated by PostHog initialization (only fires when opted in).
No PII in any event properties — counts, booleans, and enums only.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-03 20:42:43 +02:00
|
|
|
import { trackEvent } from '../lib/telemetry'
|
2026-04-15 22:42:40 +02:00
|
|
|
import {
|
|
|
|
|
useCreateBlockNote,
|
|
|
|
|
SuggestionMenuController,
|
|
|
|
|
BlockNoteViewRaw,
|
|
|
|
|
ComponentsContext,
|
2026-04-24 23:25:22 +02:00
|
|
|
DeleteLinkButton,
|
|
|
|
|
EditLinkButton,
|
|
|
|
|
LinkToolbar,
|
|
|
|
|
LinkToolbarController,
|
2026-04-17 17:48:12 +02:00
|
|
|
SideMenuController,
|
2026-04-24 23:25:22 +02:00
|
|
|
useComponentsContext,
|
|
|
|
|
useDictionary,
|
|
|
|
|
type LinkToolbarProps,
|
2026-04-15 22:42:40 +02:00
|
|
|
} from '@blocknote/react'
|
2026-04-13 22:45:25 +02:00
|
|
|
import { components } from '@blocknote/mantine'
|
|
|
|
|
import { MantineContext, MantineProvider } from '@mantine/core'
|
2026-05-03 11:34:56 +02:00
|
|
|
import { Copy } from '@phosphor-icons/react'
|
2026-04-24 23:25:22 +02:00
|
|
|
import { ExternalLink } from 'lucide-react'
|
2026-04-24 22:26:07 +02:00
|
|
|
import { useDocumentThemeMode } from '../hooks/useDocumentThemeMode'
|
refactor: decompose Editor.tsx, NoteList.tsx, Sidebar.tsx — CodeScene hotspots (#126)
* refactor: decompose Editor.tsx into focused subcomponents and hooks
Extract from the monolithic Editor component (cc=35, 213 LoC, score 7.82):
- useDiffMode hook: diff state + toggle/commit-diff handlers
- useEditorFocus hook: new-note focus event listener
- editorSchema: WikiLink spec + BlockNote schema (module-level)
- SingleEditorView: BlockNote editor + suggestion menus
- EditorContent: breadcrumb bar + diff/editor/loading views
- EditorRightPanel: AI chat / Inspector panel switching
- suggestionEnrichment util: shared enrichment logic (eliminates duplication)
Editor.tsx is now 10.0 code health (was 7.82). All 25 existing tests pass.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* refactor: extract standalone functions from NoteList and Sidebar to reduce cc
NoteList: extract createNoteStatusResolver and toggleSetMember from
NoteListInner (cc 13→8, score 9.04→9.38).
Sidebar: extract applyCustomization from Sidebar (cc 10→7, score 9.09→10.0).
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* test: add tests for useDiffMode, useEditorFocus, and suggestionEnrichment
Cover extracted hook/utility logic: diff toggle/reset, editor focus event
listener with adaptive timing, and suggestion item enrichment pipeline.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* docs: update architecture for editor decomposition and write .claude-done
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2026-02-27 15:27:25 +01:00
|
|
|
import { useEditorTheme } from '../hooks/useTheme'
|
|
|
|
|
import { useImageDrop } from '../hooks/useImageDrop'
|
2026-04-30 05:49:40 +02:00
|
|
|
import { useImageLightbox } from '../hooks/useImageLightbox'
|
2026-05-03 11:34:56 +02:00
|
|
|
import { createTranslator, type AppLocale } from '../lib/i18n'
|
|
|
|
|
import { isTauri } from '../mock-tauri'
|
refactor: decompose Editor.tsx, NoteList.tsx, Sidebar.tsx — CodeScene hotspots (#126)
* refactor: decompose Editor.tsx into focused subcomponents and hooks
Extract from the monolithic Editor component (cc=35, 213 LoC, score 7.82):
- useDiffMode hook: diff state + toggle/commit-diff handlers
- useEditorFocus hook: new-note focus event listener
- editorSchema: WikiLink spec + BlockNote schema (module-level)
- SingleEditorView: BlockNote editor + suggestion menus
- EditorContent: breadcrumb bar + diff/editor/loading views
- EditorRightPanel: AI chat / Inspector panel switching
- suggestionEnrichment util: shared enrichment logic (eliminates duplication)
Editor.tsx is now 10.0 code health (was 7.82). All 25 existing tests pass.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* refactor: extract standalone functions from NoteList and Sidebar to reduce cc
NoteList: extract createNoteStatusResolver and toggleSetMember from
NoteListInner (cc 13→8, score 9.04→9.38).
Sidebar: extract applyCustomization from Sidebar (cc 10→7, score 9.09→10.0).
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* test: add tests for useDiffMode, useEditorFocus, and suggestionEnrichment
Cover extracted hook/utility logic: diff toggle/reset, editor focus event
listener with adaptive timing, and suggestion item enrichment pipeline.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* docs: update architecture for editor decomposition and write .claude-done
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2026-02-27 15:27:25 +01:00
|
|
|
import { buildTypeEntryMap } from '../utils/typeColors'
|
|
|
|
|
import { preFilterWikilinks, deduplicateByPath, MIN_QUERY_LENGTH } from '../utils/wikilinkSuggestions'
|
|
|
|
|
import { filterPersonMentions, PERSON_MENTION_MIN_QUERY } from '../utils/personMentionSuggestions'
|
|
|
|
|
import { attachClickHandlers, enrichSuggestionItems } from '../utils/suggestionEnrichment'
|
2026-04-24 23:25:22 +02:00
|
|
|
import { openExternalUrl } from '../utils/url'
|
2026-04-26 03:01:10 +02:00
|
|
|
import { observeNativeTextAssistanceDisabled } from '../lib/nativeTextAssistance'
|
2026-04-26 11:31:41 +02:00
|
|
|
import { getRuntimeStyleNonce } from '../lib/runtimeStyleNonce'
|
refactor: decompose Editor.tsx, NoteList.tsx, Sidebar.tsx — CodeScene hotspots (#126)
* refactor: decompose Editor.tsx into focused subcomponents and hooks
Extract from the monolithic Editor component (cc=35, 213 LoC, score 7.82):
- useDiffMode hook: diff state + toggle/commit-diff handlers
- useEditorFocus hook: new-note focus event listener
- editorSchema: WikiLink spec + BlockNote schema (module-level)
- SingleEditorView: BlockNote editor + suggestion menus
- EditorContent: breadcrumb bar + diff/editor/loading views
- EditorRightPanel: AI chat / Inspector panel switching
- suggestionEnrichment util: shared enrichment logic (eliminates duplication)
Editor.tsx is now 10.0 code health (was 7.82). All 25 existing tests pass.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* refactor: extract standalone functions from NoteList and Sidebar to reduce cc
NoteList: extract createNoteStatusResolver and toggleSetMember from
NoteListInner (cc 13→8, score 9.04→9.38).
Sidebar: extract applyCustomization from Sidebar (cc 10→7, score 9.09→10.0).
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* test: add tests for useDiffMode, useEditorFocus, and suggestionEnrichment
Cover extracted hook/utility logic: diff toggle/reset, editor focus event
listener with adaptive timing, and suggestion item enrichment pipeline.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* docs: update architecture for editor decomposition and write .claude-done
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2026-02-27 15:27:25 +01:00
|
|
|
import { WikilinkSuggestionMenu, type WikilinkSuggestionItem } from './WikilinkSuggestionMenu'
|
|
|
|
|
import type { VaultEntry } from '../types'
|
|
|
|
|
import { _wikilinkEntriesRef } from './editorSchema'
|
2026-04-15 19:42:25 +02:00
|
|
|
import { useBlockNoteSideMenuHoverGuard } from './blockNoteSideMenuHoverGuard'
|
2026-04-15 22:42:40 +02:00
|
|
|
import { getTolariaSlashMenuItems } from './tolariaEditorFormattingConfig'
|
2026-04-16 11:24:37 +02:00
|
|
|
import {
|
|
|
|
|
TolariaFormattingToolbar,
|
|
|
|
|
TolariaFormattingToolbarController,
|
|
|
|
|
} from './tolariaEditorFormatting'
|
2026-04-17 17:48:12 +02:00
|
|
|
import { TolariaSideMenu } from './tolariaBlockNoteSideMenu'
|
2026-04-08 19:06:21 +02:00
|
|
|
import { useEditorLinkActivation } from './useEditorLinkActivation'
|
2026-04-25 21:21:41 +02:00
|
|
|
import { findNearestTextCursorBlock } from './blockNoteCursorTarget'
|
2026-04-30 05:49:40 +02:00
|
|
|
import { ImageLightbox } from './ImageLightbox'
|
2026-05-03 11:34:56 +02:00
|
|
|
import { ActionTooltip } from './ui/action-tooltip'
|
|
|
|
|
import { Button } from './ui/button'
|
2026-05-01 00:31:14 +02:00
|
|
|
import {
|
|
|
|
|
activatePlainTextPasteTarget,
|
|
|
|
|
registerPlainTextPasteTarget,
|
|
|
|
|
type PlainTextPasteTarget,
|
|
|
|
|
} from '../utils/plainTextPaste'
|
refactor: decompose Editor.tsx, NoteList.tsx, Sidebar.tsx — CodeScene hotspots (#126)
* refactor: decompose Editor.tsx into focused subcomponents and hooks
Extract from the monolithic Editor component (cc=35, 213 LoC, score 7.82):
- useDiffMode hook: diff state + toggle/commit-diff handlers
- useEditorFocus hook: new-note focus event listener
- editorSchema: WikiLink spec + BlockNote schema (module-level)
- SingleEditorView: BlockNote editor + suggestion menus
- EditorContent: breadcrumb bar + diff/editor/loading views
- EditorRightPanel: AI chat / Inspector panel switching
- suggestionEnrichment util: shared enrichment logic (eliminates duplication)
Editor.tsx is now 10.0 code health (was 7.82). All 25 existing tests pass.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* refactor: extract standalone functions from NoteList and Sidebar to reduce cc
NoteList: extract createNoteStatusResolver and toggleSetMember from
NoteListInner (cc 13→8, score 9.04→9.38).
Sidebar: extract applyCustomization from Sidebar (cc 10→7, score 9.09→10.0).
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* test: add tests for useDiffMode, useEditorFocus, and suggestionEnrichment
Cover extracted hook/utility logic: diff toggle/reset, editor focus event
listener with adaptive timing, and suggestion item enrichment pipeline.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* docs: update architecture for editor decomposition and write .claude-done
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2026-02-27 15:27:25 +01:00
|
|
|
|
2026-04-12 11:45:59 +02:00
|
|
|
const TEST_TABLE_MARKDOWN = `| Head 1 | Head 2 | Head 3 |
|
|
|
|
|
| --- | --- | --- |
|
|
|
|
|
| A | B | C |
|
|
|
|
|
| D | E | F |
|
|
|
|
|
`
|
2026-04-24 23:25:22 +02:00
|
|
|
const CONTAINER_CLICK_IGNORE_SELECTOR = [
|
|
|
|
|
'[contenteditable="true"]',
|
|
|
|
|
'.bn-formatting-toolbar',
|
|
|
|
|
'.bn-link-toolbar',
|
2026-04-26 01:20:11 +02:00
|
|
|
'.bn-side-menu',
|
2026-04-24 23:25:22 +02:00
|
|
|
'.bn-form-popover',
|
2026-05-03 11:34:56 +02:00
|
|
|
'[data-editor-code-copy]',
|
2026-04-24 23:25:22 +02:00
|
|
|
'[role="menu"]',
|
|
|
|
|
'[role="dialog"]',
|
|
|
|
|
].join(', ')
|
|
|
|
|
const TOOLBAR_MOUSE_DOWN_ALLOW_SELECTOR = [
|
|
|
|
|
'[role="menu"]',
|
|
|
|
|
'[role="dialog"]',
|
|
|
|
|
'button[aria-haspopup]',
|
|
|
|
|
'input',
|
|
|
|
|
'textarea',
|
|
|
|
|
'[contenteditable="true"]',
|
|
|
|
|
].join(', ')
|
2026-04-12 11:45:59 +02:00
|
|
|
|
|
|
|
|
type TestTableBlock = {
|
|
|
|
|
type?: string
|
|
|
|
|
content?: { type?: string; columnWidths?: Array<number | null> }
|
|
|
|
|
}
|
2026-04-28 23:29:09 +02:00
|
|
|
type SuggestionAction = () => void
|
|
|
|
|
type SuggestionItemWithClick = { onItemClick?: SuggestionAction }
|
|
|
|
|
|
|
|
|
|
function isEditorReadyForSuggestionAction(
|
|
|
|
|
editor: ReturnType<typeof useCreateBlockNote>,
|
|
|
|
|
container: HTMLElement | null,
|
|
|
|
|
) {
|
|
|
|
|
if (!container?.isConnected) return false
|
|
|
|
|
|
|
|
|
|
const editorElement = editor.domElement
|
|
|
|
|
if (!(editorElement instanceof HTMLElement)) return true
|
|
|
|
|
|
|
|
|
|
return editorElement.isConnected && container.contains(editorElement)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function runSuggestionActionSafely({
|
|
|
|
|
action,
|
|
|
|
|
container,
|
|
|
|
|
editor,
|
|
|
|
|
}: {
|
|
|
|
|
action: SuggestionAction
|
|
|
|
|
container: HTMLElement | null
|
|
|
|
|
editor: ReturnType<typeof useCreateBlockNote>
|
|
|
|
|
}) {
|
|
|
|
|
if (!isEditorReadyForSuggestionAction(editor, container)) return
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
action()
|
|
|
|
|
} catch (error) {
|
|
|
|
|
console.warn('[editor] Ignored stale suggestion menu action:', error)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function guardSuggestionMenuItems<T extends SuggestionItemWithClick>(
|
|
|
|
|
items: T[],
|
|
|
|
|
runEditorAction: (action: SuggestionAction) => void,
|
|
|
|
|
): T[] {
|
|
|
|
|
return items.map((item) => {
|
|
|
|
|
if (!item.onItemClick) return item
|
|
|
|
|
|
|
|
|
|
const onItemClick = item.onItemClick
|
|
|
|
|
return {
|
|
|
|
|
...item,
|
|
|
|
|
onItemClick: () => runEditorAction(onItemClick),
|
|
|
|
|
}
|
|
|
|
|
})
|
|
|
|
|
}
|
2026-04-12 11:45:59 +02:00
|
|
|
|
2026-04-13 22:45:25 +02:00
|
|
|
function SharedContextBlockNoteView(props: React.ComponentProps<typeof BlockNoteViewRaw>) {
|
2026-04-15 22:42:40 +02:00
|
|
|
const { children, className, theme, ...rest } = props
|
2026-04-13 22:45:25 +02:00
|
|
|
const mantineContext = useContext(MantineContext)
|
2026-04-14 11:14:49 +02:00
|
|
|
const colorScheme = theme === 'dark' ? 'dark' : 'light'
|
2026-04-13 22:45:25 +02:00
|
|
|
const view = (
|
|
|
|
|
<ComponentsContext.Provider value={components}>
|
2026-04-14 11:14:49 +02:00
|
|
|
<BlockNoteViewRaw
|
|
|
|
|
{...rest}
|
|
|
|
|
className={['bn-mantine', className].filter(Boolean).join(' ')}
|
|
|
|
|
data-mantine-color-scheme={colorScheme}
|
|
|
|
|
theme={theme}
|
2026-04-15 22:42:40 +02:00
|
|
|
>
|
|
|
|
|
{children}
|
|
|
|
|
</BlockNoteViewRaw>
|
2026-04-13 22:45:25 +02:00
|
|
|
</ComponentsContext.Provider>
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
if (mantineContext) return view
|
|
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
<MantineProvider
|
2026-04-14 11:14:49 +02:00
|
|
|
// BlockNote scopes Mantine defaults under `.bn-mantine` instead of `:root`.
|
2026-04-13 22:45:25 +02:00
|
|
|
withCssVariables={false}
|
2026-04-26 11:31:41 +02:00
|
|
|
getStyleNonce={getRuntimeStyleNonce}
|
2026-04-13 22:45:25 +02:00
|
|
|
getRootElement={() => undefined}
|
|
|
|
|
>
|
|
|
|
|
{view}
|
|
|
|
|
</MantineProvider>
|
|
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-24 23:25:22 +02:00
|
|
|
function shouldAllowToolbarMouseDown(target: HTMLElement) {
|
|
|
|
|
return Boolean(target.closest(TOOLBAR_MOUSE_DOWN_ALLOW_SELECTOR))
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function handleToolbarMouseDownCapture(
|
|
|
|
|
event: Pick<React.MouseEvent<HTMLElement>, 'target' | 'preventDefault'>,
|
|
|
|
|
) {
|
|
|
|
|
if (!(event.target instanceof HTMLElement) || shouldAllowToolbarMouseDown(event.target)) {
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
event.preventDefault()
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function TolariaOpenLinkButton({ url }: Pick<LinkToolbarProps, 'url'>) {
|
|
|
|
|
const Components = useComponentsContext()!
|
|
|
|
|
const dict = useDictionary()
|
|
|
|
|
const handleOpen = useCallback(() => {
|
|
|
|
|
void openExternalUrl(url).catch((error) => {
|
|
|
|
|
console.warn('[link] Failed to open URL from toolbar:', error)
|
|
|
|
|
})
|
|
|
|
|
}, [url])
|
|
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
<Components.LinkToolbar.Button
|
|
|
|
|
className="bn-button"
|
|
|
|
|
label={dict.link_toolbar.open.tooltip}
|
|
|
|
|
mainTooltip={dict.link_toolbar.open.tooltip}
|
|
|
|
|
isSelected={false}
|
|
|
|
|
onClick={handleOpen}
|
|
|
|
|
icon={<ExternalLink size={16} />}
|
|
|
|
|
/>
|
|
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function TolariaLinkToolbar(props: LinkToolbarProps) {
|
|
|
|
|
return (
|
|
|
|
|
<LinkToolbar {...props}>
|
|
|
|
|
<EditLinkButton
|
|
|
|
|
url={props.url}
|
|
|
|
|
text={props.text}
|
|
|
|
|
range={props.range}
|
|
|
|
|
setToolbarOpen={props.setToolbarOpen}
|
|
|
|
|
setToolbarPositionFrozen={props.setToolbarPositionFrozen}
|
|
|
|
|
/>
|
|
|
|
|
<TolariaOpenLinkButton url={props.url} />
|
|
|
|
|
<DeleteLinkButton
|
|
|
|
|
range={props.range}
|
|
|
|
|
setToolbarOpen={props.setToolbarOpen}
|
|
|
|
|
/>
|
|
|
|
|
</LinkToolbar>
|
|
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-12 11:45:59 +02:00
|
|
|
function applySeededColumnWidths(
|
|
|
|
|
parsedBlocks: Array<TestTableBlock>,
|
|
|
|
|
columnWidths?: Array<number | null>,
|
|
|
|
|
) {
|
2026-04-13 22:45:25 +02:00
|
|
|
if (!columnWidths) return
|
|
|
|
|
|
2026-04-12 11:45:59 +02:00
|
|
|
const tableBlock = parsedBlocks[0]
|
2026-04-13 22:45:25 +02:00
|
|
|
if (tableBlock?.type !== 'table') return
|
2026-04-12 12:29:13 +02:00
|
|
|
|
2026-04-13 22:45:25 +02:00
|
|
|
const tableContent = tableBlock.content
|
|
|
|
|
if (tableContent?.type !== 'tableContent') return
|
2026-04-12 11:45:59 +02:00
|
|
|
|
2026-04-12 12:29:13 +02:00
|
|
|
tableContent.columnWidths = [...columnWidths]
|
2026-04-12 11:45:59 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async function seedEditorWithTestTable(
|
|
|
|
|
editor: ReturnType<typeof useCreateBlockNote>,
|
|
|
|
|
columnWidths?: Array<number | null>,
|
|
|
|
|
) {
|
|
|
|
|
const parsedBlocks = await Promise.resolve(
|
|
|
|
|
editor.tryParseMarkdownToBlocks(TEST_TABLE_MARKDOWN),
|
|
|
|
|
) as Array<TestTableBlock>
|
|
|
|
|
|
|
|
|
|
applySeededColumnWidths(parsedBlocks, columnWidths)
|
|
|
|
|
|
|
|
|
|
const tableHtml = editor.blocksToHTMLLossy([
|
|
|
|
|
...parsedBlocks,
|
|
|
|
|
{ type: 'paragraph', content: [], children: [] },
|
|
|
|
|
] as typeof editor.document)
|
|
|
|
|
editor._tiptapEditor.commands.setContent(tableHtml)
|
|
|
|
|
editor.focus()
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function useSeedBlockNoteTableBridge(editor: ReturnType<typeof useCreateBlockNote>) {
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
const seedBlockNoteTable = (columnWidths?: Array<number | null>) => (
|
|
|
|
|
seedEditorWithTestTable(editor, columnWidths)
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
window.__laputaTest = {
|
|
|
|
|
...window.__laputaTest,
|
|
|
|
|
seedBlockNoteTable,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return () => {
|
|
|
|
|
if (window.__laputaTest?.seedBlockNoteTable === seedBlockNoteTable) {
|
|
|
|
|
delete window.__laputaTest.seedBlockNoteTable
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}, [editor])
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-16 11:24:37 +02:00
|
|
|
function shouldIgnoreContainerClick(target: HTMLElement) {
|
2026-04-24 23:25:22 +02:00
|
|
|
return Boolean(target.closest(CONTAINER_CLICK_IGNORE_SELECTOR))
|
2026-04-16 11:24:37 +02:00
|
|
|
}
|
|
|
|
|
|
2026-04-20 18:35:50 +02:00
|
|
|
function normalizeSuggestionQuery(query: string, triggerCharacter: string): string {
|
|
|
|
|
return query.startsWith(triggerCharacter)
|
|
|
|
|
? query.slice(triggerCharacter.length)
|
|
|
|
|
: query
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function isSelectionInsideElement(element: HTMLElement): boolean {
|
|
|
|
|
const selection = window.getSelection()
|
|
|
|
|
const anchorNode = selection?.anchorNode ?? null
|
|
|
|
|
const anchorElement = anchorNode instanceof Element ? anchorNode : anchorNode?.parentElement ?? null
|
|
|
|
|
return Boolean(anchorElement && element.contains(anchorElement))
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-24 20:13:34 +02:00
|
|
|
const TITLE_HEADING_SELECTOR = 'h1, [data-content-type="heading"][data-level="1"], [data-content-type="heading"]:not([data-level])'
|
|
|
|
|
const TITLE_HEADING_WRAPPER_SELECTOR = '.bn-block-outer, .bn-block'
|
2026-04-27 18:31:51 +02:00
|
|
|
const CODE_BLOCK_SELECTOR = '[data-content-type="codeBlock"]'
|
2026-05-03 11:34:56 +02:00
|
|
|
const CODE_BLOCK_COPY_RESET_MS = 1200
|
2026-04-27 18:31:51 +02:00
|
|
|
|
|
|
|
|
function nodeElement(node: Node | null): HTMLElement | null {
|
|
|
|
|
if (!node) return null
|
|
|
|
|
if (node instanceof HTMLElement) return node
|
|
|
|
|
return node.parentElement
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function hasSingleActiveRange(selection: Selection | null): selection is Selection {
|
|
|
|
|
return Boolean(selection && selection.rangeCount === 1 && !selection.isCollapsed)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function closestCodeBlockInContainer(options: {
|
|
|
|
|
range: Range
|
|
|
|
|
container: HTMLElement
|
|
|
|
|
}): HTMLElement | null {
|
|
|
|
|
const { range, container } = options
|
|
|
|
|
const codeBlock = nodeElement(range.commonAncestorContainer)
|
|
|
|
|
?.closest<HTMLElement>(CODE_BLOCK_SELECTOR)
|
|
|
|
|
|
|
|
|
|
return codeBlock && container.contains(codeBlock) ? codeBlock : null
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function nodeBelongsToElement(node: Node, element: HTMLElement): boolean {
|
|
|
|
|
const elementNode = nodeElement(node)
|
|
|
|
|
return Boolean(elementNode && element.contains(elementNode))
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function rangeBelongsToElement(range: Range, element: HTMLElement): boolean {
|
|
|
|
|
return nodeBelongsToElement(range.startContainer, element)
|
|
|
|
|
&& nodeBelongsToElement(range.endContainer, element)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function selectedCodeBlockRange(options: {
|
|
|
|
|
selection: Selection | null
|
|
|
|
|
container: HTMLElement
|
|
|
|
|
}): Range | null {
|
|
|
|
|
const { selection, container } = options
|
|
|
|
|
if (!hasSingleActiveRange(selection)) return null
|
|
|
|
|
|
|
|
|
|
const range = selection.getRangeAt(0)
|
|
|
|
|
const codeBlock = closestCodeBlockInContainer({ range, container })
|
|
|
|
|
if (!codeBlock || !rangeBelongsToElement(range, codeBlock)) return null
|
|
|
|
|
|
|
|
|
|
return range
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function selectedCodeBlockText(options: {
|
|
|
|
|
selection: Selection | null
|
|
|
|
|
container: HTMLElement
|
|
|
|
|
}): string | null {
|
|
|
|
|
const range = selectedCodeBlockRange(options)
|
|
|
|
|
if (!range) return null
|
|
|
|
|
|
|
|
|
|
return options.selection?.toString() || range.cloneContents().textContent || ''
|
|
|
|
|
}
|
2026-04-24 20:13:34 +02:00
|
|
|
|
2026-05-03 11:34:56 +02:00
|
|
|
function codeBlockText(codeBlock: HTMLElement): string {
|
|
|
|
|
const codeElement = codeBlock.querySelector<HTMLElement>('pre code')
|
|
|
|
|
return codeElement?.textContent ?? ''
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async function writeClipboardText(text: string): Promise<void> {
|
|
|
|
|
if (isTauri()) {
|
|
|
|
|
await invoke('copy_text_to_clipboard', { text })
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (!navigator.clipboard?.writeText) {
|
|
|
|
|
throw new Error('Clipboard API is unavailable')
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
await navigator.clipboard.writeText(text)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
type CodeBlockCopyTarget = {
|
|
|
|
|
codeBlock: HTMLElement
|
|
|
|
|
left: number
|
|
|
|
|
top: number
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function codeBlockCopyTarget(codeBlock: HTMLElement, container: HTMLElement): CodeBlockCopyTarget {
|
|
|
|
|
const codeBlockRect = codeBlock.getBoundingClientRect()
|
|
|
|
|
const containerRect = container.getBoundingClientRect()
|
|
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
codeBlock,
|
|
|
|
|
left: codeBlockRect.right - containerRect.left + container.scrollLeft - 30,
|
|
|
|
|
top: codeBlockRect.top - containerRect.top + container.scrollTop + 6,
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function sameCopyTarget(left: CodeBlockCopyTarget | null, right: CodeBlockCopyTarget): boolean {
|
|
|
|
|
return Boolean(
|
|
|
|
|
left
|
|
|
|
|
&& left.codeBlock === right.codeBlock
|
|
|
|
|
&& left.left === right.left
|
|
|
|
|
&& left.top === right.top,
|
|
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function useCodeBlockCopyTarget(containerRef: React.RefObject<HTMLDivElement | null>) {
|
|
|
|
|
const [copyTarget, setCopyTarget] = useState<CodeBlockCopyTarget | null>(null)
|
|
|
|
|
|
|
|
|
|
const showCopyTarget = useCallback((codeBlock: HTMLElement) => {
|
|
|
|
|
const container = containerRef.current
|
|
|
|
|
if (!container || !container.contains(codeBlock)) return
|
|
|
|
|
|
|
|
|
|
const nextTarget = codeBlockCopyTarget(codeBlock, container)
|
|
|
|
|
setCopyTarget((previous) => sameCopyTarget(previous, nextTarget) ? previous : nextTarget)
|
|
|
|
|
}, [containerRef])
|
|
|
|
|
|
|
|
|
|
const updateFromEventTarget = useCallback((target: EventTarget | null) => {
|
|
|
|
|
const container = containerRef.current
|
|
|
|
|
if (!(target instanceof HTMLElement) || !container) return
|
|
|
|
|
if (target.closest('[data-editor-code-copy]')) return
|
|
|
|
|
|
|
|
|
|
const codeBlock = target.closest<HTMLElement>(CODE_BLOCK_SELECTOR)
|
|
|
|
|
if (codeBlock && container.contains(codeBlock)) {
|
|
|
|
|
showCopyTarget(codeBlock)
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
setCopyTarget(null)
|
|
|
|
|
}, [containerRef, showCopyTarget])
|
|
|
|
|
|
|
|
|
|
const handleMouseMove = useCallback((event: React.MouseEvent<HTMLDivElement>) => {
|
|
|
|
|
updateFromEventTarget(event.target)
|
|
|
|
|
}, [updateFromEventTarget])
|
|
|
|
|
|
|
|
|
|
const handleFocus = useCallback((event: React.FocusEvent<HTMLDivElement>) => {
|
|
|
|
|
updateFromEventTarget(event.target)
|
|
|
|
|
}, [updateFromEventTarget])
|
|
|
|
|
|
|
|
|
|
const clearCopyTarget = useCallback(() => setCopyTarget(null), [])
|
|
|
|
|
|
|
|
|
|
return { clearCopyTarget, copyTarget, handleFocus, handleMouseMove }
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function CodeBlockCopyButton({ copyTarget, locale }: { copyTarget: CodeBlockCopyTarget; locale: AppLocale }) {
|
|
|
|
|
const [active, setActive] = useState(false)
|
|
|
|
|
const resetTimerRef = useRef<number | null>(null)
|
|
|
|
|
const t = useMemo(() => createTranslator(locale), [locale])
|
|
|
|
|
const label = t('editor.codeBlock.copy')
|
|
|
|
|
|
|
|
|
|
useEffect(() => () => {
|
|
|
|
|
if (resetTimerRef.current !== null) window.clearTimeout(resetTimerRef.current)
|
|
|
|
|
}, [])
|
|
|
|
|
|
|
|
|
|
const handleCopy = useCallback((event: React.MouseEvent<HTMLButtonElement>) => {
|
|
|
|
|
event.preventDefault()
|
|
|
|
|
event.stopPropagation()
|
|
|
|
|
|
|
|
|
|
void writeClipboardText(codeBlockText(copyTarget.codeBlock))
|
|
|
|
|
.then(() => {
|
|
|
|
|
trackEvent('code_block_copied')
|
|
|
|
|
setActive(true)
|
|
|
|
|
if (resetTimerRef.current !== null) window.clearTimeout(resetTimerRef.current)
|
|
|
|
|
resetTimerRef.current = window.setTimeout(() => {
|
|
|
|
|
setActive(false)
|
|
|
|
|
resetTimerRef.current = null
|
|
|
|
|
}, CODE_BLOCK_COPY_RESET_MS)
|
|
|
|
|
})
|
|
|
|
|
.catch((error) => {
|
|
|
|
|
console.warn('[editor] Failed to copy code block:', error)
|
|
|
|
|
})
|
|
|
|
|
}, [copyTarget])
|
|
|
|
|
|
|
|
|
|
const stopEditorMouseDown = useCallback((event: React.MouseEvent<HTMLButtonElement>) => {
|
|
|
|
|
event.preventDefault()
|
|
|
|
|
event.stopPropagation()
|
|
|
|
|
}, [])
|
|
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
<div
|
|
|
|
|
className="editor__code-block-copy"
|
|
|
|
|
contentEditable={false}
|
|
|
|
|
data-editor-code-copy
|
|
|
|
|
style={{ left: copyTarget.left, top: copyTarget.top }}
|
|
|
|
|
>
|
|
|
|
|
<ActionTooltip copy={{ label }} side="left" align="center">
|
|
|
|
|
<Button
|
|
|
|
|
aria-label={label}
|
2026-05-03 12:28:00 +02:00
|
|
|
className="border-transparent bg-transparent text-muted-foreground shadow-none hover:bg-transparent hover:text-foreground focus-visible:bg-transparent focus-visible:text-foreground"
|
2026-05-03 11:34:56 +02:00
|
|
|
data-editor-code-copy-button
|
|
|
|
|
onBlur={() => setActive(false)}
|
|
|
|
|
onClick={handleCopy}
|
|
|
|
|
onFocus={() => setActive(true)}
|
|
|
|
|
onMouseDown={stopEditorMouseDown}
|
|
|
|
|
onMouseEnter={() => setActive(true)}
|
|
|
|
|
onMouseLeave={() => setActive(false)}
|
|
|
|
|
size="icon-xs"
|
|
|
|
|
type="button"
|
|
|
|
|
variant="ghost"
|
|
|
|
|
>
|
2026-05-03 12:28:00 +02:00
|
|
|
<Copy aria-hidden="true" className="size-6" weight={active ? 'fill' : 'regular'} />
|
2026-05-03 11:34:56 +02:00
|
|
|
</Button>
|
|
|
|
|
</ActionTooltip>
|
|
|
|
|
</div>
|
|
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-24 20:13:34 +02:00
|
|
|
function findTitleHeadingElement(target: HTMLElement): HTMLElement | null {
|
|
|
|
|
const directHeading = target.closest<HTMLElement>(TITLE_HEADING_SELECTOR)
|
|
|
|
|
if (directHeading) return directHeading
|
|
|
|
|
|
|
|
|
|
const titleWrapper = target.closest<HTMLElement>(TITLE_HEADING_WRAPPER_SELECTOR)
|
|
|
|
|
return titleWrapper?.querySelector<HTMLElement>(TITLE_HEADING_SELECTOR) ?? null
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-20 18:35:50 +02:00
|
|
|
function queueTitleHeadingCursorRepair(
|
|
|
|
|
target: HTMLElement,
|
|
|
|
|
editor: ReturnType<typeof useCreateBlockNote>,
|
|
|
|
|
): boolean {
|
2026-04-24 20:13:34 +02:00
|
|
|
const titleHeading = findTitleHeadingElement(target)
|
2026-04-20 18:35:50 +02:00
|
|
|
if (!titleHeading) return false
|
|
|
|
|
|
|
|
|
|
queueMicrotask(() => {
|
|
|
|
|
if (isSelectionInsideElement(titleHeading)) return
|
|
|
|
|
|
|
|
|
|
const firstBlock = editor.document[0]
|
|
|
|
|
if (firstBlock?.type !== 'heading') return
|
|
|
|
|
|
2026-04-25 21:21:41 +02:00
|
|
|
try {
|
|
|
|
|
editor.setTextCursorPosition(firstBlock.id, 'end')
|
|
|
|
|
} catch {
|
|
|
|
|
return
|
|
|
|
|
}
|
2026-04-20 18:35:50 +02:00
|
|
|
editor.focus()
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
return true
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function useEditorContainerClickHandler(options: {
|
|
|
|
|
editable: boolean
|
|
|
|
|
editor: ReturnType<typeof useCreateBlockNote>
|
|
|
|
|
}) {
|
|
|
|
|
const { editable, editor } = options
|
|
|
|
|
|
|
|
|
|
return useCallback((e: React.MouseEvent<HTMLDivElement>) => {
|
|
|
|
|
if (!editable) return
|
|
|
|
|
const target = e.target as HTMLElement
|
|
|
|
|
if (queueTitleHeadingCursorRepair(target, editor)) return
|
|
|
|
|
if (shouldIgnoreContainerClick(target)) return
|
|
|
|
|
const blocks = editor.document
|
|
|
|
|
if (blocks.length > 0) {
|
2026-04-25 21:21:41 +02:00
|
|
|
const targetBlock = findNearestTextCursorBlock(blocks, blocks.length - 1)
|
|
|
|
|
if (targetBlock) {
|
|
|
|
|
try {
|
|
|
|
|
editor.setTextCursorPosition(targetBlock.id, 'end')
|
|
|
|
|
} catch {
|
|
|
|
|
// Ignore transient BlockNote selection errors and at least restore focus.
|
|
|
|
|
}
|
|
|
|
|
}
|
2026-04-20 18:35:50 +02:00
|
|
|
}
|
|
|
|
|
editor.focus()
|
|
|
|
|
}, [editor, editable])
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-24 02:50:12 +02:00
|
|
|
function useCompositionAwareEditorChange(options: {
|
|
|
|
|
containerRef: React.RefObject<HTMLDivElement | null>
|
|
|
|
|
onChange?: () => void
|
|
|
|
|
}) {
|
|
|
|
|
const { containerRef, onChange } = options
|
|
|
|
|
const onChangeRef = useRef(onChange)
|
|
|
|
|
const composingRef = useRef(false)
|
|
|
|
|
const pendingChangeRef = useRef(false)
|
|
|
|
|
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
onChangeRef.current = onChange
|
|
|
|
|
}, [onChange])
|
|
|
|
|
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
const container = containerRef.current
|
|
|
|
|
if (!container) return
|
|
|
|
|
|
|
|
|
|
const flushPendingChange = () => {
|
|
|
|
|
if (composingRef.current || !pendingChangeRef.current) return
|
|
|
|
|
pendingChangeRef.current = false
|
|
|
|
|
onChangeRef.current?.()
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const handleCompositionStart = () => {
|
|
|
|
|
composingRef.current = true
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const handleCompositionEnd = () => {
|
|
|
|
|
composingRef.current = false
|
|
|
|
|
queueMicrotask(flushPendingChange)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
container.addEventListener('compositionstart', handleCompositionStart, true)
|
|
|
|
|
container.addEventListener('compositionend', handleCompositionEnd, true)
|
|
|
|
|
return () => {
|
|
|
|
|
container.removeEventListener('compositionstart', handleCompositionStart, true)
|
|
|
|
|
container.removeEventListener('compositionend', handleCompositionEnd, true)
|
|
|
|
|
}
|
|
|
|
|
}, [containerRef])
|
|
|
|
|
|
|
|
|
|
return useCallback(() => {
|
|
|
|
|
if (composingRef.current) {
|
|
|
|
|
pendingChangeRef.current = true
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
pendingChangeRef.current = false
|
|
|
|
|
onChangeRef.current?.()
|
|
|
|
|
}, [])
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-27 18:31:51 +02:00
|
|
|
function handleCodeBlockCopy(event: React.ClipboardEvent<HTMLDivElement>) {
|
|
|
|
|
const codeText = selectedCodeBlockText({
|
|
|
|
|
selection: window.getSelection(),
|
|
|
|
|
container: event.currentTarget,
|
|
|
|
|
})
|
|
|
|
|
if (codeText === null) return
|
|
|
|
|
|
|
|
|
|
event.clipboardData.setData('text/plain', codeText)
|
|
|
|
|
event.preventDefault()
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-30 12:09:35 +02:00
|
|
|
function nonEmptyString(value: unknown): string | null {
|
|
|
|
|
return typeof value === 'string' && value.trim().length > 0 ? value : null
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function markdownStem(value: string): string {
|
|
|
|
|
return value.replace(/\.md$/i, '')
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function pathStem(path: string): string {
|
|
|
|
|
return markdownStem(path.split('/').pop() ?? path)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function safeStringArray(value: unknown): string[] {
|
|
|
|
|
return Array.isArray(value)
|
|
|
|
|
? value.filter((item): item is string => nonEmptyString(item) !== null)
|
|
|
|
|
: []
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-20 18:35:50 +02:00
|
|
|
function buildBaseSuggestionItems(entries: VaultEntry[]) {
|
2026-04-30 12:09:35 +02:00
|
|
|
return deduplicateByPath(entries.flatMap(entry => {
|
|
|
|
|
const path = nonEmptyString(entry.path)
|
|
|
|
|
if (!path) return []
|
|
|
|
|
|
|
|
|
|
const filename = nonEmptyString(entry.filename)
|
|
|
|
|
const filenameStem = filename ? markdownStem(filename) : pathStem(path)
|
|
|
|
|
const title = nonEmptyString(entry.title) ?? filenameStem
|
|
|
|
|
const entryType = nonEmptyString(entry.isA)
|
|
|
|
|
return [{
|
|
|
|
|
title,
|
|
|
|
|
aliases: [...new Set([filenameStem, ...safeStringArray(entry.aliases)])],
|
|
|
|
|
group: entryType ?? 'Note',
|
|
|
|
|
entryType,
|
|
|
|
|
entryTitle: title,
|
|
|
|
|
path,
|
|
|
|
|
}]
|
|
|
|
|
}))
|
2026-04-20 18:35:50 +02:00
|
|
|
}
|
|
|
|
|
|
2026-04-28 23:29:09 +02:00
|
|
|
function useInsertWikilink(
|
|
|
|
|
editor: ReturnType<typeof useCreateBlockNote>,
|
|
|
|
|
runEditorAction: (action: SuggestionAction) => void,
|
|
|
|
|
) {
|
2026-04-20 18:35:50 +02:00
|
|
|
return useCallback((target: string) => {
|
2026-04-28 23:29:09 +02:00
|
|
|
runEditorAction(() => {
|
|
|
|
|
editor.insertInlineContent([
|
|
|
|
|
{ type: 'wikilink' as const, props: { target } },
|
|
|
|
|
" ",
|
|
|
|
|
], { updateSelection: true })
|
|
|
|
|
trackEvent('wikilink_inserted')
|
|
|
|
|
})
|
|
|
|
|
}, [editor, runEditorAction])
|
2026-04-20 18:35:50 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function useSuggestionMenuItems(options: {
|
|
|
|
|
baseItems: ReturnType<typeof buildBaseSuggestionItems>
|
|
|
|
|
editor: ReturnType<typeof useCreateBlockNote>
|
|
|
|
|
insertWikilink: (target: string) => void
|
2026-04-28 23:29:09 +02:00
|
|
|
runEditorAction: (action: SuggestionAction) => void
|
2026-04-20 18:35:50 +02:00
|
|
|
typeEntryMap: Record<string, VaultEntry>
|
|
|
|
|
vaultPath?: string
|
|
|
|
|
}) {
|
|
|
|
|
const {
|
|
|
|
|
baseItems,
|
|
|
|
|
editor,
|
|
|
|
|
insertWikilink,
|
2026-04-28 23:29:09 +02:00
|
|
|
runEditorAction,
|
2026-04-20 18:35:50 +02:00
|
|
|
typeEntryMap,
|
|
|
|
|
vaultPath,
|
|
|
|
|
} = options
|
|
|
|
|
|
|
|
|
|
const buildItems = useCallback((query: string, triggerCharacter: '[[' | '@') => {
|
|
|
|
|
const normalizedQuery = normalizeSuggestionQuery(query, triggerCharacter)
|
|
|
|
|
const minLength = triggerCharacter === '[[' ? MIN_QUERY_LENGTH : PERSON_MENTION_MIN_QUERY
|
|
|
|
|
if (normalizedQuery.length < minLength) return null
|
|
|
|
|
|
|
|
|
|
const candidates = triggerCharacter === '[['
|
|
|
|
|
? preFilterWikilinks(baseItems, normalizedQuery)
|
|
|
|
|
: filterPersonMentions(baseItems, normalizedQuery)
|
|
|
|
|
|
|
|
|
|
const items = attachClickHandlers(candidates, insertWikilink, vaultPath ?? '')
|
2026-04-28 23:29:09 +02:00
|
|
|
return guardSuggestionMenuItems(
|
|
|
|
|
enrichSuggestionItems(items, normalizedQuery, typeEntryMap),
|
|
|
|
|
runEditorAction,
|
|
|
|
|
)
|
|
|
|
|
}, [baseItems, insertWikilink, runEditorAction, typeEntryMap, vaultPath])
|
2026-04-20 18:35:50 +02:00
|
|
|
|
|
|
|
|
const getWikilinkItems = useCallback(async (query: string): Promise<WikilinkSuggestionItem[]> => (
|
|
|
|
|
buildItems(query, '[[') ?? []
|
|
|
|
|
), [buildItems])
|
|
|
|
|
|
|
|
|
|
const getPersonMentionItems = useCallback(async (query: string): Promise<WikilinkSuggestionItem[]> => (
|
|
|
|
|
buildItems(query, '@') ?? []
|
|
|
|
|
), [buildItems])
|
|
|
|
|
|
2026-04-28 23:29:09 +02:00
|
|
|
const getSlashMenuItems = useCallback(async (query: string) => {
|
|
|
|
|
try {
|
|
|
|
|
return guardSuggestionMenuItems(
|
|
|
|
|
await Promise.resolve(getTolariaSlashMenuItems(editor, query)),
|
|
|
|
|
runEditorAction,
|
|
|
|
|
)
|
|
|
|
|
} catch (error) {
|
|
|
|
|
console.warn('[editor] Ignored stale slash menu query:', error)
|
|
|
|
|
return []
|
|
|
|
|
}
|
|
|
|
|
}, [editor, runEditorAction])
|
2026-04-20 18:35:50 +02:00
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
getWikilinkItems,
|
|
|
|
|
getPersonMentionItems,
|
|
|
|
|
getSlashMenuItems,
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-03 11:34:56 +02:00
|
|
|
type EditorInteractionControllersProps = ReturnType<typeof useSuggestionMenuItems> & {
|
|
|
|
|
runEditorAction: (action: SuggestionAction) => void
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function EditorInteractionControllers({
|
|
|
|
|
getPersonMentionItems,
|
|
|
|
|
getSlashMenuItems,
|
|
|
|
|
getWikilinkItems,
|
|
|
|
|
runEditorAction,
|
|
|
|
|
}: EditorInteractionControllersProps) {
|
|
|
|
|
return (
|
|
|
|
|
<>
|
|
|
|
|
<SideMenuController sideMenu={TolariaSideMenu} />
|
|
|
|
|
<TolariaFormattingToolbarController
|
|
|
|
|
formattingToolbar={TolariaFormattingToolbar}
|
|
|
|
|
floatingUIOptions={{
|
|
|
|
|
elementProps: {
|
|
|
|
|
onMouseDownCapture: handleToolbarMouseDownCapture,
|
|
|
|
|
},
|
|
|
|
|
}}
|
|
|
|
|
/>
|
|
|
|
|
<LinkToolbarController
|
|
|
|
|
linkToolbar={TolariaLinkToolbar}
|
|
|
|
|
floatingUIOptions={{
|
|
|
|
|
elementProps: {
|
|
|
|
|
onMouseDownCapture: handleToolbarMouseDownCapture,
|
|
|
|
|
},
|
|
|
|
|
}}
|
|
|
|
|
/>
|
|
|
|
|
<SuggestionMenuController
|
|
|
|
|
triggerCharacter="/"
|
|
|
|
|
getItems={getSlashMenuItems}
|
|
|
|
|
/>
|
|
|
|
|
<SuggestionMenuController
|
|
|
|
|
triggerCharacter="[["
|
|
|
|
|
getItems={getWikilinkItems}
|
|
|
|
|
suggestionMenuComponent={WikilinkSuggestionMenu}
|
|
|
|
|
onItemClick={(item: WikilinkSuggestionItem) => runEditorAction(item.onItemClick)}
|
|
|
|
|
/>
|
|
|
|
|
<SuggestionMenuController
|
|
|
|
|
triggerCharacter="@"
|
|
|
|
|
getItems={getPersonMentionItems}
|
|
|
|
|
suggestionMenuComponent={WikilinkSuggestionMenu}
|
|
|
|
|
onItemClick={(item: WikilinkSuggestionItem) => runEditorAction(item.onItemClick)}
|
|
|
|
|
/>
|
|
|
|
|
</>
|
|
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-28 21:31:47 +01:00
|
|
|
/** Insert an image block after the current cursor position. */
|
|
|
|
|
function useInsertImageCallback(editor: ReturnType<typeof useCreateBlockNote>) {
|
|
|
|
|
const editorRef = useRef(editor)
|
|
|
|
|
useEffect(() => { editorRef.current = editor }, [editor])
|
|
|
|
|
return useCallback((url: string) => {
|
|
|
|
|
const e = editorRef.current
|
|
|
|
|
const cursorBlock = e.getTextCursorPosition().block
|
|
|
|
|
e.insertBlocks([{ type: 'image' as const, props: { url } }], cursorBlock, 'after')
|
|
|
|
|
}, [])
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-01 00:31:14 +02:00
|
|
|
function useRichEditorPlainTextPasteTarget(options: {
|
|
|
|
|
containerRef: React.RefObject<HTMLDivElement | null>
|
|
|
|
|
editable: boolean
|
|
|
|
|
editor: ReturnType<typeof useCreateBlockNote>
|
|
|
|
|
runEditorAction: (action: SuggestionAction) => void
|
|
|
|
|
}) {
|
|
|
|
|
const { containerRef, editable, editor, runEditorAction } = options
|
|
|
|
|
const targetRef = useRef<PlainTextPasteTarget | null>(null)
|
|
|
|
|
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
const target: PlainTextPasteTarget = {
|
|
|
|
|
surface: 'rich_editor',
|
|
|
|
|
contains: (element) => Boolean(element && containerRef.current?.contains(element)),
|
|
|
|
|
isConnected: () => containerRef.current?.isConnected === true,
|
|
|
|
|
insert: (text) => {
|
|
|
|
|
if (!editable) return false
|
|
|
|
|
|
|
|
|
|
let inserted = false
|
|
|
|
|
runEditorAction(() => {
|
|
|
|
|
editor.focus()
|
|
|
|
|
editor.insertInlineContent(text, { updateSelection: true })
|
|
|
|
|
inserted = true
|
|
|
|
|
})
|
|
|
|
|
return inserted
|
|
|
|
|
},
|
|
|
|
|
}
|
|
|
|
|
targetRef.current = target
|
|
|
|
|
const unregister = registerPlainTextPasteTarget(target)
|
|
|
|
|
|
|
|
|
|
return () => {
|
|
|
|
|
unregister()
|
|
|
|
|
if (targetRef.current === target) {
|
|
|
|
|
targetRef.current = null
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}, [containerRef, editable, editor, runEditorAction])
|
|
|
|
|
|
|
|
|
|
return useCallback(() => {
|
|
|
|
|
if (targetRef.current) {
|
|
|
|
|
activatePlainTextPasteTarget(targetRef.current)
|
|
|
|
|
}
|
|
|
|
|
}, [])
|
|
|
|
|
}
|
|
|
|
|
|
refactor: decompose Editor.tsx, NoteList.tsx, Sidebar.tsx — CodeScene hotspots (#126)
* refactor: decompose Editor.tsx into focused subcomponents and hooks
Extract from the monolithic Editor component (cc=35, 213 LoC, score 7.82):
- useDiffMode hook: diff state + toggle/commit-diff handlers
- useEditorFocus hook: new-note focus event listener
- editorSchema: WikiLink spec + BlockNote schema (module-level)
- SingleEditorView: BlockNote editor + suggestion menus
- EditorContent: breadcrumb bar + diff/editor/loading views
- EditorRightPanel: AI chat / Inspector panel switching
- suggestionEnrichment util: shared enrichment logic (eliminates duplication)
Editor.tsx is now 10.0 code health (was 7.82). All 25 existing tests pass.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* refactor: extract standalone functions from NoteList and Sidebar to reduce cc
NoteList: extract createNoteStatusResolver and toggleSetMember from
NoteListInner (cc 13→8, score 9.04→9.38).
Sidebar: extract applyCustomization from Sidebar (cc 10→7, score 9.09→10.0).
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* test: add tests for useDiffMode, useEditorFocus, and suggestionEnrichment
Cover extracted hook/utility logic: diff toggle/reset, editor focus event
listener with adaptive timing, and suggestion item enrichment pipeline.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* docs: update architecture for editor decomposition and write .claude-done
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2026-02-27 15:27:25 +01:00
|
|
|
/** Single BlockNote editor view — content is swapped via replaceBlocks */
|
2026-04-30 05:49:40 +02:00
|
|
|
export function SingleEditorView({ editor, entries, onNavigateWikilink, onChange, vaultPath, editable = true, locale = 'en' }: {
|
refactor: decompose Editor.tsx, NoteList.tsx, Sidebar.tsx — CodeScene hotspots (#126)
* refactor: decompose Editor.tsx into focused subcomponents and hooks
Extract from the monolithic Editor component (cc=35, 213 LoC, score 7.82):
- useDiffMode hook: diff state + toggle/commit-diff handlers
- useEditorFocus hook: new-note focus event listener
- editorSchema: WikiLink spec + BlockNote schema (module-level)
- SingleEditorView: BlockNote editor + suggestion menus
- EditorContent: breadcrumb bar + diff/editor/loading views
- EditorRightPanel: AI chat / Inspector panel switching
- suggestionEnrichment util: shared enrichment logic (eliminates duplication)
Editor.tsx is now 10.0 code health (was 7.82). All 25 existing tests pass.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* refactor: extract standalone functions from NoteList and Sidebar to reduce cc
NoteList: extract createNoteStatusResolver and toggleSetMember from
NoteListInner (cc 13→8, score 9.04→9.38).
Sidebar: extract applyCustomization from Sidebar (cc 10→7, score 9.09→10.0).
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* test: add tests for useDiffMode, useEditorFocus, and suggestionEnrichment
Cover extracted hook/utility logic: diff toggle/reset, editor focus event
listener with adaptive timing, and suggestion item enrichment pipeline.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* docs: update architecture for editor decomposition and write .claude-done
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2026-02-27 15:27:25 +01:00
|
|
|
editor: ReturnType<typeof useCreateBlockNote>
|
|
|
|
|
entries: VaultEntry[]
|
|
|
|
|
onNavigateWikilink: (target: string) => void
|
|
|
|
|
onChange?: () => void
|
2026-02-28 21:31:47 +01:00
|
|
|
vaultPath?: string
|
2026-03-03 01:37:25 +01:00
|
|
|
editable?: boolean
|
2026-04-30 05:49:40 +02:00
|
|
|
locale?: AppLocale
|
refactor: decompose Editor.tsx, NoteList.tsx, Sidebar.tsx — CodeScene hotspots (#126)
* refactor: decompose Editor.tsx into focused subcomponents and hooks
Extract from the monolithic Editor component (cc=35, 213 LoC, score 7.82):
- useDiffMode hook: diff state + toggle/commit-diff handlers
- useEditorFocus hook: new-note focus event listener
- editorSchema: WikiLink spec + BlockNote schema (module-level)
- SingleEditorView: BlockNote editor + suggestion menus
- EditorContent: breadcrumb bar + diff/editor/loading views
- EditorRightPanel: AI chat / Inspector panel switching
- suggestionEnrichment util: shared enrichment logic (eliminates duplication)
Editor.tsx is now 10.0 code health (was 7.82). All 25 existing tests pass.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* refactor: extract standalone functions from NoteList and Sidebar to reduce cc
NoteList: extract createNoteStatusResolver and toggleSetMember from
NoteListInner (cc 13→8, score 9.04→9.38).
Sidebar: extract applyCustomization from Sidebar (cc 10→7, score 9.09→10.0).
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* test: add tests for useDiffMode, useEditorFocus, and suggestionEnrichment
Cover extracted hook/utility logic: diff toggle/reset, editor focus event
listener with adaptive timing, and suggestion item enrichment pipeline.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* docs: update architecture for editor decomposition and write .claude-done
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2026-02-27 15:27:25 +01:00
|
|
|
}) {
|
|
|
|
|
const { cssVars } = useEditorTheme()
|
2026-04-24 22:26:07 +02:00
|
|
|
const themeMode = useDocumentThemeMode()
|
refactor: decompose Editor.tsx, NoteList.tsx, Sidebar.tsx — CodeScene hotspots (#126)
* refactor: decompose Editor.tsx into focused subcomponents and hooks
Extract from the monolithic Editor component (cc=35, 213 LoC, score 7.82):
- useDiffMode hook: diff state + toggle/commit-diff handlers
- useEditorFocus hook: new-note focus event listener
- editorSchema: WikiLink spec + BlockNote schema (module-level)
- SingleEditorView: BlockNote editor + suggestion menus
- EditorContent: breadcrumb bar + diff/editor/loading views
- EditorRightPanel: AI chat / Inspector panel switching
- suggestionEnrichment util: shared enrichment logic (eliminates duplication)
Editor.tsx is now 10.0 code health (was 7.82). All 25 existing tests pass.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* refactor: extract standalone functions from NoteList and Sidebar to reduce cc
NoteList: extract createNoteStatusResolver and toggleSetMember from
NoteListInner (cc 13→8, score 9.04→9.38).
Sidebar: extract applyCustomization from Sidebar (cc 10→7, score 9.09→10.0).
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* test: add tests for useDiffMode, useEditorFocus, and suggestionEnrichment
Cover extracted hook/utility logic: diff toggle/reset, editor focus event
listener with adaptive timing, and suggestion item enrichment pipeline.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* docs: update architecture for editor decomposition and write .claude-done
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2026-02-27 15:27:25 +01:00
|
|
|
const containerRef = useRef<HTMLDivElement>(null)
|
2026-04-20 18:35:50 +02:00
|
|
|
const handleContainerClick = useEditorContainerClickHandler({ editable, editor })
|
2026-04-24 02:50:12 +02:00
|
|
|
const handleEditorChange = useCompositionAwareEditorChange({ containerRef, onChange })
|
2026-02-28 21:31:47 +01:00
|
|
|
const onImageUrl = useInsertImageCallback(editor)
|
|
|
|
|
const { isDragOver } = useImageDrop({ containerRef, onImageUrl, vaultPath })
|
2026-04-30 05:49:40 +02:00
|
|
|
const lightbox = useImageLightbox({ containerRef })
|
2026-05-03 11:34:56 +02:00
|
|
|
const {
|
|
|
|
|
clearCopyTarget,
|
|
|
|
|
copyTarget,
|
|
|
|
|
handleFocus: handleCodeBlockCopyFocus,
|
|
|
|
|
handleMouseMove: handleCodeBlockCopyMouseMove,
|
|
|
|
|
} = useCodeBlockCopyTarget(containerRef)
|
2026-04-15 19:42:25 +02:00
|
|
|
useBlockNoteSideMenuHoverGuard(containerRef)
|
2026-04-08 19:06:21 +02:00
|
|
|
useEditorLinkActivation(containerRef, onNavigateWikilink)
|
refactor: decompose Editor.tsx, NoteList.tsx, Sidebar.tsx — CodeScene hotspots (#126)
* refactor: decompose Editor.tsx into focused subcomponents and hooks
Extract from the monolithic Editor component (cc=35, 213 LoC, score 7.82):
- useDiffMode hook: diff state + toggle/commit-diff handlers
- useEditorFocus hook: new-note focus event listener
- editorSchema: WikiLink spec + BlockNote schema (module-level)
- SingleEditorView: BlockNote editor + suggestion menus
- EditorContent: breadcrumb bar + diff/editor/loading views
- EditorRightPanel: AI chat / Inspector panel switching
- suggestionEnrichment util: shared enrichment logic (eliminates duplication)
Editor.tsx is now 10.0 code health (was 7.82). All 25 existing tests pass.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* refactor: extract standalone functions from NoteList and Sidebar to reduce cc
NoteList: extract createNoteStatusResolver and toggleSetMember from
NoteListInner (cc 13→8, score 9.04→9.38).
Sidebar: extract applyCustomization from Sidebar (cc 10→7, score 9.09→10.0).
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* test: add tests for useDiffMode, useEditorFocus, and suggestionEnrichment
Cover extracted hook/utility logic: diff toggle/reset, editor focus event
listener with adaptive timing, and suggestion item enrichment pipeline.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* docs: update architecture for editor decomposition and write .claude-done
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2026-02-27 15:27:25 +01:00
|
|
|
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
_wikilinkEntriesRef.current = entries
|
|
|
|
|
}, [entries])
|
|
|
|
|
|
2026-04-26 03:01:10 +02:00
|
|
|
useEffect(() => {
|
|
|
|
|
const container = containerRef.current
|
|
|
|
|
if (!container) return
|
|
|
|
|
return observeNativeTextAssistanceDisabled(container)
|
|
|
|
|
}, [])
|
|
|
|
|
|
2026-04-12 11:45:59 +02:00
|
|
|
useSeedBlockNoteTableBridge(editor)
|
|
|
|
|
|
refactor: decompose Editor.tsx, NoteList.tsx, Sidebar.tsx — CodeScene hotspots (#126)
* refactor: decompose Editor.tsx into focused subcomponents and hooks
Extract from the monolithic Editor component (cc=35, 213 LoC, score 7.82):
- useDiffMode hook: diff state + toggle/commit-diff handlers
- useEditorFocus hook: new-note focus event listener
- editorSchema: WikiLink spec + BlockNote schema (module-level)
- SingleEditorView: BlockNote editor + suggestion menus
- EditorContent: breadcrumb bar + diff/editor/loading views
- EditorRightPanel: AI chat / Inspector panel switching
- suggestionEnrichment util: shared enrichment logic (eliminates duplication)
Editor.tsx is now 10.0 code health (was 7.82). All 25 existing tests pass.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* refactor: extract standalone functions from NoteList and Sidebar to reduce cc
NoteList: extract createNoteStatusResolver and toggleSetMember from
NoteListInner (cc 13→8, score 9.04→9.38).
Sidebar: extract applyCustomization from Sidebar (cc 10→7, score 9.09→10.0).
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* test: add tests for useDiffMode, useEditorFocus, and suggestionEnrichment
Cover extracted hook/utility logic: diff toggle/reset, editor focus event
listener with adaptive timing, and suggestion item enrichment pipeline.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* docs: update architecture for editor decomposition and write .claude-done
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2026-02-27 15:27:25 +01:00
|
|
|
const typeEntryMap = useMemo(() => buildTypeEntryMap(entries), [entries])
|
2026-04-20 18:35:50 +02:00
|
|
|
const baseItems = useMemo(() => buildBaseSuggestionItems(entries), [entries])
|
2026-04-28 23:29:09 +02:00
|
|
|
const runEditorAction = useCallback((action: SuggestionAction) => {
|
|
|
|
|
runSuggestionActionSafely({
|
|
|
|
|
action,
|
|
|
|
|
container: containerRef.current,
|
|
|
|
|
editor,
|
|
|
|
|
})
|
|
|
|
|
}, [editor])
|
2026-05-01 00:31:14 +02:00
|
|
|
const activatePlainTextPaste = useRichEditorPlainTextPasteTarget({
|
|
|
|
|
containerRef,
|
|
|
|
|
editable,
|
|
|
|
|
editor,
|
|
|
|
|
runEditorAction,
|
|
|
|
|
})
|
2026-05-03 11:34:56 +02:00
|
|
|
const handleFocusCapture = useCallback((event: React.FocusEvent<HTMLDivElement>) => {
|
|
|
|
|
activatePlainTextPaste()
|
|
|
|
|
handleCodeBlockCopyFocus(event)
|
|
|
|
|
}, [activatePlainTextPaste, handleCodeBlockCopyFocus])
|
2026-04-28 23:29:09 +02:00
|
|
|
const insertWikilink = useInsertWikilink(editor, runEditorAction)
|
2026-05-03 11:34:56 +02:00
|
|
|
const suggestionMenuItems = useSuggestionMenuItems({
|
2026-04-20 18:35:50 +02:00
|
|
|
baseItems,
|
|
|
|
|
editor,
|
|
|
|
|
insertWikilink,
|
2026-04-28 23:29:09 +02:00
|
|
|
runEditorAction,
|
2026-04-20 18:35:50 +02:00
|
|
|
typeEntryMap,
|
|
|
|
|
vaultPath,
|
|
|
|
|
})
|
2026-04-15 22:42:40 +02:00
|
|
|
|
refactor: decompose Editor.tsx, NoteList.tsx, Sidebar.tsx — CodeScene hotspots (#126)
* refactor: decompose Editor.tsx into focused subcomponents and hooks
Extract from the monolithic Editor component (cc=35, 213 LoC, score 7.82):
- useDiffMode hook: diff state + toggle/commit-diff handlers
- useEditorFocus hook: new-note focus event listener
- editorSchema: WikiLink spec + BlockNote schema (module-level)
- SingleEditorView: BlockNote editor + suggestion menus
- EditorContent: breadcrumb bar + diff/editor/loading views
- EditorRightPanel: AI chat / Inspector panel switching
- suggestionEnrichment util: shared enrichment logic (eliminates duplication)
Editor.tsx is now 10.0 code health (was 7.82). All 25 existing tests pass.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* refactor: extract standalone functions from NoteList and Sidebar to reduce cc
NoteList: extract createNoteStatusResolver and toggleSetMember from
NoteListInner (cc 13→8, score 9.04→9.38).
Sidebar: extract applyCustomization from Sidebar (cc 10→7, score 9.09→10.0).
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* test: add tests for useDiffMode, useEditorFocus, and suggestionEnrichment
Cover extracted hook/utility logic: diff toggle/reset, editor focus event
listener with adaptive timing, and suggestion item enrichment pipeline.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* docs: update architecture for editor decomposition and write .claude-done
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2026-02-27 15:27:25 +01:00
|
|
|
return (
|
2026-05-01 00:31:14 +02:00
|
|
|
<div
|
|
|
|
|
ref={containerRef}
|
|
|
|
|
className={`editor__blocknote-container${isDragOver ? ' editor__blocknote-container--drag-over' : ''}`}
|
|
|
|
|
style={cssVars as React.CSSProperties}
|
|
|
|
|
onClick={handleContainerClick}
|
|
|
|
|
onCopyCapture={handleCodeBlockCopy}
|
2026-05-03 11:34:56 +02:00
|
|
|
onFocusCapture={handleFocusCapture}
|
|
|
|
|
onMouseLeave={clearCopyTarget}
|
2026-05-01 00:31:14 +02:00
|
|
|
onMouseDownCapture={activatePlainTextPaste}
|
2026-05-03 11:34:56 +02:00
|
|
|
onMouseMove={handleCodeBlockCopyMouseMove}
|
2026-05-01 00:31:14 +02:00
|
|
|
>
|
refactor: decompose Editor.tsx, NoteList.tsx, Sidebar.tsx — CodeScene hotspots (#126)
* refactor: decompose Editor.tsx into focused subcomponents and hooks
Extract from the monolithic Editor component (cc=35, 213 LoC, score 7.82):
- useDiffMode hook: diff state + toggle/commit-diff handlers
- useEditorFocus hook: new-note focus event listener
- editorSchema: WikiLink spec + BlockNote schema (module-level)
- SingleEditorView: BlockNote editor + suggestion menus
- EditorContent: breadcrumb bar + diff/editor/loading views
- EditorRightPanel: AI chat / Inspector panel switching
- suggestionEnrichment util: shared enrichment logic (eliminates duplication)
Editor.tsx is now 10.0 code health (was 7.82). All 25 existing tests pass.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* refactor: extract standalone functions from NoteList and Sidebar to reduce cc
NoteList: extract createNoteStatusResolver and toggleSetMember from
NoteListInner (cc 13→8, score 9.04→9.38).
Sidebar: extract applyCustomization from Sidebar (cc 10→7, score 9.09→10.0).
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* test: add tests for useDiffMode, useEditorFocus, and suggestionEnrichment
Cover extracted hook/utility logic: diff toggle/reset, editor focus event
listener with adaptive timing, and suggestion item enrichment pipeline.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* docs: update architecture for editor decomposition and write .claude-done
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2026-02-27 15:27:25 +01:00
|
|
|
{isDragOver && (
|
|
|
|
|
<div className="editor__drop-overlay">
|
|
|
|
|
<div className="editor__drop-overlay-label">Drop image here</div>
|
|
|
|
|
</div>
|
|
|
|
|
)}
|
2026-04-13 22:45:25 +02:00
|
|
|
<SharedContextBlockNoteView
|
refactor: decompose Editor.tsx, NoteList.tsx, Sidebar.tsx — CodeScene hotspots (#126)
* refactor: decompose Editor.tsx into focused subcomponents and hooks
Extract from the monolithic Editor component (cc=35, 213 LoC, score 7.82):
- useDiffMode hook: diff state + toggle/commit-diff handlers
- useEditorFocus hook: new-note focus event listener
- editorSchema: WikiLink spec + BlockNote schema (module-level)
- SingleEditorView: BlockNote editor + suggestion menus
- EditorContent: breadcrumb bar + diff/editor/loading views
- EditorRightPanel: AI chat / Inspector panel switching
- suggestionEnrichment util: shared enrichment logic (eliminates duplication)
Editor.tsx is now 10.0 code health (was 7.82). All 25 existing tests pass.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* refactor: extract standalone functions from NoteList and Sidebar to reduce cc
NoteList: extract createNoteStatusResolver and toggleSetMember from
NoteListInner (cc 13→8, score 9.04→9.38).
Sidebar: extract applyCustomization from Sidebar (cc 10→7, score 9.09→10.0).
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* test: add tests for useDiffMode, useEditorFocus, and suggestionEnrichment
Cover extracted hook/utility logic: diff toggle/reset, editor focus event
listener with adaptive timing, and suggestion item enrichment pipeline.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* docs: update architecture for editor decomposition and write .claude-done
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2026-02-27 15:27:25 +01:00
|
|
|
editor={editor}
|
2026-04-24 22:26:07 +02:00
|
|
|
theme={themeMode}
|
2026-04-24 02:50:12 +02:00
|
|
|
onChange={handleEditorChange}
|
2026-03-03 01:37:25 +01:00
|
|
|
editable={editable}
|
2026-04-15 22:42:40 +02:00
|
|
|
formattingToolbar={false}
|
2026-04-24 23:25:22 +02:00
|
|
|
linkToolbar={false}
|
2026-04-15 22:42:40 +02:00
|
|
|
slashMenu={false}
|
2026-04-17 17:48:12 +02:00
|
|
|
sideMenu={false}
|
refactor: decompose Editor.tsx, NoteList.tsx, Sidebar.tsx — CodeScene hotspots (#126)
* refactor: decompose Editor.tsx into focused subcomponents and hooks
Extract from the monolithic Editor component (cc=35, 213 LoC, score 7.82):
- useDiffMode hook: diff state + toggle/commit-diff handlers
- useEditorFocus hook: new-note focus event listener
- editorSchema: WikiLink spec + BlockNote schema (module-level)
- SingleEditorView: BlockNote editor + suggestion menus
- EditorContent: breadcrumb bar + diff/editor/loading views
- EditorRightPanel: AI chat / Inspector panel switching
- suggestionEnrichment util: shared enrichment logic (eliminates duplication)
Editor.tsx is now 10.0 code health (was 7.82). All 25 existing tests pass.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* refactor: extract standalone functions from NoteList and Sidebar to reduce cc
NoteList: extract createNoteStatusResolver and toggleSetMember from
NoteListInner (cc 13→8, score 9.04→9.38).
Sidebar: extract applyCustomization from Sidebar (cc 10→7, score 9.09→10.0).
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* test: add tests for useDiffMode, useEditorFocus, and suggestionEnrichment
Cover extracted hook/utility logic: diff toggle/reset, editor focus event
listener with adaptive timing, and suggestion item enrichment pipeline.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* docs: update architecture for editor decomposition and write .claude-done
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2026-02-27 15:27:25 +01:00
|
|
|
>
|
2026-05-03 11:34:56 +02:00
|
|
|
<EditorInteractionControllers
|
|
|
|
|
{...suggestionMenuItems}
|
|
|
|
|
runEditorAction={runEditorAction}
|
refactor: decompose Editor.tsx, NoteList.tsx, Sidebar.tsx — CodeScene hotspots (#126)
* refactor: decompose Editor.tsx into focused subcomponents and hooks
Extract from the monolithic Editor component (cc=35, 213 LoC, score 7.82):
- useDiffMode hook: diff state + toggle/commit-diff handlers
- useEditorFocus hook: new-note focus event listener
- editorSchema: WikiLink spec + BlockNote schema (module-level)
- SingleEditorView: BlockNote editor + suggestion menus
- EditorContent: breadcrumb bar + diff/editor/loading views
- EditorRightPanel: AI chat / Inspector panel switching
- suggestionEnrichment util: shared enrichment logic (eliminates duplication)
Editor.tsx is now 10.0 code health (was 7.82). All 25 existing tests pass.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* refactor: extract standalone functions from NoteList and Sidebar to reduce cc
NoteList: extract createNoteStatusResolver and toggleSetMember from
NoteListInner (cc 13→8, score 9.04→9.38).
Sidebar: extract applyCustomization from Sidebar (cc 10→7, score 9.09→10.0).
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* test: add tests for useDiffMode, useEditorFocus, and suggestionEnrichment
Cover extracted hook/utility logic: diff toggle/reset, editor focus event
listener with adaptive timing, and suggestion item enrichment pipeline.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* docs: update architecture for editor decomposition and write .claude-done
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2026-02-27 15:27:25 +01:00
|
|
|
/>
|
2026-04-13 22:45:25 +02:00
|
|
|
</SharedContextBlockNoteView>
|
2026-05-03 11:34:56 +02:00
|
|
|
{copyTarget && <CodeBlockCopyButton copyTarget={copyTarget} locale={locale} />}
|
2026-04-30 05:49:40 +02:00
|
|
|
<ImageLightbox image={lightbox.image} locale={locale} onClose={lightbox.close} />
|
refactor: decompose Editor.tsx, NoteList.tsx, Sidebar.tsx — CodeScene hotspots (#126)
* refactor: decompose Editor.tsx into focused subcomponents and hooks
Extract from the monolithic Editor component (cc=35, 213 LoC, score 7.82):
- useDiffMode hook: diff state + toggle/commit-diff handlers
- useEditorFocus hook: new-note focus event listener
- editorSchema: WikiLink spec + BlockNote schema (module-level)
- SingleEditorView: BlockNote editor + suggestion menus
- EditorContent: breadcrumb bar + diff/editor/loading views
- EditorRightPanel: AI chat / Inspector panel switching
- suggestionEnrichment util: shared enrichment logic (eliminates duplication)
Editor.tsx is now 10.0 code health (was 7.82). All 25 existing tests pass.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* refactor: extract standalone functions from NoteList and Sidebar to reduce cc
NoteList: extract createNoteStatusResolver and toggleSetMember from
NoteListInner (cc 13→8, score 9.04→9.38).
Sidebar: extract applyCustomization from Sidebar (cc 10→7, score 9.09→10.0).
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* test: add tests for useDiffMode, useEditorFocus, and suggestionEnrichment
Cover extracted hook/utility logic: diff toggle/reset, editor focus event
listener with adaptive timing, and suggestion item enrichment pipeline.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* docs: update architecture for editor decomposition and write .claude-done
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2026-02-27 15:27:25 +01:00
|
|
|
</div>
|
|
|
|
|
)
|
|
|
|
|
}
|