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 && (