From deae847faa8fd762ea534b95507eb629bcaaf8c8 Mon Sep 17 00:00:00 2001 From: tejasM17 Date: Thu, 16 Jul 2026 22:01:47 +0530 Subject: [PATCH 1/4] compleated 14-feat --- components/editor/canvas-node.tsx | 201 +++++++++++++++++++---- components/editor/node-shape-visual.tsx | 167 +++++++++++++++++++ components/editor/shape-panel.tsx | 155 +++++++++++++---- context/feature-specs/13-node-shape.md | 33 ++++ context/feature-specs/14-node-editing.md | 40 +++++ context/progress-tracker.md | 9 +- types/canvas.ts | 7 +- 7 files changed, 545 insertions(+), 67 deletions(-) create mode 100644 components/editor/node-shape-visual.tsx create mode 100644 context/feature-specs/13-node-shape.md create mode 100644 context/feature-specs/14-node-editing.md diff --git a/components/editor/canvas-node.tsx b/components/editor/canvas-node.tsx index a705840..b6bb652 100644 --- a/components/editor/canvas-node.tsx +++ b/components/editor/canvas-node.tsx @@ -1,52 +1,195 @@ "use client"; -import { memo } from "react"; -import { Handle, Position, type NodeProps } from "@xyflow/react"; +import { + memo, + useCallback, + useEffect, + useRef, + useState, + type KeyboardEvent, + type MouseEvent, + type FocusEvent, +} from "react"; +import { + Handle, + NodeResizer, + Position, + useReactFlow, + type NodeProps, +} from "@xyflow/react"; import type { CanvasNode } from "@/types/canvas"; import { NODE_COLORS } from "@/types/canvas"; +import { NodeShapeVisual } from "./node-shape-visual"; + +/** Minimum node size — prevents nodes from collapsing to unusable dimensions. */ +const MIN_NODE_WIDTH = 80; +const MIN_NODE_HEIGHT = 40; + +const LABEL_PLACEHOLDER = "Label"; + +const HANDLE_CLASS = + "!h-2 !w-2 !rounded-full !border !border-border-default !bg-white"; /** - * Basic canvas node renderer. For this initial implementation, renders - * every shape as a simple bordered rectangle with the label centered. - * Shape-specific visuals will be added in later features. + * Custom React Flow node: per-shape visuals, resize handles when selected, + * four connectable points, and double-click inline label editing. + * All dimension and label updates flow through the collaborative node state. */ -function CanvasNodeComponent({ data }: NodeProps) { +function CanvasNodeComponent({ + id, + data, + selected, + width, + height, +}: NodeProps) { const { label, color: colorName, shape } = data; + const { updateNodeData } = useReactFlow(); + + const [isEditing, setIsEditing] = useState(false); + const textareaRef = useRef(null); + + const colorPair = + NODE_COLORS.find((c) => c.name === colorName) ?? NODE_COLORS[0]; + + const nodeWidth = width ?? 160; + const nodeHeight = height ?? 80; + + // Subtle border at rest; brighter when selected + const borderColor = selected + ? colorPair.text + : `${colorPair.text}55`; + + // Focus the textarea when editing starts + useEffect(() => { + if (!isEditing) return; + const el = textareaRef.current; + if (!el) return; + el.focus(); + // Place caret at end without scrolling the canvas + const len = el.value.length; + el.setSelectionRange(len, len); + }, [isEditing]); + + const startEditing = useCallback((e: MouseEvent) => { + e.stopPropagation(); + e.preventDefault(); + setIsEditing(true); + }, []); + + const handleLabelChange = useCallback( + (value: string) => { + updateNodeData(id, { label: value }); + }, + [id, updateNodeData], + ); + + const handleTextareaKeyDown = useCallback( + (e: KeyboardEvent) => { + // Escape closes editing; stop propagation so the canvas doesn't + // treat Escape as a global shortcut while typing. + if (e.key === "Escape") { + e.preventDefault(); + e.stopPropagation(); + setIsEditing(false); + textareaRef.current?.blur(); + } + // Keep Enter for newlines inside the textarea; do not bubble to RF. + e.stopPropagation(); + }, + [], + ); + + const handleTextareaBlur = useCallback((_e: FocusEvent) => { + setIsEditing(false); + }, []); - // Get the color pair for this node - const colorPair = NODE_COLORS.find((c) => c.name === colorName) ?? NODE_COLORS[0]; + // Block pointer events from starting a node drag / canvas pan while editing. + const stopPointer = useCallback((e: MouseEvent) => { + e.stopPropagation(); + }, []); return (
- {/* Input handle */} + + + {/* Four connectable points — one per side */} + - - - {label || shape} - - - {/* Output handle */} + + + + + {isEditing && ( +
+