feat: add FillSpace viewport-height spacer with repeating measureElement

Adds a reusable FillSpace component to dashgui that fills the remaining
viewport height between an aboveRef and optional belowRef, with a
scrollable middle section. Supports an optional measureElement that is
repeated top-to-bottom to fill the leftover space below children (rows
are rendered first, units fill height - contentHeight).

Includes mock page, docs wiring, and screenshot references.
This commit is contained in:
seb
2026-08-12 02:42:38 +02:00
parent f0e7a523da
commit 5f676ca43f
11 changed files with 571 additions and 1 deletions

View File

@@ -8,6 +8,7 @@ import TilesMockPage from './mock-pages/TilesMockPage';
import PillsMockPage from './mock-pages/PillsMockPage';
import TextFieldsMockPage from './mock-pages/TextFieldsMockPage';
import ListsMockPage from './mock-pages/ListsMockPage';
import FillSpaceMockPage from './mock-pages/FillSpaceMockPage';
const PAGES = [
{ id: 'panels', label: 'Panels', node: <PanelsMockPage /> },
@@ -15,6 +16,7 @@ const PAGES = [
{ id: 'tiles', label: 'Tiles', node: <TilesMockPage /> },
{ id: 'pills', label: 'Pills', node: <PillsMockPage /> },
{ id: 'textfields', label: 'Text fields', node: <TextFieldsMockPage /> },
{ id: 'fillspace', label: 'FillSpace', node: <FillSpaceMockPage /> },
];
/**

View File

@@ -0,0 +1,171 @@
import { Component, createRef } from 'react';
import Box from '@mui/material/Box';
import Paper from '@mui/material/Paper';
import Typography from '@mui/material/Typography';
import FillSpace from '../../FillSpace';
import { ComponentDocs } from '../ComponentDocs';
/**
* Mock page exercising FillSpace: real content on top, and a repeating
* `measureElement` filling only the leftover space down to the end of the
* viewport.
*/
class FillSpaceMockPage extends Component {
constructor(props) {
super(props);
this.ex1 = { header: createRef(), footer: createRef() };
this.ex2 = { header: createRef(), footer: createRef() };
this.ex3 = { header: createRef(), footer: createRef() };
}
render() {
return (
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 3 }}>
<Typography variant="body2" color="text.secondary">
FillSpace renders real content on top and repeats the measurement
element until the end of the page.
</Typography>
{/* Main example: content + repeating fill to end of page */}
<Paper variant="outlined" sx={{ p: 2 }}>
<Typography variant="subtitle2" gutterBottom>
Content + repeating element to end of page
</Typography>
<Box sx={{ display: 'flex', flexDirection: 'column' }}>
<Box
ref={this.ex1.header}
sx={{ bgcolor: '#e2e8f0', p: 1, borderRadius: 1 }}
>
<Typography variant="caption">Header (measured)</Typography>
</Box>
<FillSpace
aboveRef={this.ex1.header}
belowRef={this.ex1.footer}
measureElement={
<Box
sx={{
bgcolor: '#dbeafe',
p: 2,
borderRadius: 1,
textAlign: 'center',
}}
>
<Typography variant="body2">Einheit</Typography>
</Box>
}
>
<Box
sx={{
bgcolor: '#fef3c7',
p: 2,
borderRadius: 1,
}}
>
<Typography variant="body2" fontWeight={600}>
Echte Inhalte (children)
</Typography>
<Typography variant="caption" color="text.secondary">
Diese Zeilen stehen oben, darunter wiederholt sich die
Einheit bis zum Seitenende.
</Typography>
</Box>
</FillSpace>
<Box
ref={this.ex1.footer}
sx={{ bgcolor: '#e2e8f0', p: 1, borderRadius: 1 }}
>
<Typography variant="caption">Footer (measured)</Typography>
</Box>
</Box>
</Paper>
{/* Example 2: With minHeight */}
<Paper variant="outlined" sx={{ p: 2 }}>
<Typography variant="subtitle2" gutterBottom>
With minHeight constraint
</Typography>
<Box sx={{ display: 'flex', flexDirection: 'column' }}>
<Box
ref={this.ex2.header}
sx={{ bgcolor: '#e2e8f0', p: 1, borderRadius: 1 }}
>
<Typography variant="caption">Header</Typography>
</Box>
<FillSpace
aboveRef={this.ex2.header}
belowRef={this.ex2.footer}
minHeight={200}
>
<Box
sx={{
bgcolor: '#fef3c7',
p: 2,
borderRadius: 1,
}}
>
<Typography variant="body2">
Minimum height of 200px enforced.
</Typography>
</Box>
</FillSpace>
<Box
ref={this.ex2.footer}
sx={{ bgcolor: '#e2e8f0', p: 1, borderRadius: 1 }}
>
<Typography variant="caption">Footer</Typography>
</Box>
</Box>
</Paper>
{/* Example 3: Debug mode */}
<Paper variant="outlined" sx={{ p: 2 }}>
<Typography variant="subtitle2" gutterBottom>
Debug mode overlay
</Typography>
<Box sx={{ display: 'flex', flexDirection: 'column' }}>
<Box
ref={this.ex3.header}
sx={{ bgcolor: '#e2e8f0', p: 1, borderRadius: 1 }}
>
<Typography variant="caption">Header</Typography>
</Box>
<FillSpace
aboveRef={this.ex3.header}
belowRef={this.ex3.footer}
debug
>
<Box
sx={{
bgcolor: '#dcfce7',
p: 2,
borderRadius: 1,
}}
>
<Typography variant="body2">
Green background shows actual spacer bounds; purple overlay
shows full viewport coverage.
</Typography>
</Box>
</FillSpace>
<Box
ref={this.ex3.footer}
sx={{ bgcolor: '#e2e8f0', p: 1, borderRadius: 1 }}
>
<Typography variant="caption">Footer</Typography>
</Box>
</Box>
</Paper>
<ComponentDocs name="FillSpace" />
</Box>
);
}
}
export default FillSpaceMockPage;

View File

@@ -1,4 +1,4 @@
import { Component } from 'react';
import { Component, createRef } from 'react';
import Alert from '@mui/material/Alert';
import Box from '@mui/material/Box';
import Chip from '@mui/material/Chip';
@@ -15,6 +15,7 @@ import ElevatedPill, {
ToggleElevatedPill,
} from '../../ElevatedPill';
import SingleLineTextEdit from '../../SingleLineTextEdit';
import FillSpace from '../../FillSpace';
const MOCK_ROWS = [
{
@@ -292,6 +293,128 @@ class ScreenshotHarness extends Component {
color="#e2e8f0"
/>
</Section>
<Section id="fillspace" label="FillSpace">
<FillSpaceExample />
</Section>
<Section id="fillspace-measure" label="FillSpace – repeat measure element">
<FillSpaceMeasureExample />
</Section>
</Box>
);
}
}
/**
* Deterministic FillSpace example for the screenshot harness. Bound with a
* `maxHeight` so the reference is stable regardless of the viewport height.
*/
class FillSpaceExample extends Component {
constructor(props) {
super(props);
this.headerRef = createRef();
this.footerRef = createRef();
}
render() {
return (
<Box sx={{ display: 'flex', flexDirection: 'column' }}>
<Box
ref={this.headerRef}
sx={{ bgcolor: '#e2e8f0', p: 1, borderRadius: 1 }}
>
<Typography variant="caption">Header (gemessen)</Typography>
</Box>
<FillSpace
aboveRef={this.headerRef}
belowRef={this.footerRef}
minHeight={80}
maxHeight={220}
>
<Box
sx={{
bgcolor: '#dbeafe',
p: 2,
borderRadius: 1,
height: '100%',
}}
>
<Typography variant="body2">
FillSpace füllt den Platz zwischen Header und Footer.
</Typography>
</Box>
</FillSpace>
<Box
ref={this.footerRef}
sx={{ bgcolor: '#e2e8f0', p: 1, borderRadius: 1 }}
>
<Typography variant="caption">Footer (gemessen)</Typography>
</Box>
</Box>
);
}
}
/**
* FillSpace with a repeating measurement element plus real children on top,
* for the screenshot harness. Bound with maxHeight so the reference is stable.
*/
class FillSpaceMeasureExample extends Component {
constructor(props) {
super(props);
this.headerRef = createRef();
this.footerRef = createRef();
}
render() {
return (
<Box sx={{ display: 'flex', flexDirection: 'column' }}>
<Box
ref={this.headerRef}
sx={{ bgcolor: '#e2e8f0', p: 1, borderRadius: 1 }}
>
<Typography variant="caption">Header</Typography>
</Box>
<FillSpace
aboveRef={this.headerRef}
belowRef={this.footerRef}
measureElement={
<Box
sx={{
bgcolor: '#c7d2fe',
p: 1,
borderRadius: 1,
textAlign: 'center',
}}
>
<Typography variant="caption">Einheit</Typography>
</Box>
}
measureGap={8}
minHeight={60}
maxHeight={260}
>
<Box
sx={{
bgcolor: '#fef3c7',
p: 1,
borderRadius: 1,
}}
>
<Typography variant="body2">Echte Inhalte</Typography>
</Box>
</FillSpace>
<Box
ref={this.footerRef}
sx={{ bgcolor: '#e2e8f0', p: 1, borderRadius: 1 }}
>
<Typography variant="caption">Footer</Typography>
</Box>
</Box>
);
}