Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions src/components/Dashboard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -1187,6 +1187,7 @@ export default function Dashboard({
{ id: "classDiagram", label: "Class" },
{ id: "erDiagram", label: "ER" },
{ id: "stateDiagram", label: "State" },
{ id: "mindmap", label: "Mindmap" },
].map((t) => (
<button
key={t.id}
Expand Down
25 changes: 25 additions & 0 deletions src/components/editor/CommentLayer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import {
getVisibleSequenceMessageTexts,
findOwningLineForSequenceLabel,
} from "@/hooks/useCanvasInteraction";
import { findMindmapSvgElementByNodeId } from "@/lib/diagrams/mindmap";

const SHAPE_COMMENT_OFFSET = 4;
const SEQUENCE_COMMENT_OFFSET = 5;
Expand All @@ -27,6 +28,7 @@ type CommentComposerState = {
} | null;

interface CommentLayerProps {
code: string;
comments: DiagramComment[];
scale: number;
containerRef: RefObject<HTMLDivElement | null>;
Expand Down Expand Up @@ -74,6 +76,7 @@ function isEdgeAnchorElement(el: Element) {
}

export function CommentLayer({
code,
comments,
scale,
containerRef,
Expand Down Expand Up @@ -199,6 +202,27 @@ export function CommentLayer({
continue;
}
}

if (comment.anchor.shapeId?.startsWith("MINDMAP_")) {
const node = findMindmapSvgElementByNodeId(code, safeContainer, comment.anchor.shapeId);
if (node) {
const rect = node.getBoundingClientRect();
x = (rect.right + SHAPE_COMMENT_OFFSET * scale - safeContainerRect.left) / scale;
y = (rect.top - SHAPE_COMMENT_OFFSET * scale - safeContainerRect.top) / scale;
x = Math.min(contentWidth - 16, Math.max(16, x));
y = Math.min(contentHeight - 16, Math.max(16, y));
entries.set(comment.id, { x, y, missingTarget: false });
continue;
}
if (comment.anchor.fallbackPos) {
x = comment.anchor.fallbackPos.x;
y = comment.anchor.fallbackPos.y;
missingTarget = true;
entries.set(comment.id, { x, y, missingTarget });
continue;
}
}

const directSequenceIndex = Number(
comment.anchor.shapeId?.match(/^SEQ_MSG_(\d+)$/)?.[1] ?? -1,
);
Expand Down Expand Up @@ -325,6 +349,7 @@ export function CommentLayer({
return entries;
}, [
canMeasure,
code,
comments,
container,
containerRect,
Expand Down
65 changes: 63 additions & 2 deletions src/components/editor/EditorCanvas.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import { ClassNodeToolbar } from "./ClassNodeToolbar";
import { ErNodeToolbar } from "./ErNodeToolbar";
import { StateNodeToolbar } from "./StateNodeToolbar";
import { StateEdgeToolbar } from "./StateEdgeToolbar";
import { MindmapNodeToolbar } from "./MindmapNodeToolbar";
import { ErEdgeToolbar } from "./ErEdgeToolbar";
import { ErPropertyPanel } from "./ErPropertyPanel";
import { InlineTextEditor } from "./InlineTextEditor";
Expand Down Expand Up @@ -46,6 +47,7 @@ import {
hasEndState,
} from "@/lib/diagrams/stateDiagram";
import type { StateNodeShapeKind, StateShapeKind } from "@/lib/diagrams/stateDiagram";
import { getMindmapNode, parseMindmap, type MindmapShapeKind } from "@/lib/diagrams/mindmap";
import { StateConnectMenu, type StateConnectMenuState } from "./StateConnectMenu";
import type { SequenceBlockArea, SequenceBlockType } from "@/hooks/useCanvasInteraction";
import { findOwningLineForSequenceLabel } from "@/hooks/useCanvasInteraction";
Expand Down Expand Up @@ -226,6 +228,9 @@ interface EditorCanvasProps {
onAddStateTransition?: (source: string, target: string) => void;
/** Drop-on-empty-canvas: create the chosen shape and link `source --> <shape>` in one edit. */
onCreateStateShapeLinked?: (source: string, kind: StateShapeKind) => void;
onAddMindmapChild?: (nodeId: string) => void;
onDeleteMindmapNode?: (nodeId: string) => void;
onChangeMindmapShape?: (nodeId: string, shape: MindmapShapeKind) => void;
handleAddNodeFromSelected: (
startId: string | null,
targetNodeId?: string,
Expand Down Expand Up @@ -489,6 +494,9 @@ export function EditorCanvas({
onDeleteStateTransition,
onAddStateTransition,
onCreateStateShapeLinked,
onAddMindmapChild,
onDeleteMindmapNode,
onChangeMindmapShape,
handleUpdateStyle,
handleFormatNodeLabel,
handleChangeShape,
Expand Down Expand Up @@ -1329,6 +1337,16 @@ export function EditorCanvas({
return stateNameFromSvgId(selectedSvgId);
}, [currentType, selectedSvgId]);

const selectedMindmapNode = useMemo(() => {
if (currentType !== "mindmap" || !selectedNodeId?.startsWith("MINDMAP_")) return null;
return getMindmapNode(code, selectedNodeId);
}, [code, currentType, selectedNodeId]);

const mindmapHasNodes = useMemo(
() => (currentType === "mindmap" ? parseMindmap(code).nodes.length > 0 : true),
[code, currentType],
);

const connectSourceStateIsComposite = useMemo(
() => (connectSourceState ? isCompositeState(code, connectSourceState) : false),
[connectSourceState, code],
Expand Down Expand Up @@ -1998,7 +2016,7 @@ export function EditorCanvas({
onMouseUp={handleMouseUp}
onMouseLeave={handleMouseUp}
>
{parseError && (
{parseError && !(currentType === "mindmap" && !mindmapHasNodes) && (
<div
className="absolute inset-0 z-40 bg-white/60 cursor-not-allowed flex items-center justify-center pointer-events-auto"
onClick={(e) => e.stopPropagation()}
Expand All @@ -2011,6 +2029,7 @@ export function EditorCanvas({
/>

<CommentLayer
code={code}
comments={comments}
scale={state.scale}
containerRef={containerRef}
Expand Down Expand Up @@ -2703,10 +2722,20 @@ export function EditorCanvas({
: undefined
}
/>
) : currentType === "mindmap" && selectedMindmapNode ? (
<MindmapNodeToolbar
scale={state.scale}
currentShape={selectedMindmapNode.shape}
onChangeShape={(shape) =>
onChangeMindmapShape?.(selectedMindmapNode.id, shape)
}
onDelete={() => onDeleteMindmapNode?.(selectedMindmapNode.id)}
/>
) : currentType === "sequence" ||
currentType === "classDiagram" ||
currentType === "erDiagram" ||
currentType === "stateDiagram" ? null : (
currentType === "stateDiagram" ||
currentType === "mindmap" ? null : (
<NodeManipulationToolbar
code={code}
selectedNodeId={selectedNodeId}
Expand Down Expand Up @@ -2760,11 +2789,43 @@ export function EditorCanvas({
selectedSvgId={selectedSvgId}
/>

{!isInlineEditing &&
currentType === "mindmap" &&
selectedMindmapNode &&
selectionBox && (
<div
data-scale-lock
data-base-transform="translateX(-50%) translateY(100%)"
className="absolute left-1/2 pointer-events-auto origin-top"
style={{
bottom: `calc(-12px * var(--zoom-inverse-scale, ${1 / state.scale}))`,
transform: `translateX(-50%) translateY(100%) scale(var(--zoom-inverse-scale, ${1 / state.scale}))`,
}}
>
<button
type="button"
onMouseDown={(e) => {
e.stopPropagation();
e.preventDefault();
}}
onClick={(e) => {
e.stopPropagation();
onAddMindmapChild?.(selectedMindmapNode.id);
}}
className="h-5 w-5 rounded-full bg-indigo-500 text-white shadow-md transition-transform hover:scale-110 hover:bg-indigo-600 flex items-center justify-center"
title="Click to add child element"
>
<Plus className="h-3 w-3 pointer-events-none" />
</button>
</div>
)}

{!isInlineEditing &&
currentType !== "sequence" &&
currentType !== "classDiagram" &&
currentType !== "erDiagram" &&
currentType !== "stateDiagram" &&
currentType !== "mindmap" &&
(!selectedNodeId ||
(!isEdgeId(selectedNodeId) &&
!selectedNodeId.startsWith("SEQ_MSG_") &&
Expand Down
43 changes: 43 additions & 0 deletions src/components/editor/LiveMaidEditor.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -151,6 +151,13 @@ import {
setStateNodeShape,
} from "@/lib/diagrams/stateDiagram";
import type { StateNodeShapeKind, StateShapeKind } from "@/lib/diagrams/stateDiagram";
import {
addMindmapChild,
changeMindmapNodeShape,
deleteMindmapNode,
mindmapLineFromNodeId,
type MindmapShapeKind,
} from "@/lib/diagrams/mindmap";
import { FONT_OPTIONS } from "@/lib/diagrams/constants";
import { updateMermaidConfigProperty, updateMermaidFontFamily } from "@/lib/diagrams/utils";
import { useRouter } from "next/navigation";
Expand Down Expand Up @@ -460,6 +467,35 @@ export function LiveMaidEditor({
setSelectedClassName(null);
}, [setSelectedNodeId, setSelectedSvgId, setSelectionBox, setTextBox, setIsInlineEditing]);

const handleAddMindmapChild = useCallback(
(nodeId: string) => {
const result = addMindmapChild(code, nodeId);
if (result.code !== code) {
handleCodeChange(result.code);
setSelectedNodeId(result.nodeId);
setSelectedSvgId(null);
}
},
[code, handleCodeChange, setSelectedNodeId, setSelectedSvgId],
);

const handleDeleteMindmapNode = useCallback(
(nodeId: string) => {
const newCode = deleteMindmapNode(code, nodeId);
if (newCode !== code) handleCodeChange(newCode);
handleDeselect();
},
[code, handleCodeChange, handleDeselect],
);

const handleChangeMindmapShape = useCallback(
(nodeId: string, shape: MindmapShapeKind) => {
const newCode = changeMindmapNodeShape(code, nodeId, shape);
if (newCode !== code) handleCodeChange(newCode);
},
[code, handleCodeChange],
);

// Class-diagram property panel state. `selectedClassName` is sticky: the interaction hook's
// `recalculateSelection` clears the underlying canvas selection whenever it cannot re-resolve a
// node after a re-render (which happens for class nodes on every member edit), so deriving the
Expand Down Expand Up @@ -2059,6 +2095,10 @@ export function LiveMaidEditor({
return msg ? toRange(msg.index) : null;
}

if (selectedNodeId.startsWith("MINDMAP_")) {
return toRange(mindmapLineFromNodeId(selectedNodeId) ?? -1);
}

if (isEdgeId(selectedNodeId)) {
const { src, dst, occurrenceIndex } = parseEdgeId(selectedNodeId);
return toRange(findFlowchartEdgeLine(code, src, dst, occurrenceIndex));
Expand Down Expand Up @@ -4736,6 +4776,9 @@ export function LiveMaidEditor({
onDeleteStateTransition={handleDeleteStateTransition}
onAddStateTransition={handleAddStateTransition}
onCreateStateShapeLinked={handleCreateStateShapeLinked}
onAddMindmapChild={handleAddMindmapChild}
onDeleteMindmapNode={handleDeleteMindmapNode}
onChangeMindmapShape={handleChangeMindmapShape}
handleUpdateStyle={handleUpdateStyle}
handleFormatNodeLabel={handleFormatNodeLabel}
handleChangeShape={handleChangeShape}
Expand Down
Loading
Loading