From 1710e50b63dc798f826af2ce135033c6a44bf5be Mon Sep 17 00:00:00 2001 From: Kamil Krzywda Date: Tue, 29 Sep 2026 10:22:14 +0000 Subject: [PATCH] fix(ui): pin the OPERATIONS tab bar while panel content scrolls (#31) The TabsList is now position:sticky top:0 with a z-index above the panel content. In wide mode it pins to the top of the OPERATIONS scroll area (#21's center-pane scroller); in narrow mode, where the body scrolls and there is no inner scroll container, it pins to the viewport. The existing opaque background keeps scrolled content from showing through. --- CHANGELOG.md | 4 ++++ components/mission-control.tsx | 5 ++++- e2e/smoke.spec.ts | 30 ++++++++++++++++++++++++++++++ package.json | 2 +- 4 files changed, 39 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 297353c..7f00542 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,9 @@ # Changelog +## v0.9.8 — 2026-09-29 10:20 +- The OPERATIONS tab bar now stays pinned while its content scrolls (#31): the TabsList is `position: sticky; top: 0` with a z-index above the panel content, so it pins to the top of the OPERATIONS scroll area in wide mode (where only the center pane scrolls, #21) and to the viewport when the body scrolls in narrow mode. Its existing opaque background (`--surface-raised`) keeps scrolled content from showing through; no layout restructure needed — #21 behavior is untouched. +- e2e: new smoke spec — shrink the viewport so WORLD MAP's fixed-height chart overflows the pane, scroll the panel to the bottom (asserting a real offset), and check the tablist's bounding box hasn't moved. + ## v0.9.7 — 2026-09-29 09:59 - LOCAL SYSTEM no longer shows the previous system after a jump (#30): WorldMapPanel owns the world state and applied the `/api/jump` response locally, so MissionControl's lifted `/api/system` state (which drives the LOCAL SYSTEM tab, CARGO HOLD and the SECTOR row) went stale until a page refresh. The panel now takes an `onJumped` callback that MissionControl wires to a `/api/system` refetch after every successful jump — same failure policy as the mount fetch (keep old state on error). - e2e: new smoke spec — click-to-jump on WORLD MAP, wait for the chart's turn counter to update, switch to LOCAL SYSTEM and assert its header shows the NEW current system from `/api/world` (no `page.reload()` workaround; one jump per run/retry, same pattern as the existing jump test). diff --git a/components/mission-control.tsx b/components/mission-control.tsx index b74face..f5f8bf3 100644 --- a/components/mission-control.tsx +++ b/components/mission-control.tsx @@ -393,8 +393,11 @@ export function MissionControl() { WORLD MAP diff --git a/e2e/smoke.spec.ts b/e2e/smoke.spec.ts index 981ed81..9034524 100644 --- a/e2e/smoke.spec.ts +++ b/e2e/smoke.spec.ts @@ -82,6 +82,36 @@ test('active tab survives a page reload (#27)', async ({ page }) => { await expect(page.getByRole('tab', { name: 'CREW' })).toHaveAttribute('aria-selected', 'true'); }); +test('tab bar stays pinned while panel content scrolls (#31)', async ({ page }) => { + await page.goto('/'); + // Shrink the viewport so the center pane's content overflows its scroll area regardless of save state. + await page.setViewportSize({ width: 1280, height: 560 }); + + // WORLD MAP is always tall enough to overflow (fixed-height chart) — deterministic across retries. + await page.getByRole('tab', { name: 'WORLD MAP' }).click(); + + const tablist = page.getByRole('tablist'); + await expect(tablist).toBeVisible(); + const before = await tablist.boundingBox(); + expect(before).not.toBeNull(); + + // Scroll the nearest scroll container above the tab list all the way down. + const scrolled = await page.evaluate(() => { + let el = document.querySelector('[role="tablist"]')?.parentElement ?? null; + while (el && !['auto', 'scroll'].includes(getComputedStyle(el).overflowY)) el = el.parentElement; + if (!el) throw new Error('no scroll container above the tab list'); + el.scrollTop = el.scrollHeight; + return el.scrollTop; + }); + expect(scrolled).toBeGreaterThan(50); // content really overflowed — guards against a vacuous pass + + await expect(tablist).toBeVisible(); + const after = await tablist.boundingBox(); + expect(after).not.toBeNull(); + // Pinned: the tab bar's viewport position is unchanged even though the content scrolled away (#31). + expect(Math.abs(after!.y - before!.y)).toBeLessThan(2); +}); + test('world map shows the generated starting system and its neighbours', async ({ page }) => { const world = (await (await page.request.get('/api/world')).json()) as WorldState; diff --git a/package.json b/package.json index 207a31d..4fd69a5 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "space-wanderer", - "version": "0.9.7", + "version": "0.9.8", "private": true, "scripts": { "dev": "next dev -p 3000", -- 2.54.0