diff --git a/frontend/src/canvas/markdownStyles.ts b/frontend/src/canvas/markdownStyles.ts new file mode 100644 index 0000000..259c864 --- /dev/null +++ b/frontend/src/canvas/markdownStyles.ts @@ -0,0 +1,16 @@ +// Shared style constants + BlockNote dark-theme CSS string for injection + +export const MD_BLOCKNOTE_DARK_CSS = ` + .bn-container[data-theme="dark"] { + --bn-colors-editor-background: #232336; + --bn-colors-editor-text: #e0e0e0; + --bn-colors-menu-background: #2a2a42; + --bn-colors-menu-text: #e0e0e0; + --bn-colors-tooltip-background: #333; + --bn-colors-tooltip-text: #ccc; + --bn-colors-hovered-background: rgba(121,80,242,0.15); + --bn-colors-selected-background: rgba(121,80,242,0.25); + --bn-colors-side-menu: #666; + --bn-font-family: Inter, system-ui, sans-serif; + } +`; diff --git a/frontend/src/components/MarkdownEditView.tsx b/frontend/src/components/MarkdownEditView.tsx new file mode 100644 index 0000000..f505d07 --- /dev/null +++ b/frontend/src/components/MarkdownEditView.tsx @@ -0,0 +1,132 @@ +/** + * MarkdownEditView — BlockNote editor wrapper for markdown card edit mode. + * Lazy-loaded on first double-click via dynamic import(). + */ + +import React, { useCallback, useEffect, useRef } from 'react'; +import { useCreateBlockNote } from '@blocknote/react'; +import { BlockNoteViewRaw } from '@blocknote/react'; +import '@blocknote/react/style.css'; + +interface MarkdownEditViewProps { + initialContent: string; + accentColor: string; + onSave: (newContent: string) => void; + onCancel: () => void; +} + +// Markdown conversion is built into the BlockNote editor instance: +// - editor.tryParseMarkdownToBlocks(md) — markdown → blocks +// - editor.blocksToMarkdownLossy(blocks) — blocks → markdown +// No separate @blocknote/xl-markdown package needed. + +export default function MarkdownEditView(props: MarkdownEditViewProps) { + const { initialContent, accentColor, onSave, onCancel } = props; + const savedRef = useRef(false); + + const editor = useCreateBlockNote({ + domAttributes: { + editor: { + 'data-theme': 'dark', + }, + }, + }); + + // Initialize editor content from markdown + useEffect(() => { + const blocks = editor.tryParseMarkdownToBlocks(initialContent); + editor.replaceBlocks(editor.document, blocks); + }, []); // Only on mount + + const doSave = useCallback(() => { + if (savedRef.current) return; + savedRef.current = true; + const md = editor.blocksToMarkdownLossy(editor.document); + onSave(md); + }, [editor, onSave]); + + const doCancel = useCallback(() => { + if (savedRef.current) return; + savedRef.current = true; + onCancel(); + }, [onCancel]); + + // Ctrl+Enter to save, Escape to cancel + useEffect(() => { + const onKeyDown = (e: KeyboardEvent) => { + if (e.key === 'Enter' && (e.ctrlKey || e.metaKey)) { + e.preventDefault(); + doSave(); + } else if (e.key === 'Escape') { + e.preventDefault(); + doCancel(); + } + }; + document.addEventListener('keydown', onKeyDown, true); // capture phase + return () => document.removeEventListener('keydown', onKeyDown, true); + }, [doSave, doCancel]); + + // Click outside to save + useEffect(() => { + const onClick = (e: MouseEvent) => { + const target = e.target as HTMLElement; + if (!target.closest('[data-markdown-edit]')) { + doSave(); + } + }; + const timer = setTimeout(() => { + document.addEventListener('pointerdown', onClick); + }, 200); + return () => { + clearTimeout(timer); + document.removeEventListener('pointerdown', onClick); + }; + }, [doSave]); + + // Header with Done/Cancel controls + const headerStyle: React.CSSProperties = { + padding: '7px 14px', + background: '#2a2a42', + borderBottom: '1px solid #333', + display: 'flex', + alignItems: 'center', + justifyContent: 'space-between', + fontSize: '11px', + }; + + return ( +