Skip to content

Commit 5ce034f

Browse files
authored
Merge pull request #72 from BrainDriveAI/feature/dynamic-page-responsive
Feature: Responsive dynamic page layout with adaptive grid and centered rendering
2 parents cc25ac2 + f5c4185 commit 5ce034f

File tree

5 files changed

+254
-23
lines changed

5 files changed

+254
-23
lines changed

frontend/src/components/dashboard/DashboardLayout.tsx

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { useEffect, useState } from 'react';
22
import { Box, Toolbar, useMediaQuery, useTheme } from '@mui/material';
3-
import { Outlet } from 'react-router-dom';
3+
import { Outlet, useLocation } from 'react-router-dom';
44
import Header from './Header';
55
import Sidebar from './Sidebar';
66
import { ThemeSelector } from '../ThemeSelector';
@@ -13,6 +13,7 @@ const DashboardLayout = () => {
1313
const isMobile = useMediaQuery(theme.breakpoints.down('sm'));
1414
const [sidebarOpen, setSidebarOpen] = useState(!isMobile);
1515
const settingsService = useSettings();
16+
const location = useLocation();
1617
const defaultCopyright = { text: 'AIs can make mistakes. Check important info.' };
1718
const [copyright, setCopyright] = useState(defaultCopyright);
1819

@@ -59,6 +60,8 @@ const DashboardLayout = () => {
5960
setSidebarOpen(!sidebarOpen);
6061
};
6162

63+
const isDynamicPage = location.pathname.startsWith('/pages/');
64+
6265
return (
6366
<Box sx={{ display: 'flex', minHeight: '100vh' }}>
6467
<Header
@@ -74,8 +77,12 @@ const DashboardLayout = () => {
7477
<Box
7578
component="main"
7679
sx={{
80+
// CSS vars for downstream layout sizing
81+
'--app-header-h': { xs: '56px', sm: '64px' },
82+
'--app-footer-h': '32px',
7783
flexGrow: 1,
78-
p: { xs: 1, sm: 2 },
84+
// Reduce padding for dynamic pages to maximize real estate
85+
p: isDynamicPage ? { xs: 0, sm: 0 } : { xs: 1, sm: 2 },
7986
width: '100%',
8087
display: 'flex',
8188
flexDirection: 'column',

frontend/src/features/unified-dynamic-page-renderer/components/LayoutEngine.tsx

Lines changed: 134 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -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) => {

frontend/src/features/unified-dynamic-page-renderer/components/ModuleRenderer.tsx

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -467,13 +467,25 @@ const UnifiedModuleRenderer: React.FC<UnifiedModuleRendererProps> = ({
467467
});
468468
}
469469

470-
// Render the component with error boundary - same as DynamicPluginRenderer
470+
// Determine layout options (safe defaults)
471+
const centerContent: boolean = (additionalProps as any)?.centerContent !== false;
472+
const viewportFill: boolean = (additionalProps as any)?.viewportFill === true;
473+
474+
// Render the component with error boundary and a centering wrapper
471475
return (
472476
<ComponentErrorBoundary
473477
fallback={fallback}
474478

475479
>
476-
<Component {...module.props} />
480+
<div
481+
className={[
482+
'module-content',
483+
!centerContent && 'module-content--no-center',
484+
viewportFill && 'module-content--fill',
485+
].filter(Boolean).join(' ')}
486+
>
487+
<Component {...module.props} />
488+
</div>
477489
</ComponentErrorBoundary>
478490
);
479491
} catch (renderError) {

frontend/src/features/unified-dynamic-page-renderer/components/UnifiedPageRenderer.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@ import { ErrorBoundary } from './ErrorBoundary';
77
import { PageProvider } from '../contexts/PageContext';
88
import { usePageLoader } from '../hooks/usePageLoader';
99
import { useErrorHandler } from '../hooks/useErrorHandler';
10+
// Load renderer styles
11+
import '../styles/index.css';
1012

1113
export interface UnifiedPageRendererProps {
1214
// Page identification

0 commit comments

Comments
 (0)