@@ -49,6 +49,7 @@ export const LayoutEngine: React.FC<LayoutEngineProps> = React.memo(({
4949 onItemConfig,
5050} ) => {
5151 const theme = useTheme ( ) ;
52+ const containerRef = useRef < HTMLDivElement > ( null ) ;
5253
5354 // Debug: Track component re-renders
5455 const layoutEngineRenderCount = useRef ( 0 ) ;
@@ -438,6 +439,63 @@ export const LayoutEngine: React.FC<LayoutEngineProps> = React.memo(({
438439 const stableIdentityRef = useRef < Map < string , { pluginId : string ; moduleId : string } > > ( new Map ( ) ) ;
439440
440441 const { currentBreakpoint } = useBreakpoint ( ) ;
442+
443+ // --- Adaptive rowHeight calculation (tracks container + viewport height) ---
444+ const [ computedRowHeight , setComputedRowHeight ] = useState < number > ( defaultGridConfig . rowHeight ) ;
445+ const [ containerHeight , setContainerHeight ] = useState < number > ( 0 ) ;
446+
447+ // Observe container size
448+ useEffect ( ( ) => {
449+ if ( ! containerRef . current ) return ;
450+ const el = containerRef . current ;
451+ const ro = new ResizeObserver ( ( ) => setContainerHeight ( el . clientHeight ) ) ;
452+ ro . observe ( el ) ;
453+ setContainerHeight ( el . clientHeight ) ;
454+ return ( ) => ro . disconnect ( ) ;
455+ } , [ ] ) ;
456+
457+ // Recompute on window resize to follow viewport height changes
458+ useEffect ( ( ) => {
459+ const onResize = ( ) => setContainerHeight ( containerRef . current ? containerRef . current . clientHeight : window . innerHeight ) ;
460+ window . addEventListener ( 'resize' , onResize ) ;
461+ return ( ) => window . removeEventListener ( 'resize' , onResize ) ;
462+ } , [ ] ) ;
463+
464+ useEffect ( ( ) => {
465+ try {
466+ const bpMap : Record < string , keyof ResponsiveLayouts > = { xs : 'mobile' , sm : 'tablet' , lg : 'desktop' , xl : 'wide' , xxl : 'ultrawide' } ;
467+ const ourBp = bpMap [ currentBreakpoint || 'lg' ] || 'desktop' ;
468+ const items = ( displayedLayouts ?. [ ourBp ] || [ ] ) as LayoutItem [ ] ;
469+
470+ const wantsFill = items . some ( it => ( it as any ) ?. config ?. viewportFill === true ) || items . length === 1 ;
471+ if ( ! wantsFill ) {
472+ if ( computedRowHeight !== defaultGridConfig . rowHeight ) setComputedRowHeight ( defaultGridConfig . rowHeight ) ;
473+ return ;
474+ }
475+
476+ // Available height: prefer remaining viewport below grid top so the grid
477+ // shrinks with browser height; fallback to container clientHeight
478+ const el = containerRef . current ;
479+ const rect = el ?. getBoundingClientRect ( ) ;
480+ const viewportAvailable = rect ? Math . max ( 0 , window . innerHeight - rect . top - 8 ) : 0 ;
481+ const available = viewportAvailable > 0 ? viewportAvailable : containerHeight ;
482+ if ( available <= 0 || items . length === 0 ) return ;
483+
484+ // Determine effective vertical paddings/margins from grid config
485+ const marginY = defaultGridConfig . margin [ 1 ] ;
486+ const containerPadY = defaultGridConfig . containerPadding [ 1 ] ;
487+
488+ const targetRows = Math . max ( ...items . map ( it => it . h || 1 ) ) ;
489+ const verticalGutter = marginY * Math . max ( 0 , targetRows - 1 ) ;
490+ const availableForRows = Math . max ( 0 , available - ( containerPadY * 2 ) ) ;
491+ const desired = Math . max ( 24 , Math . floor ( ( availableForRows - verticalGutter ) / ( targetRows || 1 ) ) ) ;
492+
493+ const next = Math . min ( 140 , Math . max ( 36 , desired ) ) ;
494+ if ( Number . isFinite ( next ) && next > 0 && next !== computedRowHeight ) setComputedRowHeight ( next ) ;
495+ } catch {
496+ if ( computedRowHeight !== defaultGridConfig . rowHeight ) setComputedRowHeight ( defaultGridConfig . rowHeight ) ;
497+ }
498+ } , [ displayedLayouts , currentBreakpoint , containerHeight ] ) ;
441499
442500 // Control visibility based on context
443501 const { showControls } = useControlVisibility ( mode ) ;
@@ -1596,9 +1654,9 @@ export const LayoutEngine: React.FC<LayoutEngineProps> = React.memo(({
15961654 data-grid = { item }
15971655 sx = { {
15981656 position : 'relative' ,
1599- backgroundColor : theme . palette . background . paper ,
1600- border : `1px solid ${ theme . palette . divider } ` ,
1601- borderRadius : 1 ,
1657+ backgroundColor : showControls ? theme . palette . background . paper : 'transparent' ,
1658+ border : showControls ? `1px solid ${ theme . palette . divider } ` : 'none' ,
1659+ borderRadius : showControls ? 1 : 0 ,
16021660 overflow : 'hidden' ,
16031661 transition : 'background-color 0.3s ease, border-color 0.3s ease' ,
16041662 ...( isSelected && {
@@ -1615,12 +1673,23 @@ export const LayoutEngine: React.FC<LayoutEngineProps> = React.memo(({
16151673 onRemove = { ( ) => handleItemRemove ( item . i ) }
16161674 />
16171675 ) }
1618- < ModuleRenderer
1619- pluginId = { fallbackPluginId }
1620- moduleId = { extractedModuleId }
1621- additionalProps = { item . config || { } }
1622- fallback = { < div style = { { padding : 8 } } > Loading module...</ div > }
1623- />
1676+ { ( ( ) => {
1677+ const activeLayout =
1678+ displayedLayouts [ currentBreakpoint as keyof ResponsiveLayouts ] ||
1679+ displayedLayouts . desktop || displayedLayouts . wide || [ ] ;
1680+ const wantsFullWidth = ! showControls && (
1681+ ( Array . isArray ( activeLayout ) && activeLayout . length === 1 ) ||
1682+ ( Array . isArray ( activeLayout ) && activeLayout . some ( ( it : any ) => it ?. config ?. viewportFill || it ?. config ?. fullWidth ) )
1683+ ) ;
1684+ return (
1685+ < ModuleRenderer
1686+ pluginId = { fallbackPluginId }
1687+ moduleId = { extractedModuleId }
1688+ additionalProps = { { ...( item . config || { } ) , ...( wantsFullWidth ? { viewportFill : true , centerContent : false } : { } ) } }
1689+ fallback = { < div style = { { padding : 8 } } > Loading module...</ div > }
1690+ />
1691+ ) ;
1692+ } ) ( ) }
16241693 </ Box >
16251694 ) ;
16261695 }
@@ -1646,9 +1715,9 @@ export const LayoutEngine: React.FC<LayoutEngineProps> = React.memo(({
16461715 data-grid = { item }
16471716 sx = { {
16481717 position : 'relative' ,
1649- backgroundColor : theme . palette . background . paper ,
1650- border : `1px solid ${ theme . palette . divider } ` ,
1651- borderRadius : 1 ,
1718+ backgroundColor : showControls ? theme . palette . background . paper : 'transparent' ,
1719+ border : showControls ? `1px solid ${ theme . palette . divider } ` : 'none' ,
1720+ borderRadius : showControls ? 1 : 0 ,
16521721 overflow : 'hidden' ,
16531722 transition : 'background-color 0.3s ease, border-color 0.3s ease' ,
16541723 ...( isSelected && {
@@ -1720,11 +1789,15 @@ export const LayoutEngine: React.FC<LayoutEngineProps> = React.memo(({
17201789 } ) ;
17211790 }
17221791
1792+ const wantsFullWidth = ! showControls && (
1793+ ( Array . isArray ( activeLayout ) && activeLayout . length === 1 ) ||
1794+ ( Array . isArray ( activeLayout ) && activeLayout . some ( ( it : any ) => it ?. config ?. viewportFill || it ?. config ?. fullWidth ) )
1795+ ) ;
17231796 return (
17241797 < ModuleRenderer
17251798 pluginId = { effectivePluginId }
17261799 moduleId = { effectiveModuleId }
1727- additionalProps = { item . config }
1800+ additionalProps = { { ... item . config , ... ( wantsFullWidth ? { viewportFill : true , centerContent : false } : { } ) } }
17281801 fallback = { < div style = { { padding : 8 } } > Loading module...</ div > }
17291802 />
17301803 ) ;
@@ -1749,6 +1822,23 @@ export const LayoutEngine: React.FC<LayoutEngineProps> = React.memo(({
17491822 const gridProps = useMemo ( ( ) => {
17501823 // Convert ResponsiveLayouts to the format expected by react-grid-layout
17511824 const reactGridLayouts : any = { } ;
1825+ // Determine if we should present a full-width experience (published mode, single item or explicit flag)
1826+ const bpMap : Record < string , string > = { mobile : 'xs' , tablet : 'sm' , desktop : 'lg' , wide : 'xl' , ultrawide : 'xxl' } ;
1827+ const activeLayout =
1828+ displayedLayouts [ currentBreakpoint as keyof ResponsiveLayouts ] ||
1829+ displayedLayouts . desktop || displayedLayouts . wide || [ ] ;
1830+ const wantsFullWidth = ! showControls && (
1831+ ( Array . isArray ( activeLayout ) && activeLayout . length === 1 ) ||
1832+ ( Array . isArray ( activeLayout ) && activeLayout . some ( ( it : any ) => it ?. config ?. viewportFill || it ?. config ?. fullWidth ) )
1833+ ) ;
1834+
1835+ const adjustForFullWidth = ( items : any [ ] , gridBp : string ) => {
1836+ if ( ! wantsFullWidth || ! Array . isArray ( items ) || items . length === 0 ) return items ;
1837+ const cols = ( defaultGridConfig . cols as any ) [ gridBp ] || 12 ;
1838+ // Expand the first item to full width
1839+ return items . map ( ( it : any , idx : number ) => idx === 0 ? { ...it , x : 0 , w : cols } : it ) ;
1840+ } ;
1841+
17521842 Object . entries ( displayedLayouts ) . forEach ( ( [ breakpoint , layout ] ) => {
17531843 if ( layout && Array . isArray ( layout ) && layout . length > 0 ) {
17541844 // Map breakpoint names to react-grid-layout breakpoint names
@@ -1760,7 +1850,7 @@ export const LayoutEngine: React.FC<LayoutEngineProps> = React.memo(({
17601850 ultrawide : 'xxl'
17611851 } ;
17621852 const gridBreakpoint = breakpointMap [ breakpoint ] || breakpoint ;
1763- reactGridLayouts [ gridBreakpoint ] = layout ;
1853+ reactGridLayouts [ gridBreakpoint ] = adjustForFullWidth ( layout , gridBreakpoint ) ;
17641854 }
17651855 } ) ;
17661856
@@ -1783,8 +1873,17 @@ export const LayoutEngine: React.FC<LayoutEngineProps> = React.memo(({
17831873 measureBeforeMount : false ,
17841874 transformScale : 1 ,
17851875 ...defaultGridConfig ,
1876+ rowHeight : computedRowHeight ,
1877+ containerPadding : wantsFullWidth
1878+ ? ( [ 0 , defaultGridConfig . containerPadding [ 1 ] ] as [ number , number ] )
1879+ : defaultGridConfig . containerPadding ,
1880+ margin : wantsFullWidth
1881+ ? ( [ 4 , defaultGridConfig . margin [ 1 ] ] as [ number , number ] )
1882+ : defaultGridConfig . margin ,
1883+ autoSize : false ,
1884+ style : { height : '100%' } ,
17861885 } ;
1787- } , [ displayedLayouts , mode , showControls , handleLayoutChange , handleDragStart , handleDragStop , handleResizeStart , handleResizeStop ] ) ;
1886+ } , [ displayedLayouts , mode , showControls , handleLayoutChange , handleDragStart , handleDragStop , handleResizeStart , handleResizeStop , computedRowHeight , currentBreakpoint ] ) ;
17881887
17891888 // Memoize the rendered grid items with minimal stable dependencies
17901889 const gridItems = useMemo ( ( ) => {
@@ -1804,13 +1903,30 @@ export const LayoutEngine: React.FC<LayoutEngineProps> = React.memo(({
18041903 return (
18051904 < div
18061905 className = { `layout-engine-container ${ isDragging ? 'layout-engine-container--dragging' : '' } ${ isResizing ? 'layout-engine-container--resizing' : '' } ${ isDragOver ? 'layout-engine-container--drag-over' : '' } ` }
1906+ ref = { containerRef }
18071907 onDragOver = { handleDragOver }
18081908 onDragLeave = { handleDragLeave }
18091909 onDrop = { handleDrop }
18101910 >
1811- < ResponsiveGridLayout { ...gridProps } >
1812- { gridItems }
1813- </ ResponsiveGridLayout >
1911+ { /* Centering wrapper to keep the grid balanced on wide screens */ }
1912+ { ( ( ) => {
1913+ const activeLayout =
1914+ displayedLayouts [ currentBreakpoint as keyof ResponsiveLayouts ] ||
1915+ displayedLayouts . desktop || displayedLayouts . wide || [ ] ;
1916+ const wantsFullWidth = ! showControls && (
1917+ ( Array . isArray ( activeLayout ) && activeLayout . length === 1 ) ||
1918+ ( Array . isArray ( activeLayout ) && activeLayout . some ( ( it : any ) => it ?. config ?. viewportFill || it ?. config ?. fullWidth ) )
1919+ ) ;
1920+ return (
1921+ < div className = "layout-engine-center" >
1922+ < div className = { `layout-engine-inner ${ wantsFullWidth ? 'layout-engine-inner--full' : '' } ` } >
1923+ < ResponsiveGridLayout { ...gridProps } >
1924+ { gridItems }
1925+ </ ResponsiveGridLayout >
1926+ </ div >
1927+ </ div >
1928+ ) ;
1929+ } ) ( ) }
18141930 </ div >
18151931 ) ;
18161932} , ( prevProps , nextProps ) => {
0 commit comments