diff --git a/src/components/EditorCanvas/Canvas.jsx b/src/components/EditorCanvas/Canvas.jsx index d4ceecbf..ffa3f76d 100644 --- a/src/components/EditorCanvas/Canvas.jsx +++ b/src/components/EditorCanvas/Canvas.jsx @@ -174,7 +174,7 @@ export default function Canvas() { const noteRect = { x: note.x, y: note.y, - width: noteWidth, + width: note.width ?? noteWidth, height: note.height, }; if (shouldAddElement(noteRect, element)) { diff --git a/src/components/EditorCanvas/Note.jsx b/src/components/EditorCanvas/Note.jsx index 34fc5373..d3800f88 100644 --- a/src/components/EditorCanvas/Note.jsx +++ b/src/components/EditorCanvas/Note.jsx @@ -14,6 +14,8 @@ import { useSelect, useNotes, useSaveState, + useTransform, + useSettings, } from "../../hooks"; import { useTranslation } from "react-i18next"; import { noteWidth, noteRadius, noteFold } from "../../data/constants"; @@ -21,11 +23,16 @@ import { noteWidth, noteRadius, noteFold } from "../../data/constants"; export default function Note({ data, onPointerDown }) { const [editField, setEditField] = useState({}); const [hovered, setHovered] = useState(false); + const [resizing, setResizing] = useState(false); + const initialWidthRef = useRef(data.width ?? noteWidth); + const initialXRef = useRef(data.x); const { layout } = useLayout(); const { t } = useTranslation(); const { setSaveState } = useSaveState(); const { updateNote, deleteNote } = useNotes(); const { setUndoStack, setRedoStack } = useUndoRedo(); + const { transform } = useTransform(); + const { settings } = useSettings(); const { selectedElement, setSelectedElement, @@ -169,6 +176,9 @@ export default function Note({ data, onPointerDown }) { ); }, [selectedElement, data, bulkSelectedElements]); + const width = data.width ?? noteWidth; + const MIN_NOTE_WIDTH = 120; + return ( e.isPrimary && setHovered(true)} @@ -181,11 +191,11 @@ export default function Note({ data, onPointerDown }) { onDoubleClick={edit} > + + {!layout.readOnly && !data.locked && hovered && ( + + + + + )} + {!layout.readOnly && !data.locked && ( + { + e.stopPropagation(); + initialWidthRef.current = data.width ?? noteWidth; + initialXRef.current = data.x; + setResizing(true); + e.currentTarget.setPointerCapture?.(e.pointerId); + }} + onPointerMove={(e) => { + if (!resizing) return; + const delta = e.movementX / (transform?.zoom || 1); + const currentWidth = data.width ?? noteWidth; + let proposedWidth = currentWidth - delta; + let proposedX = data.x + delta; + if (proposedWidth < MIN_NOTE_WIDTH) { + const clampDelta = currentWidth - MIN_NOTE_WIDTH; + proposedWidth = MIN_NOTE_WIDTH; + proposedX = data.x + clampDelta; + } + if (proposedWidth !== data.width || proposedX !== data.x) { + updateNote(data.id, { width: proposedWidth, x: proposedX }); + } + }} + onPointerUp={(e) => { + if (!resizing) return; + setResizing(false); + e.stopPropagation(); + const finalWidth = data.width ?? noteWidth; + const finalX = data.x; + const startWidth = initialWidthRef.current; + const startX = initialXRef.current; + if (finalWidth !== startWidth || finalX !== startX) { + setUndoStack((prev) => [ + ...prev, + { + action: Action.EDIT, + element: ObjectType.NOTE, + nid: data.id, + undo: { width: startWidth, x: startX }, + redo: { width: finalWidth, x: finalX }, + message: t("edit_note", { + noteTitle: data.title, + extra: "[width/x]", + }), + }, + ]); + setRedoStack([]); + } + }} + /> + )} + + {!layout.readOnly && !data.locked && ( + { + e.stopPropagation(); + initialWidthRef.current = data.width ?? noteWidth; + setResizing(true); + e.currentTarget.setPointerCapture?.(e.pointerId); + }} + onPointerMove={(e) => { + if (!resizing) return; + const delta = e.movementX / (transform?.zoom || 1); + const next = Math.max(MIN_NOTE_WIDTH, (data.width ?? noteWidth) + delta); + if (next !== data.width) { + updateNote(data.id, { width: next }); + } + }} + onPointerUp={(e) => { + if (!resizing) return; + setResizing(false); + e.stopPropagation(); + const finalWidth = data.width ?? noteWidth; + const startWidth = initialWidthRef.current; + if (finalWidth !== startWidth) { + setUndoStack((prev) => [ + ...prev, + { + action: Action.EDIT, + element: ObjectType.NOTE, + nid: data.id, + undo: { width: startWidth }, + redo: { width: finalWidth }, + message: t("edit_note", { + noteTitle: data.title, + extra: "[width]", + }), + }, + ]); + setRedoStack([]); + } + }} + /> + )} diff --git a/src/components/EditorHeader/ControlPanel.jsx b/src/components/EditorHeader/ControlPanel.jsx index 99635aed..1083422a 100644 --- a/src/components/EditorHeader/ControlPanel.jsx +++ b/src/components/EditorHeader/ControlPanel.jsx @@ -541,7 +541,10 @@ export default function ControlPanel({ notes.forEach((note) => { minMaxXY.minX = Math.min(minMaxXY.minX, note.x); minMaxXY.minY = Math.min(minMaxXY.minY, note.y); - minMaxXY.maxX = Math.max(minMaxXY.maxX, note.x + noteWidth); + minMaxXY.maxX = Math.max( + minMaxXY.maxX, + note.x + (note.width ?? noteWidth), + ); minMaxXY.maxY = Math.max(minMaxXY.maxY, note.y + note.height); }); diff --git a/src/components/Thumbnail.jsx b/src/components/Thumbnail.jsx index b95b6762..623c2a1b 100644 --- a/src/components/Thumbnail.jsx +++ b/src/components/Thumbnail.jsx @@ -120,13 +120,14 @@ export default function Thumbnail({ diagram, i, zoom, theme }) { const x = n.x; const y = n.y; const h = n.height; + const w = n.width ?? noteWidth; return ( - +