From c643f90c193e4d92da1d6e4e926ebf3af250692c Mon Sep 17 00:00:00 2001 From: Roland Date: Sun, 2 Aug 2026 13:00:56 -0600 Subject: [PATCH] ui: centre the pagination strip It sat hard against the left edge of its container despite three call sites wrapping it in `
` or `text-align: center`. Mantine builds a Pagination as a BLOCK root -- full width, so those wrappers had nothing to centre -- holding a flex Group that carries the controls and lays them out at `flex-start`. So `justify-content` on the root does nothing at all; it has to reach the Group. Setting `--group-justify` on the root does not work either, which cost a detour: Group defaults that prop and writes the variable INLINE on its own element, where an inherited value cannot reach it. The property itself is fair game, since Mantine only ever reads the variable from a hashed class, so `.wrolpi-pagination .mantine-Group-root { justify-content: center }` at (0,2,0) is what lands. The three wrappers are now plain divs keeping their margin. `
` is a deprecated element that was doing nothing here. Two tests: the gap either side is equal -- measured, not read off `justify-content` -- with an assertion that the container really is wider than the strip, since otherwise the equality holds trivially at 0 and 0. And an inverse: at 240px nothing hangs off the LEFT edge. That one is the risk centring introduces -- overflow in a flex-start row spills right, where it can be scrolled to, but centred it spills both ways and the first page becomes unreachable. It passed before this change and must keep passing after. Verified red first (0px left against 230px right), and the wrong-element fix -- centring the root rather than the Group -- reddens it again. 1080 jest, 160 in ui-layout.cy.js, clean build. Co-Authored-By: Claude Opus 5 (1M context) --- app/src/components/Files.js | 4 +- app/src/components/MapSearchView.js | 2 +- app/src/components/Zim.js | 4 +- app/src/components/ui/ui-layout.cy.js | 53 ++++++++++++++++++++++++++- app/src/components/ui/ui.css | 22 +++++++++++ 5 files changed, 79 insertions(+), 6 deletions(-) diff --git a/app/src/components/Files.js b/app/src/components/Files.js index 2b8bd23a..499ea9ab 100644 --- a/app/src/components/Files.js +++ b/app/src/components/Files.js @@ -333,9 +333,9 @@ export function FilesView( ) { const {view} = useSearchView(); - const paginator =
+ const paginator =
-
; + ; let body; if (view === 'list') { diff --git a/app/src/components/MapSearchView.js b/app/src/components/MapSearchView.js index e0ec66b8..106cd66b 100644 --- a/app/src/components/MapSearchView.js +++ b/app/src/components/MapSearchView.js @@ -124,7 +124,7 @@ export function MapSearchView() { )} -
+
; diff --git a/app/src/components/Zim.js b/app/src/components/Zim.js index b7e18f7a..83fc0316 100644 --- a/app/src/components/Zim.js +++ b/app/src/components/Zim.js @@ -223,9 +223,9 @@ const ZimAccordionItem = ({value, active, data, searchStr, activeTags}) => { } } - const paginator =
+ const paginator =
-
; +
; const label = ; diff --git a/app/src/components/ui/ui-layout.cy.js b/app/src/components/ui/ui-layout.cy.js index be7348df..13db0e9a 100644 --- a/app/src/components/ui/ui-layout.cy.js +++ b/app/src/components/ui/ui-layout.cy.js @@ -1,7 +1,7 @@ import React from 'react'; import { ActionInput, Button, Card, Header, Icon, IconButton, IconStack, Loading, MultiSelect, Panel, PathInput, - Message, Placeholder, Progress, SearchBox, Statistic, StatisticGroup, Status, TabBar, Table, + Message, Pagination, Placeholder, Progress, SearchBox, Statistic, StatisticGroup, Status, TabBar, Table, tabClassName, TextInput, } from './index'; import {MemoryRouter} from 'react-router'; @@ -2020,3 +2020,54 @@ describe('the search clear button matches the field it clears', () => { }); }); }); + +describe('pagination sits in the middle of its container', () => { + /* + * Three call sites wrap the pager in `
` or `text-align: center` and it still sat + * hard against the left edge. Both of those centre INLINE content, and Mantine's + * Pagination root is a flex container that fills its parent's width -- so the buttons + * were laid out at `flex-start` inside a box that was already the full width, and no + * amount of centring outside it could move them. + * + * Measured as the gap on each side rather than by reading `justify-content`, so the + * claim is about where the control actually lands. + */ + it('leaves the same gap either side', () => { + cy.mountUI(
+ {}}/> +
); + + cy.get('.wrolpi-pagination').should(($pager) => { + const container = $pager[0].getBoundingClientRect(); + const buttons = [...$pager[0].querySelectorAll('button')] + .map(button => button.getBoundingClientRect()); + expect(buttons.length, 'the pager rendered its controls').to.be.greaterThan(3); + + const left = Math.min(...buttons.map(b => b.left)) - container.left; + const right = container.right - Math.max(...buttons.map(b => b.right)); + // The premise: the container really is wider than the strip, so there is slack + // to distribute. Without this the equality holds trivially at 0 and 0. + expect(left + right, 'the container is wider than the strip').to.be.greaterThan(40); + expect(left, 'gap left vs gap right').to.be.closeTo(right, 1); + }); + }); + + it('does not push its first page off the left edge when space is tight', () => { + /* + * The risk centring introduces. Overflow in a `flex-start` row spills to the right, + * where it can at least be scrolled to; centred, it spills BOTH ways and the first + * page number is unreachable. 240px is narrower than any real layout gives it. + */ + cy.mountUI(
+ {}} showFirstAndLast/> +
); + + cy.get('.wrolpi-pagination').should(($pager) => { + const container = $pager[0].getBoundingClientRect(); + const buttons = [...$pager[0].querySelectorAll('button')] + .map(button => button.getBoundingClientRect()); + expect(Math.min(...buttons.map(b => b.left)), 'nothing hangs off the left') + .to.be.at.least(container.left - 0.5); + }); + }); +}); diff --git a/app/src/components/ui/ui.css b/app/src/components/ui/ui.css index d9115edd..f398c087 100644 --- a/app/src/components/ui/ui.css +++ b/app/src/components/ui/ui.css @@ -190,6 +190,28 @@ html[data-theme="amber"] .wrolpi-theme-option-active { /* ------------------------------------------------------------ Pagination */ +/* + * The pager is centred here rather than by whoever renders it. Three call sites wrapped it + * in `
` or `text-align: center` and it still sat hard against the left edge. + * + * Mantine builds a Pagination as a BLOCK root -- full width, so those wrappers had nothing + * to centre -- holding a flex Group that carries the controls and lays them out at + * `flex-start`. `justify-content` on the root therefore does nothing at all; it has to + * reach the Group. + * + * Setting `--group-justify` on the root does not work either: Group defaults that prop and + * writes the variable INLINE on its own element, where an inherited value cannot reach it. + * The property itself is fair game, though -- Mantine only ever reads the variable from a + * hashed class -- so this rule at (0,2,0) is what actually lands. + * + * The Group already wraps by default (`--group-wrap` falls back to `wrap`), which matters + * once the row is centred: centred overflow spills off BOTH edges, and the first page would + * be unreachable rather than merely off-screen to the right. + */ +.wrolpi-pagination .mantine-Group-root { + justify-content: center; +} + .wrolpi-pagination button { background: var(--panel); color: var(--text);