From dfdfd07a1de305ef35def51c3db82123c6e7d38a Mon Sep 17 00:00:00 2001 From: Hiren Kangad Date: Fri, 13 Mar 2026 10:07:31 +0530 Subject: [PATCH] feat(markdown): add contextual toolbar for background color, accent color, and width presets --- .../src/components/MarkdownFormatToolbar.tsx | 160 ++++++++++++++++++ frontend/src/pages/Editor.tsx | 38 +++++ 2 files changed, 198 insertions(+) create mode 100644 frontend/src/components/MarkdownFormatToolbar.tsx diff --git a/frontend/src/components/MarkdownFormatToolbar.tsx b/frontend/src/components/MarkdownFormatToolbar.tsx new file mode 100644 index 0000000..d9ca1d0 --- /dev/null +++ b/frontend/src/components/MarkdownFormatToolbar.tsx @@ -0,0 +1,160 @@ +/** + * MarkdownFormatToolbar — contextual toolbar for selected markdown cards. + * Shows background color picker, width presets, and accent color picker. + */ + +import React, { useState, useEffect } from 'react'; + +interface MarkdownFormatToolbarProps { + x: number; + y: number; + bgColor: string; + accentColor: string; + width: number; + onBgColorChange: (color: string) => void; + onAccentColorChange: (color: string) => void; + onWidthChange: (width: number) => void; +} + +const PRESET_COLORS = [ + '#232336', '#1e1e2e', '#2a2a3a', '#1a2332', '#2a1a2a', + '#1a2a1a', '#2a2a1a', '#333333', '#1a1a1a', '#3a2a42', +]; + +const ACCENT_COLORS = [ + '#7950f2', '#4dabf7', '#69db7c', '#ffd43b', '#ff6b6b', + '#e64980', '#ffa94d', '#868e96', '#ffffff', '#f783ac', +]; + +const WIDTH_PRESETS = [ + { label: 'S', value: 300 }, + { label: 'M', value: 450 }, + { label: 'L', value: 650 }, +]; + +export default function MarkdownFormatToolbar(props: MarkdownFormatToolbarProps) { + const { x, y, bgColor, accentColor, width, onBgColorChange, onAccentColorChange, onWidthChange } = props; + const [showBgPicker, setShowBgPicker] = useState(false); + const [showAccentPicker, setShowAccentPicker] = useState(false); + + useEffect(() => { + const onDown = () => { setShowBgPicker(false); setShowAccentPicker(false); }; + window.addEventListener('pointerdown', onDown); + return () => window.removeEventListener('pointerdown', onDown); + }, []); + + const btnStyle: React.CSSProperties = { + display: 'flex', alignItems: 'center', justifyContent: 'center', + height: '26px', background: 'transparent', border: 'none', borderRadius: '5px', + color: '#999', cursor: 'pointer', padding: '0 6px', fontSize: '11px', + fontFamily: 'system-ui, sans-serif', whiteSpace: 'nowrap', transition: 'all 0.1s', + }; + + return ( +
e.stopPropagation()} + > + {/* Background color */} +
+ + {showBgPicker && ( +
e.stopPropagation()} + > +
+ {PRESET_COLORS.map((c) => ( +
+
+ )} +
+ +
+ + {/* Width presets */} +
+ {WIDTH_PRESETS.map((p) => ( + + ))} +
+ +
+ + {/* Accent color */} +
+ + {showAccentPicker && ( +
e.stopPropagation()} + > +
+ {ACCENT_COLORS.map((c) => ( +
+
+ )} +
+
+ ); +} diff --git a/frontend/src/pages/Editor.tsx b/frontend/src/pages/Editor.tsx index 8828c4a..e03503a 100644 --- a/frontend/src/pages/Editor.tsx +++ b/frontend/src/pages/Editor.tsx @@ -44,6 +44,7 @@ import * as ops from '../canvas/operations'; import ReactDOM from 'react-dom'; import MarkdownReadView from '../components/MarkdownReadView'; import PasteChoicePopup from '../components/PasteChoicePopup'; +import MarkdownFormatToolbar from '../components/MarkdownFormatToolbar'; const LazyMarkdownEditView = React.lazy(() => import('../components/MarkdownEditView')); // Hooks @@ -133,6 +134,7 @@ export default function Editor({ isPublicView }: EditorProps) { >(null); const [videoCtrl, setVideoCtrl] = useState<{ videoSprite: VideoSprite; screenRect: { x: number; y: number; w: number; h: number } } | null>(null); const [pastePopup, setPastePopup] = useState<{ x: number; y: number; text: string; html: string; hasImage: boolean } | null>(null); + const [mdToolbar, setMdToolbar] = useState<{ x: number; y: number; item: SceneItem } | null>(null); const [showMinimap, setShowMinimap] = useState(true); const [minimapData, setMinimapData] = useState<{ items: any[]; viewportBounds: any; contentBounds: any }>({ items: [], viewportBounds: { x: 0, y: 0, w: 1, h: 1 }, contentBounds: { x: 0, y: 0, w: 1, h: 1 }, @@ -708,6 +710,17 @@ export default function Editor({ isPublicView }: EditorProps) { setTextToolbar(null); } + // Markdown format toolbar: show when exactly one markdown card is selected + const mdItems = items.filter((it) => it.type === 'markdown'); + if (mdItems.length === 1 && items.length === 1) { + const b = getItemWorldBounds(mdItems[0]); + const screenTL = vp.toScreen(b.x, b.y); + const screenTR = vp.toScreen(b.x + b.w, b.y); + setMdToolbar({ x: (screenTL.x + screenTR.x) / 2, y: screenTL.y, item: mdItems[0] }); + } else { + setMdToolbar(null); + } + if (items.length < 2) { setSelToolbar(null); } else { // Compute world bounding box of selection let minX = Infinity, minY = Infinity, maxX = -Infinity; @@ -1066,6 +1079,31 @@ export default function Editor({ isPublicView }: EditorProps) { /> )} + {/* Markdown format toolbar */} + {mdToolbar && activeTool === ToolType.SELECT && !contextMenu && !editingMdId && ( + { + (mdToolbar.item.data as MarkdownObject).bgColor = color; + onCanvasChange([mdToolbar.item.id]); + }} + onAccentColorChange={(color) => { + (mdToolbar.item.data as MarkdownObject).accentColor = color; + onCanvasChange([mdToolbar.item.id]); + }} + onWidthChange={(w) => { + mdToolbar.item.data.w = w; + onCanvasChange([mdToolbar.item.id]); + mdOverlay?.measureHeight(mdToolbar.item.id); + selectionRef.current?.transformBox.update([mdToolbar.item]); + }} + /> + )} + {/* Video controls (floating, below selected video) */} {videoCtrl && activeTool === ToolType.SELECT && !contextMenu && (