-
Size
+
+ Size
{STROKE_WIDTHS.map((width) => (
-
+
{/* Fill Toggle */}
{/* Zoom */}
-
-
-
{Math.round(scale * 100)}%
-
+
+
+ {Math.round(scale * 100)}%
+
- {/* Text Editing Modal */}
- {editingTextId && (
-
-
-
a.id === editingTextId) as TextShape)?.text || ""}
- className="w-full px-2 py-1.5 text-sm text-gray-900 border border-gray-200 rounded mb-3 focus:outline-none focus:ring-1 focus:ring-gray-300"
- onKeyDown={(e) => {
- if (e.key === "Enter") { updateAnnotation(editingTextId, { text: (e.target as HTMLInputElement).value }); setEditingTextId(null); }
- if (e.key === "Escape") setEditingTextId(null);
- }}
- />
-
-
-
-
-
-
+ {/* Inline Text Input */}
+ {editingTextId && textInputPosition && (
+
a.id === editingTextId) as TextShape)?.text || ""}
+ className="fixed z-[110] bg-transparent border-none outline-none"
+ style={{
+ left: textInputPosition.x,
+ top: textInputPosition.y,
+ fontSize: `${toolOptions.fontSize * scale}px`,
+ color: editingTextId === "new" ? toolOptions.strokeColor : ((annotations.find((a) => a.id === editingTextId) as TextShape)?.fill || toolOptions.strokeColor),
+ minWidth: "100px",
+ caretColor: "white",
+ }}
+ onKeyDown={(e) => {
+ if (e.key === "Enter") {
+ const value = (e.target as HTMLInputElement).value;
+ if (value.trim()) {
+ if (editingTextId === "new" && pendingTextPosition) {
+ // Create new text annotation
+ const newShape: TextShape = {
+ id: `shape-${Date.now()}`,
+ type: "text",
+ x: pendingTextPosition.x,
+ y: pendingTextPosition.y,
+ text: value,
+ fontSize: toolOptions.fontSize,
+ fill: toolOptions.strokeColor,
+ stroke: toolOptions.strokeColor,
+ strokeWidth: toolOptions.strokeWidth,
+ opacity: toolOptions.opacity,
+ };
+ addAnnotation(newShape);
+ } else {
+ updateAnnotation(editingTextId, { text: value });
+ }
+ } else if (editingTextId !== "new") {
+ deleteAnnotation(editingTextId);
+ }
+ setEditingTextId(null);
+ setTextInputPosition(null);
+ setPendingTextPosition(null);
+ }
+ if (e.key === "Escape") {
+ if (editingTextId !== "new") {
+ const currentText = (annotations.find((a) => a.id === editingTextId) as TextShape)?.text;
+ if (!currentText) {
+ deleteAnnotation(editingTextId);
+ }
+ }
+ setEditingTextId(null);
+ setTextInputPosition(null);
+ setPendingTextPosition(null);
+ }
+ }}
+ onBlur={(e) => {
+ // Ignore blur events that happen immediately after creation (within 200ms)
+ // This prevents the click that created the input from also triggering blur
+ if (Date.now() - textInputCreatedAt.current < 200) {
+ e.target.focus();
+ return;
+ }
+
+ const value = e.target.value;
+ if (value.trim()) {
+ if (editingTextId === "new" && pendingTextPosition) {
+ // Create new text annotation
+ const newShape: TextShape = {
+ id: `shape-${Date.now()}`,
+ type: "text",
+ x: pendingTextPosition.x,
+ y: pendingTextPosition.y,
+ text: value,
+ fontSize: toolOptions.fontSize,
+ fill: toolOptions.strokeColor,
+ stroke: toolOptions.strokeColor,
+ strokeWidth: toolOptions.strokeWidth,
+ opacity: toolOptions.opacity,
+ };
+ addAnnotation(newShape);
+ } else {
+ updateAnnotation(editingTextId, { text: value });
+ }
+ } else if (editingTextId !== "new") {
+ deleteAnnotation(editingTextId);
+ }
+ setEditingTextId(null);
+ setTextInputPosition(null);
+ setPendingTextPosition(null);
+ }}
+ />
)}
);
diff --git a/src/components/EdgeToolbar.tsx b/src/components/EdgeToolbar.tsx
new file mode 100644
index 00000000..a9d31ea0
--- /dev/null
+++ b/src/components/EdgeToolbar.tsx
@@ -0,0 +1,101 @@
+"use client";
+
+import { useWorkflowStore } from "@/store/workflowStore";
+import { useMemo, useEffect, useState, useRef } from "react";
+
+export function EdgeToolbar() {
+ const { edges, toggleEdgePause, removeEdge } = useWorkflowStore();
+ const [clickPosition, setClickPosition] = useState<{ x: number; y: number } | null>(null);
+ const previousSelectedEdgeId = useRef