ui: a Back button is a row of one, not a stretched flex child

Reported: the Back button fills the page width on the domain and channel edit
pages.  It fills it on all nine pages that have one, and this is mine from the
last PR.

`.wrolpi-stack` is a flex column at `align-items: stretch`, deliberately -- a
panel was full-width as a block element and has to stay that way.  A BARE button
as a direct child of that stack is stretched too, and every page's Back button
was one.  On the domain, channel and playlist edit pages it also pushed the
button beside it onto its own line, where the two had been inline.

Wrapping each in a `.wrolpi-button-row` fixes both without touching the stack:
the row is the stretched child, and it lays its buttons out at their own width.
The three pages that have a second button get one row holding both, which is how
they read before.

Also the Videos, Archives and Documents select bars, whose Tag/Delete/Invert/
Clear were four touching buttons in a bare div.  They are inline elements rather
than interpolations, which is the shape the bare-siblings scan does not see.

Guarded twice: the row keeps its buttons their own width inside a stack while
itself stretching (both halves, since a row that refused to stretch would pass a
test that only looked at the button), and every `<BackButton/>` in the app is
inside a row.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
Roland 2026-08-05 16:47:33 -06:00
parent 7741ee1f12
commit 076e982b69
No known key found for this signature in database
GPG key ID: 1BA620CE6C9A4680
8 changed files with 88 additions and 21 deletions

View file

@ -254,7 +254,7 @@ function ArchivePage() {
const labelStyle = {whiteSpace: 'nowrap', paddingRight: '1.5em', verticalAlign: 'top'};
return <>
<BackButton/>
<div className='wrolpi-button-row'><BackButton/></div>
<Panel>
{screenshot}
@ -619,7 +619,7 @@ export function DomainEditPage() {
if (!form.ready) {
if (form.error) {
return <>
<BackButton/>
<div className='wrolpi-button-row'><BackButton/></div>
<Message kind='error' title='Domain not found'>
<p>{form.error}</p>
</Message>
@ -685,10 +685,12 @@ export function DomainEditPage() {
const [descriptionProps] = form.getCustomProps({name: 'description', path: 'description'});
return <>
<BackButton/>
<Link to={`/archives?domain=${domain?.domain}`}>
<Button>Archives</Button>
</Link>
<div className='wrolpi-button-row'>
<BackButton/>
<Link to={`/archives?domain=${domain?.domain}`}>
<Button>Archives</Button>
</Link>
</div>
{domain?.needs_reorganization && (
<Message kind='warning' title='File Format Changed'>
@ -1171,7 +1173,7 @@ function ArchivesPage() {
setSelectedArchives([]);
}
const selectElm = <div style={{marginTop: '0.5em'}}>
const selectElm = <div className='wrolpi-button-row' style={{marginTop: '0.5em'}}>
<Button
role='primary'
disabled={_.isEmpty(selectedArchives)}

View file

@ -209,10 +209,12 @@ export function ChannelEditPage() {
const downloadMissingDataLabel = <>Download Missing Data<InfoPopup content={downloadMissingDataInfo}/></>;
return <>
<BackButton/>
<Link to={`/videos/channel/${channel.id}/video`}>
<Button>Videos</Button>
</Link>
<div className='wrolpi-button-row'>
<BackButton/>
<Link to={`/videos/channel/${channel.id}/video`}>
<Button>Videos</Button>
</Link>
</div>
{channel?.needs_reorganization && (
<Message kind='warning' title='File Format Changed'>
@ -419,7 +421,7 @@ export function ChannelNewPage() {
}
return <>
<BackButton/>
<div className='wrolpi-button-row'><BackButton/></div>
<Panel>
<Header as="h1">New Channel</Header>

View file

@ -1002,6 +1002,41 @@ describe('a row of buttons', () => {
expect(offenders).toEqual([]);
});
it('puts every Back button in a row, so the page stack cannot stretch it', () => {
/*
* A page's blocks are stacked by `.wrolpi-stack`, which is a flex column at
* `align-items: stretch` -- deliberately, because a panel was full-width as a block
* element and has to stay that way. A BARE button as a direct child of that stack is
* stretched too, and every page's Back button was one: full-page-width, and on the domain,
* channel and playlist edit pages the button beside it was pushed onto its own line where
* the two had been inline before.
*
* A row fixes it without touching the stack: the row is the stretched child, and it lays
* its buttons out at their own width. Nine sites, so this scan rather than nine
* assertions -- and a scan because a stretched button is still a working button.
*/
const offenders = [];
let found = 0;
for (const file of sourceFiles()) {
const text = fs.readFileSync(file, 'utf8');
const tags = tagsIn(text);
for (const match of text.matchAll(/<BackButton\b/g)) {
found += 1;
if (!isRow(enclosingTag(tags, match.index))) {
offenders.push(`${path.relative(SRC, file)}:${
text.slice(0, match.index).split('\n').length}`);
}
}
}
// Nine renders. Common.js declares it, and a declaration is not a `<BackButton`.
expect(found).toBe(9);
expect(offenders).toEqual([]);
});
});
/** The app's own source, minus its tests: what a source scan is allowed to read. */

View file

@ -83,7 +83,7 @@ function DocsPage() {
setSelectedDocs([]);
}
const selectElm = <div style={{marginTop: '0.5em'}}>
const selectElm = <div className='wrolpi-button-row' style={{marginTop: '0.5em'}}>
<Button color='violet' disabled={_.isEmpty(selectedDocs)}
onClick={() => setBulkTagOpen(true)}>Tag</Button>
<APIButton
@ -339,7 +339,7 @@ function DocPage() {
</table>;
return <>
<BackButton/>
<div className='wrolpi-button-row'><BackButton/></div>
{isCbz && activePath && <CbzViewer path={activePath}/>}

View file

@ -346,7 +346,7 @@ export function PlaylistViewPage() {
const items = playlist.items || [];
return <>
<BackButton/>
<div className='wrolpi-button-row'><BackButton/></div>
<Header as='h1'>
{playlist.name}
@ -498,10 +498,12 @@ export function PlaylistEditPage() {
const form = {error: null, loading: false, disabled: !editable || !name.trim(), ready: true};
return <>
<BackButton/>
<Link to={`/playlists/${playlistId}`}>
<Button>View</Button>
</Link>
<div className='wrolpi-button-row'>
<BackButton/>
<Link to={`/playlists/${playlistId}`}>
<Button>View</Button>
</Link>
</div>
<CollectionEditForm
form={form}

View file

@ -397,7 +397,7 @@ function VideoPage({videoFile, prevFile, nextFile, fetchVideo, ...props}) {
const audioPlayPauseLabel = audioPlaying ? 'Pause' : 'Play';
return <>
<div style={{margin: '1em'}}>
<div className='wrolpi-button-row' style={{margin: '1em'}}>
<BackButton/>
</div>

View file

@ -175,7 +175,7 @@ export function VideosPage() {
setSelectedVideos([]);
}
const selectElm = <div style={{marginTop: '0.5em'}}>
const selectElm = <div className='wrolpi-button-row' style={{marginTop: '0.5em'}}>
<Button
role='primary'
disabled={_.isEmpty(selectedVideos)}

View file

@ -390,6 +390,32 @@ describe('a row of buttons is spaced and aligned by the row', () => {
});
});
it('keeps its buttons their own width inside a page stack', () => {
/*
* `.wrolpi-stack` is a flex column at `align-items: stretch`, which is what keeps a panel
* full-width as it was as a block element -- and which stretched every page's Back button
* to the full width of the page, because a bare button was a direct child of it.
*
* The row is what absorbs that: the ROW is the stretched child, and it lays its buttons out
* at their own width. Both halves are asserted, because a row that also refused to stretch
* would pass a test that only looked at the button.
*/
cy.mountUI(<div className='wrolpi-stack'>
<div className='wrolpi-button-row'><Button>Back</Button></div>
<Panel>The page</Panel>
</div>);
cy.get('.wrolpi-stack').should(($stack) => {
const stack = $stack[0].getBoundingClientRect();
const row = $stack[0].querySelector('.wrolpi-button-row').getBoundingClientRect();
const button = $stack[0].querySelector('button').getBoundingClientRect();
expect(stack.width, 'the stack has a width to fill').to.be.greaterThan(200);
expect(row.width, 'the row stretches, as the panel does').to.be.closeTo(stack.width, 0.5);
expect(button.width, 'the button does not').to.be.lessThan(stack.width / 2);
});
});
it('grows its gap with the interface scale', () => {
cy.mountUI(<div className='wrolpi-button-row'><Button>One</Button><Button>Two</Button></div>);