From 185feb5a2cbe600d0216b4209d20fa0d2ec551b8 Mon Sep 17 00:00:00 2001 From: Test Date: Mon, 2 Mar 2026 18:38:25 +0100 Subject: [PATCH] =?UTF-8?q?feat:=20raw=20editor=20mode=20=E2=80=94=20plain?= =?UTF-8?q?=20textarea=20with=20frontmatter=20+=20wikilink=20autocomplete?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds a third editor mode (alongside WYSIWYG and Diff) that shows the raw markdown file in a monospaced textarea. Includes: - useRawMode hook with derived state (no setState-in-effect) and onFlushPending - RawEditorView: textarea with 500ms debounce, YAML error banner, wikilink autocomplete via [[ trigger, Cmd+S save - BreadcrumbBar Code icon toggles raw mode; mutual exclusion with diff mode - Command palette: "Toggle Raw Editor" (View group, requires active tab) - useEditorModeExclusion hook extracted to keep Editor.tsx under complexity threshold - buildViewCommands extracted from useCommandRegistry for same reason - RawToggleButton and RawModeEditorSection components extracted for clean CC Co-Authored-By: Claude Sonnet 4.6 --- design/raw-editor-mode.pen | 1 + src/App.tsx | 6 + src/components/BreadcrumbBar.test.tsx | 21 ++ src/components/BreadcrumbBar.tsx | 20 ++ src/components/Editor.tsx | 46 ++++- src/components/EditorContent.tsx | 46 ++++- src/components/RawEditorView.test.tsx | 143 +++++++++++++ src/components/RawEditorView.tsx | 276 ++++++++++++++++++++++++++ src/hooks/useAppCommands.ts | 2 + src/hooks/useCommandRegistry.test.ts | 30 +++ src/hooks/useCommandRegistry.ts | 38 +++- src/hooks/useRawMode.test.ts | 87 ++++++++ src/hooks/useRawMode.ts | 29 +++ src/utils/rawEditorUtils.ts | 21 ++ 14 files changed, 750 insertions(+), 16 deletions(-) create mode 100644 design/raw-editor-mode.pen create mode 100644 src/components/RawEditorView.test.tsx create mode 100644 src/components/RawEditorView.tsx create mode 100644 src/hooks/useRawMode.test.ts create mode 100644 src/hooks/useRawMode.ts create mode 100644 src/utils/rawEditorUtils.ts diff --git a/design/raw-editor-mode.pen b/design/raw-editor-mode.pen new file mode 100644 index 00000000..747b5ab3 --- /dev/null +++ b/design/raw-editor-mode.pen @@ -0,0 +1 @@ +{"children":[],"variables":{}} diff --git a/src/App.tsx b/src/App.tsx index d3ce21de..8408591b 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -270,6 +270,9 @@ function App() { const bulkActions = useBulkActions(entryActions, setToastMessage) + // Raw-toggle ref: Editor registers its handleToggleRaw here so the command palette can call it + const rawToggleRef = useRef<() => void>(() => {}) + const { setViewMode, sidebarVisible, noteListVisible } = useViewMode() const zoom = useZoom() const buildNumber = useBuildNumber() @@ -290,6 +293,7 @@ function App() { onArchiveNote: entryActions.handleArchiveNote, onUnarchiveNote: entryActions.handleUnarchiveNote, onCommitPush: commitFlow.openCommitDialog, onSetViewMode: setViewMode, onToggleInspector: () => layout.setInspectorCollapsed(c => !c), + onToggleRawEditor: () => rawToggleRef.current(), onZoomIn: zoom.zoomIn, onZoomOut: zoom.zoomOut, onZoomReset: zoom.zoomReset, zoomLevel: zoom.zoomLevel, onSelect: setSelection, onCloseTab: notes.handleCloseTab, @@ -389,7 +393,9 @@ function App() { onUnarchiveNote={entryActions.handleUnarchiveNote} onRenameTab={handleRenameTab} onContentChange={handleContentChange} + onSave={handleSave} onTitleSync={handleTitleSync} + rawToggleRef={rawToggleRef} canGoBack={navHistory.canGoBack} canGoForward={navHistory.canGoForward} onGoBack={handleGoBack} diff --git a/src/components/BreadcrumbBar.test.tsx b/src/components/BreadcrumbBar.test.tsx index 85fcb7f8..9e867c54 100644 --- a/src/components/BreadcrumbBar.test.tsx +++ b/src/components/BreadcrumbBar.test.tsx @@ -115,3 +115,24 @@ describe('BreadcrumbBar — archive/unarchive', () => { expect(onUnarchive).toHaveBeenCalledOnce() }) }) + +describe('BreadcrumbBar — raw editor toggle', () => { + it('shows Raw editor button with tooltip "Raw editor" when rawMode is off', () => { + const onToggleRaw = vi.fn() + render() + expect(screen.getByTitle('Raw editor')).toBeInTheDocument() + }) + + it('shows "Back to editor" tooltip when rawMode is on', () => { + const onToggleRaw = vi.fn() + render() + expect(screen.getByTitle('Back to editor')).toBeInTheDocument() + }) + + it('calls onToggleRaw when raw button is clicked', () => { + const onToggleRaw = vi.fn() + render() + fireEvent.click(screen.getByTitle('Raw editor')) + expect(onToggleRaw).toHaveBeenCalledOnce() + }) +}) diff --git a/src/components/BreadcrumbBar.tsx b/src/components/BreadcrumbBar.tsx index 80b63135..ee3af859 100644 --- a/src/components/BreadcrumbBar.tsx +++ b/src/components/BreadcrumbBar.tsx @@ -4,6 +4,7 @@ import { cn } from '@/lib/utils' import { MagnifyingGlass, GitBranch, + Code, CursorText, Sparkle, SlidersHorizontal, @@ -22,6 +23,8 @@ interface BreadcrumbBarProps { diffMode: boolean diffLoading: boolean onToggleDiff: () => void + rawMode?: boolean + onToggleRaw?: () => void showAIChat?: boolean onToggleAIChat?: () => void inspectorCollapsed?: boolean @@ -34,7 +37,23 @@ interface BreadcrumbBarProps { const DISABLED_ICON_STYLE = { opacity: 0.4, cursor: 'not-allowed' } as const +function RawToggleButton({ rawMode, onToggleRaw }: { rawMode?: boolean; onToggleRaw?: () => void }) { + return ( + + ) +} + function BreadcrumbActions({ entry, showDiffToggle, diffMode, diffLoading, onToggleDiff, + rawMode, onToggleRaw, showAIChat, onToggleAIChat, inspectorCollapsed, onToggleInspector, onTrash, onRestore, onArchive, onUnarchive, }: Omit) { @@ -68,6 +87,7 @@ function BreadcrumbActions({ entry, showDiffToggle, diffMode, diffLoading, onTog )} +