1- import { BLOCK_DIMENSIONS , CONTAINER_DIMENSIONS , getNoteBlockHeight } from '@sim/workflow-renderer'
1+ import {
2+ BLOCK_DIMENSIONS ,
3+ CONTAINER_CHILD_MIN_POSITION ,
4+ CONTAINER_DIMENSIONS ,
5+ getNoteBlockHeight ,
6+ } from '@sim/workflow-renderer'
27import { showsCanvasErrorRow } from '@/lib/workflows/blocks/canvas-rows'
38import { calculateWorkflowBlockDimensions } from '@/lib/workflows/blocks/deterministic-dimensions'
49import { getBlock } from '@/blocks/registry'
@@ -54,8 +59,7 @@ export function clampPositionToContainer(
5459 const { width : containerWidth , height : containerHeight } = containerDimensions
5560 const { width : blockWidth , height : blockHeight } = blockDimensions
5661
57- const minX = CONTAINER_DIMENSIONS . LEFT_PADDING
58- const minY = CONTAINER_DIMENSIONS . HEADER_HEIGHT + CONTAINER_DIMENSIONS . TOP_PADDING
62+ const { x : minX , y : minY } = CONTAINER_CHILD_MIN_POSITION
5963 const maxX = containerWidth - CONTAINER_DIMENSIONS . RIGHT_PADDING - blockWidth
6064 const maxY = containerHeight - CONTAINER_DIMENSIONS . BOTTOM_PADDING - blockHeight
6165
@@ -65,6 +69,42 @@ export function clampPositionToContainer(
6569 }
6670}
6771
72+ /**
73+ * Runtime React Flow bounds for a nested node.
74+ *
75+ * The finite leading edge protects the header and Start lane. The trailing
76+ * edge stays open because moving a child right or down expands its container.
77+ * Persisted workflow data continues using `extent: 'parent'`.
78+ */
79+ export function getContainerChildExtent ( ) : [ [ number , number ] , [ number , number ] ] {
80+ return [
81+ [ CONTAINER_CHILD_MIN_POSITION . x , CONTAINER_CHILD_MIN_POSITION . y ] ,
82+ [ Number . POSITIVE_INFINITY , Number . POSITIVE_INFINITY ] ,
83+ ]
84+ }
85+
86+ /** Keeps a batch's relative arrangement while moving it clear of container chrome. */
87+ export function shiftPositionsToContainerBounds <
88+ T extends { newPosition : { x : number ; y : number } } ,
89+ > ( rawUpdates : T [ ] ) : T [ ] {
90+ if ( rawUpdates . length === 0 ) return rawUpdates
91+
92+ const minX = Math . min ( ...rawUpdates . map ( ( update ) => update . newPosition . x ) )
93+ const minY = Math . min ( ...rawUpdates . map ( ( update ) => update . newPosition . y ) )
94+ const shiftX = Math . max ( 0 , CONTAINER_CHILD_MIN_POSITION . x - minX )
95+ const shiftY = Math . max ( 0 , CONTAINER_CHILD_MIN_POSITION . y - minY )
96+
97+ if ( shiftX === 0 && shiftY === 0 ) return rawUpdates
98+
99+ return rawUpdates . map ( ( update ) => ( {
100+ ...update ,
101+ newPosition : {
102+ x : update . newPosition . x + shiftX ,
103+ y : update . newPosition . y + shiftY ,
104+ } ,
105+ } ) )
106+ }
107+
68108/**
69109 * Calculates container dimensions based on child block positions.
70110 * Single source of truth for container sizing - ensures consistency between
@@ -73,11 +113,10 @@ export function clampPositionToContainer(
73113 * Child coordinates are relative to the container's own origin — React Flow
74114 * places a child at the parent's origin plus its position, and
75115 * {@link clampPositionToContainer} keeps them clear of the chrome by flooring
76- * them at `LEFT_PADDING` and `HEADER_HEIGHT + TOP_PADDING`. A child's far edge
77- * is therefore already the distance the container has to cover, and only the
78- * trailing padding is owed on top. Adding the header and leading padding here
79- * as well counted them twice, leaving every container 66px taller and 16px
80- * wider than its contents.
116+ * them at `CONTAINER_CHILD_MIN_POSITION`. A child's far edge is therefore
117+ * already the distance the container has to cover, and only the trailing
118+ * padding is owed on top. Adding the header and leading lane here as well would
119+ * count them twice.
81120 *
82121 * @param childPositions - Array of child positions with their dimensions
83122 * @returns Calculated width and height for the container
0 commit comments