From f76ee87336a9a2c92122f63b26fe81617d8c3f2d Mon Sep 17 00:00:00 2001 From: Maurice Date: Thu, 20 Aug 2026 17:41:37 +0200 Subject: [PATCH] fix(nav): stop the tab pill from running under the logo and the user menu MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The centred tab menu was absolutely positioned, so it had no relationship to what sat beside it. Its width grows with every enabled addon and every page plugin, and once it outgrew the free space in the middle of the bar it simply ran underneath the logo on one side and the bell and user menu on the other. The only adaptation was a hard-coded 1024px breakpoint that drops the tab labels — a constant chosen when two or three addons was the whole story, and one that cannot know a plugin has added anything. So the bar is three columns in the flow now: the brand block and the action cluster carry equal weight either side, and the pill sits between them. That keeps it centred on the bar as before, and makes overlap something that cannot happen rather than something a breakpoint has to predict. With min-w-0 the pill gives way and scrolls when even it runs out of room, instead of pushing the actions off the edge. Measuring the pill against the free space and folding the overflow into a menu was the other option. It buys a tidier look at a narrow width and costs a ResizeObserver on every desktop page, a cache to keep the measurement from oscillating against the mode it chose, and a new string in 23 locales. Not for a fix to overlapping. Closes #1983 --- client/src/components/Layout/Navbar.test.tsx | 60 ++++++++++++++++++++ client/src/components/Layout/Navbar.tsx | 37 +++++++++--- 2 files changed, 88 insertions(+), 9 deletions(-) diff --git a/client/src/components/Layout/Navbar.test.tsx b/client/src/components/Layout/Navbar.test.tsx index 322299ce4..4976972e1 100644 --- a/client/src/components/Layout/Navbar.test.tsx +++ b/client/src/components/Layout/Navbar.test.tsx @@ -559,3 +559,63 @@ describe('Navbar styling and menu details', () => { document.documentElement.classList.remove('trek-theme-transitioning'); }); }); + +/** + * The bar's three columns (#1983). + * + * The tab pill was absolutely positioned on the centre of the bar, so it had no + * relationship to what sat beside it. Its width grows with every enabled addon + * and every page plugin, and once it outgrew the free space in the middle it + * ran underneath the logo on one side and the user menu on the other. The only + * adaptation was a fixed 1024px breakpoint that drops the labels, tuned when + * two or three addons was the whole story and blind to plugins entirely. + * + * These check the shape rather than pixels, because that is where the defect + * was: three columns in the flow cannot overlap, whatever ends up in them, and + * no measurement has to be right for that to hold. jsdom reports every width as + * zero, so an assertion on how wide anything is would pass for the wrong reason. + */ +describe('Navbar layout (#1983)', () => { + const withAddons = (n: number) => { + const addons = Array.from({ length: n }, (_, i) => ({ + id: `addon${i}`, name: `Addon ${i}`, icon: 'CalendarDays', type: 'global' as const, enabled: true, + })); + server.use(http.get('/api/addons', () => HttpResponse.json({ addons }))); + seedStore(useAddonStore, { addons }); + }; + + it('keeps the tab pill in the flow rather than floating over its neighbours', () => { + withAddons(4); + const { container } = render(); + const pill = container.querySelector('.trek-nav-pill') as HTMLElement; + expect(pill).toBeTruthy(); + // The assertion that would have caught the overlap. + expect(pill.style.position).not.toBe('absolute'); + }); + + it('gives the columns either side of it equal weight, so it stays centred', () => { + withAddons(4); + const { container } = render(); + const nav = container.querySelector('nav') as HTMLElement; + const columns = Array.from(nav.children).filter(c => c.classList.contains('flex-1')); + // Left brand column and right action cluster, both flex-1 basis-0. + expect(columns).toHaveLength(2); + for (const c of columns) expect(c.classList.contains('basis-0')).toBe(true); + }); + + it('lets the pill shrink instead of pushing the actions off the bar', () => { + withAddons(8); + const { container } = render(); + const pill = container.querySelector('.trek-nav-pill') as HTMLElement; + expect(pill.classList.contains('min-w-0')).toBe(true); + expect(pill.style.overflowX).toBe('auto'); + }); + + it('still renders every addon as a reachable link, however many there are', () => { + withAddons(8); + render(); + for (let i = 0; i < 8; i++) { + expect(screen.getByRole('link', { name: new RegExp(`Addon ${i}`, 'i') })).toBeInTheDocument(); + } + }); +}); diff --git a/client/src/components/Layout/Navbar.tsx b/client/src/components/Layout/Navbar.tsx index f0aaa51e4..fffe57e90 100644 --- a/client/src/components/Layout/Navbar.tsx +++ b/client/src/components/Layout/Navbar.tsx @@ -110,8 +110,10 @@ export default function Navbar({ tripTitle, tripId, onBack, showBack, onShare }: height: 'var(--nav-h)', transition: 'background 240ms cubic-bezier(0.23,1,0.32,1), backdrop-filter 240ms cubic-bezier(0.23,1,0.32,1), box-shadow 240ms cubic-bezier(0.23,1,0.32,1)', }} className="hidden md:flex items-center px-4 gap-4 fixed top-0 left-0 right-0 z-[200]"> - {/* Left side */} -
+ {/* Left side. flex-1 basis-0, matching the action cluster on the right, so + the tab pill between them sits in the middle of the bar rather than + being centred on top of both (#1983). */} +
{showBack && (