@@ -19,6 +19,24 @@ const INSERT_AT_END = undefined; // undefined means append to end in addNode/mov
1919// Set to true to allow context menu on the root component, false to disable it
2020const ALLOW_ROOT_CONTEXT_MENU = false ;
2121
22+ // Helper to find node in schema - extracted to top level
23+ const findNodeInSchema = ( node : SchemaNode , targetId : string ) : SchemaNode | null => {
24+ if ( node . id === targetId ) return node ;
25+
26+ if ( Array . isArray ( node . body ) ) {
27+ for ( const child of node . body ) {
28+ if ( typeof child === 'object' && child !== null ) {
29+ const found = findNodeInSchema ( child as SchemaNode , targetId ) ;
30+ if ( found ) return found ;
31+ }
32+ }
33+ } else if ( node . body && typeof node . body === 'object' ) {
34+ return findNodeInSchema ( node . body as SchemaNode , targetId ) ;
35+ }
36+
37+ return null ;
38+ } ;
39+
2240export const Canvas : React . FC < CanvasProps > = React . memo ( ( { className } ) => {
2341 const {
2442 schema,
@@ -39,6 +57,7 @@ export const Canvas: React.FC<CanvasProps> = React.memo(({ className }) => {
3957
4058 const [ scale , setScale ] = useState ( 1 ) ;
4159 const [ contextMenu , setContextMenu ] = useState < { x : number ; y : number ; nodeId : string } | null > ( null ) ;
60+ const [ selectionBounds , setSelectionBounds ] = useState < { top : number ; left : number ; width : number ; height : number } | null > ( null ) ;
4261 const canvasRef = React . useRef < HTMLDivElement > ( null ) ;
4362
4463 // Memoize canvas width calculation
@@ -258,24 +277,6 @@ export const Canvas: React.FC<CanvasProps> = React.memo(({ className }) => {
258277 } ;
259278 } , [ resizingNode , setResizingNode , updateNode , schema ] ) ;
260279
261- // Helper to find node in schema
262- const findNodeInSchema = ( node : SchemaNode , targetId : string ) : SchemaNode | null => {
263- if ( node . id === targetId ) return node ;
264-
265- if ( Array . isArray ( node . body ) ) {
266- for ( const child of node . body ) {
267- if ( typeof child === 'object' && child !== null ) {
268- const found = findNodeInSchema ( child as SchemaNode , targetId ) ;
269- if ( found ) return found ;
270- }
271- }
272- } else if ( node . body && typeof node . body === 'object' ) {
273- return findNodeInSchema ( node . body as SchemaNode , targetId ) ;
274- }
275-
276- return null ;
277- } ;
278-
279280 // Make components in canvas draggable
280281 React . useEffect ( ( ) => {
281282 if ( ! canvasRef . current ) return ;
@@ -321,6 +322,44 @@ export const Canvas: React.FC<CanvasProps> = React.memo(({ className }) => {
321322 } ;
322323 } , [ schema , setDraggingNodeId ] ) ;
323324
325+ // Measure selected node bounds for resize handles
326+ React . useLayoutEffect ( ( ) => {
327+ if ( ! selectedNodeId || ! canvasRef . current ) {
328+ setSelectionBounds ( null ) ;
329+ return ;
330+ }
331+
332+ const measure = ( ) => {
333+ const element = canvasRef . current ?. querySelector ( `[data-obj-id="${ selectedNodeId } "]` ) ;
334+ if ( ! element ) {
335+ setSelectionBounds ( null ) ;
336+ return ;
337+ }
338+
339+ const rect = element . getBoundingClientRect ( ) ;
340+ const canvasRect = canvasRef . current ?. getBoundingClientRect ( ) ;
341+ if ( ! canvasRect ) return ;
342+
343+ setSelectionBounds ( {
344+ top : rect . top - canvasRect . top ,
345+ left : rect . left - canvasRect . left ,
346+ width : rect . width ,
347+ height : rect . height
348+ } ) ;
349+ } ;
350+
351+ measure ( ) ;
352+
353+ // Update on resize or scroll
354+ window . addEventListener ( 'resize' , measure ) ;
355+ window . addEventListener ( 'scroll' , measure , true ) ;
356+
357+ return ( ) => {
358+ window . removeEventListener ( 'resize' , measure ) ;
359+ window . removeEventListener ( 'scroll' , measure , true ) ;
360+ } ;
361+ } , [ selectedNodeId , schema ] ) ;
362+
324363 // Inject styles for selection/hover using dynamic CSS
325364 // Enhanced with smooth transitions and gradient effects for premium UX
326365 const highlightStyles = `
@@ -555,24 +594,13 @@ export const Canvas: React.FC<CanvasProps> = React.memo(({ className }) => {
555594 < SchemaRenderer schema = { schema } />
556595
557596 { /* Resize Handles - show only when a resizable component is selected */ }
558- { selectedNodeId && ( ( ) => {
597+ { selectedNodeId && selectionBounds && ( ( ) => {
559598 const selectedNode = findNodeInSchema ( schema , selectedNodeId ) ;
560599 if ( ! selectedNode ) return null ;
561600
562601 const config = ComponentRegistry . getConfig ( selectedNode . type ) ;
563602 if ( ! config ?. resizable ) return null ;
564603
565- const element = canvasRef . current ?. querySelector ( `[data-obj-id="${ selectedNodeId } "]` ) ;
566- if ( ! element ) return null ;
567-
568- const rect = element . getBoundingClientRect ( ) ;
569- const canvasRect = canvasRef . current ?. getBoundingClientRect ( ) ;
570- if ( ! canvasRect ) return null ;
571-
572- // Calculate position relative to canvas
573- const top = rect . top - canvasRect . top ;
574- const left = rect . left - canvasRect . left ;
575-
576604 // Determine which directions to show based on constraints
577605 const constraints = config . resizeConstraints || { } ;
578606 const directions : ResizeDirection [ ] = [ ] ;
@@ -591,10 +619,10 @@ export const Canvas: React.FC<CanvasProps> = React.memo(({ className }) => {
591619 < div
592620 className = "absolute pointer-events-none"
593621 style = { {
594- top : `${ top } px` ,
595- left : `${ left } px` ,
596- width : `${ rect . width } px` ,
597- height : `${ rect . height } px` ,
622+ top : `${ selectionBounds . top } px` ,
623+ left : `${ selectionBounds . left } px` ,
624+ width : `${ selectionBounds . width } px` ,
625+ height : `${ selectionBounds . height } px` ,
598626 } }
599627 >
600628 < ResizeHandles
0 commit comments