From 10135463b1060fbbd79c17ab77846a9ea2161b41 Mon Sep 17 00:00:00 2001 From: Hiren Kangad Date: Fri, 13 Mar 2026 09:27:03 +0530 Subject: [PATCH] feat(markdown): wire MarkdownOverlay into canvas lifecycle with React portals --- frontend/src/hooks/useCanvasSetup.ts | 20 +++++++++- frontend/src/pages/Editor.tsx | 56 +++++++++++++++++++++++++++- 2 files changed, 73 insertions(+), 3 deletions(-) diff --git a/frontend/src/hooks/useCanvasSetup.ts b/frontend/src/hooks/useCanvasSetup.ts index 305120c..d83fbb0 100644 --- a/frontend/src/hooks/useCanvasSetup.ts +++ b/frontend/src/hooks/useCanvasSetup.ts @@ -13,6 +13,7 @@ import { UploadManager } from '../stores/uploadManager'; import { AnnotationStore } from '../stores/annotationStore'; import { PinOverlay } from '../canvas/PinOverlay'; import { CropOverlay } from '../canvas/CropOverlay'; +import { MarkdownOverlay } from '../canvas/MarkdownOverlay'; import { TextSprite } from '../canvas/sprites/TextSprite'; import { TextSharpnessManager } from '../canvas/textSharpness'; // PresenceOverlay removed — remote selection highlighting was too heavy for minimal benefit @@ -78,6 +79,7 @@ export function useCanvasSetup(deps: CanvasSetupDeps) { const textEditorRef = useRef(null); if (!textEditorRef.current) textEditorRef.current = new TextEditor(); const cropOverlayRef = useRef(null); + const mdOverlayRef = useRef(null); useEffect(() => { if (!boardData || !resolvedBoardId) return; @@ -177,6 +179,13 @@ export function useCanvasSetup(deps: CanvasSetupDeps) { sharpness.destroy(); }; + // ── Markdown overlay — DOM divs positioned over canvas for each markdown card ── + if (domContainer) { + const mdOverlay = new MarkdownOverlay(viewport, scene, domContainer); + mdOverlayRef.current = mdOverlay; + mdOverlay.refreshAll(); + } + // Create InboxZone and add to viewport const inboxZone = new InboxZone(scene.textures, scene.springs); viewport.addChild(inboxZone); @@ -198,14 +207,19 @@ export function useCanvasSetup(deps: CanvasSetupDeps) { selection.onItemsTransform = (items) => { syncRef.current?.broadcastTransform(items); pinOverlayRef.current?.updatePositions(); + for (const item of items) { + if (item.type === 'markdown') mdOverlayRef.current?.updateItem(item.id); + } }; selection.onItemTransform = (item) => { syncRef.current?.broadcastTransform(item); pinOverlayRef.current?.updatePositions(); + if (item.type === 'markdown') mdOverlayRef.current?.updateItem(item.id); }; selection.transformBox.onItemTransform = (item) => { syncRef.current?.broadcastTransform(item); pinOverlayRef.current?.updatePositions(); + if (item.type === 'markdown') mdOverlayRef.current?.updateItem(item.id); }; selection.onObjectDragEnd = (itemIds) => { onCanvasChange(itemIds); // broadcasts elements + saves + undo + spatial refresh @@ -483,6 +497,10 @@ export function useCanvasSetup(deps: CanvasSetupDeps) { dropCleanupRef.current?.(); pasteCleanupRef.current?.(); textEditorRef.current?.stopEditing(false); + if (mdOverlayRef.current) { + mdOverlayRef.current.destroy(); + mdOverlayRef.current = null; + } if (cropOverlayRef.current) { cropOverlayRef.current.destroy(); cropOverlayRef.current = null; @@ -499,5 +517,5 @@ export function useCanvasSetup(deps: CanvasSetupDeps) { }; }, [boardData, resolvedBoardId, user, isPublicView, onCanvasChange, showToast, canvasRef, selectionRef, undoRef, syncRef, inboxZoneRef, uploadManager, setOnlineUsers, setSelectedLayerIds]); - return { annotationStore: annotationStoreRef.current, pinOverlay: pinOverlayRef.current, textEditor: textEditorRef.current, cropOverlayRef }; + return { annotationStore: annotationStoreRef.current, pinOverlay: pinOverlayRef.current, textEditor: textEditorRef.current, cropOverlayRef, mdOverlay: mdOverlayRef.current }; } diff --git a/frontend/src/pages/Editor.tsx b/frontend/src/pages/Editor.tsx index 99ee353..91ed6a3 100644 --- a/frontend/src/pages/Editor.tsx +++ b/frontend/src/pages/Editor.tsx @@ -36,11 +36,13 @@ import { InboxZone } from '../canvas/InboxZone'; import { getItemWorldBounds } from '../canvas/SceneManager'; import { getPointAnchorWorld } from '../canvas/reviewAnchors'; import { resolveReviewTargetAtPoint } from '../canvas/reviewTargeting'; -import type { TextObject, StickyObject } from '../canvas/scene-format'; +import type { TextObject, StickyObject, MarkdownObject } from '../canvas/scene-format'; import { VideoSprite } from '../canvas/sprites/VideoSprite'; import { TextSprite } from '../canvas/sprites/TextSprite'; import { StickySprite } from '../canvas/sprites/StickySprite'; import * as ops from '../canvas/operations'; +import ReactDOM from 'react-dom'; +import MarkdownReadView from '../components/MarkdownReadView'; // Hooks import { useBoardLoader } from '../hooks/useBoardLoader'; @@ -197,12 +199,38 @@ export default function Editor({ isPublicView }: EditorProps) { }); // Canvas setup (selection, undo, sync, socket, drag/drop, paste, inbox, annotations) - const { annotationStore, pinOverlay, textEditor, cropOverlayRef } = useCanvasSetup({ + const { annotationStore, pinOverlay, textEditor, cropOverlayRef, mdOverlay } = useCanvasSetup({ boardData, resolvedBoardId, user, isPublicView, canvasRef, selectionRef, undoRef, syncRef, inboxZoneRef, canvasContainerRef, uploadManager, onCanvasChange, showToast, setOnlineUsers, setSelectedLayerIds, }); + // ── Markdown overlay — sync visible card IDs for React portal rendering ── + const [mdCardIds, setMdCardIds] = useState([]); + + useEffect(() => { + if (!mdOverlay) return; + + const syncIds = () => { + const scene = canvasRef.current?.getScene(); + if (!scene) return; + const ids: string[] = []; + for (const [id, item] of scene.items) { + if (item.type === 'markdown' && item.data.visible) { + ids.push(id); + } + } + setMdCardIds(prev => { + if (prev.length === ids.length && prev.every((v, i) => v === ids[i])) return prev; + return ids; + }); + }; + + mdOverlay.onChange = syncIds; + syncIds(); + return () => { mdOverlay.onChange = null; }; + }, [mdOverlay]); + // Build canvas objects map for FeedbackPanel (memoized on object count changes) const canvasObjectMap = React.useMemo(() => { const scene = canvasRef.current?.getScene(); @@ -1179,6 +1207,30 @@ export default function Editor({ isPublicView }: EditorProps) { /> )} + {/* Markdown card portals */} + {mdCardIds.map(id => { + const mountPoint = mdOverlay?.getMountPoint(id); + const item = canvasRef.current?.getScene()?.getById(id); + if (!mountPoint || !item || item.type !== 'markdown') return null; + const data = item.data as MarkdownObject; + return ReactDOM.createPortal( + { + data.content = newContent; + onCanvasChange([id]); + mdOverlay?.measureHeight(id); + }} + />, + mountPoint, + ); + })} + {/* Export dialog */} {showExport && (