From bb44530649702a94ca9e28e883e0520dd412cc7a Mon Sep 17 00:00:00 2001 From: Kamil Krzywda Date: Tue, 6 Oct 2026 12:51:11 +0000 Subject: [PATCH 1/4] =?UTF-8?q?fix(ui):=20move=20NEXT=20TURN=20into=20the?= =?UTF-8?q?=20right=20pane=20=E2=80=94=20full=20width,=20no=20longer=20ove?= =?UTF-8?q?rlapped=20(#52)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- components/mission-control.tsx | 18 +++++++++++------- components/next-turn-button.tsx | 27 ++++++++++++++------------- 2 files changed, 25 insertions(+), 20 deletions(-) diff --git a/components/mission-control.tsx b/components/mission-control.tsx index 03e874e..159d54f 100644 --- a/components/mission-control.tsx +++ b/components/mission-control.tsx @@ -529,6 +529,14 @@ export function MissionControl() { )} + + {/* #52 — the manual tick control (5b), moved in from the fixed bottom-right overlay: + last child of the pane's flex column — full width, size MD, pinned to the pane + bottom when the content is short (the wrapper's marginTop:auto) and scrolling + with the pane when the cargo list is long. In normal flow, the active-tasks strip + can no longer paint over it; the pane (unlike the tab content) is outside Radix's + unmount, so it stays visible on every tab. */} + @@ -567,13 +575,9 @@ export function MissionControl() { SYS ONLINE - {/* #44 — the manual tick control (5b): fixed bottom-right, outside the tab grid so it - stays visible on every tab (Radix unmounts inactive tabs — the button only refreshes - the snapshots this shell owns) */} - - - {/* #44 — the active-tasks strip: fixed bottom-left (same mount convention as the button — - outside the tab grid so it stays visible on every tab). Data: the shell's system + {/* #44 — the active-tasks strip: fixed bottom-left, outside the tab grid so it stays + visible on every tab. (#52 moved the NEXT TURN button out of this fixed-overlay area + and into the right pane, so the two can no longer overlap.) Data: the shell's system snapshot (JC-15) — refreshAll re-runs /api/system on every turn, so it updates on every shell refresh with no fetch of its own. */} diff --git a/components/next-turn-button.tsx b/components/next-turn-button.tsx index 9d5d10e..60b2e3b 100644 --- a/components/next-turn-button.tsx +++ b/components/next-turn-button.tsx @@ -3,12 +3,16 @@ import { useState } from 'react' import { Button } from '@/components/ui/button' -// NEXT TURN — the manual tick control (5b): a fixed bottom-right button + the current turn from -// the ship snapshot the shell holds. One click = POST /api/turn (no body); on success the shell -// re-fetches its snapshots. Review Focus 4: the button is NEVER disabled by game state — an -// empty queue is a FREE SKIP (the route always returns 200) — and the only transient is an -// in-flight request, which the handler ignores. `disabled` is never set, so the cva -// disabled:opacity-40 styling never applies either. +// NEXT TURN — the manual tick control (5b): a normal-flow block in the right pane (#52) carrying +// the current turn from the ship snapshot the shell holds. One click = POST /api/turn (no body); +// on success the shell re-fetches its snapshots. Review Focus 4: the button is NEVER disabled by +// game state — an empty queue is a FREE SKIP (the route always returns 200) — and the only +// transient is an in-flight request, which the handler ignores. `disabled` is never set, so the +// cva disabled:opacity-40 styling never applies either. +// #52 — the fixed bottom-right overlay became the right pane's last flex child (mission-control): +// full width, size MD, pinned to the pane bottom when the content is short (marginTop:auto) and +// scrolling with the pane when the cargo list is long. No position/zIndex any more — so the +// active-tasks strip can no longer paint over it. export function NextTurnButton({ turn, onTicked }: { turn: number | null; onTicked: () => void }) { const [inFlight, setInFlight] = useState(false) @@ -30,14 +34,11 @@ export function NextTurnButton({ turn, onTicked }: { turn: number | null; onTick return (
{error && ( @@ -46,7 +47,7 @@ export function NextTurnButton({ turn, onTicked }: { turn: number | null; onTick TURN {turn ?? '—'} -
-- 2.54.0 From ca590c7b532a853eb16021c6c493c93712bab92e Mon Sep 17 00:00:00 2001 From: Kamil Krzywda Date: Tue, 6 Oct 2026 12:51:11 +0000 Subject: [PATCH 2/4] test(e2e): pin NEXT TURN full-width and no-overlap with the active-tasks strip (#52) --- e2e/smoke.spec.ts | 44 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 44 insertions(+) diff --git a/e2e/smoke.spec.ts b/e2e/smoke.spec.ts index e2a14d4..8042a23 100644 --- a/e2e/smoke.spec.ts +++ b/e2e/smoke.spec.ts @@ -1046,3 +1046,47 @@ test('NEXT TURN with an empty queue is a free skip — +1 turn, the strip stays expect(after.turn).toBe(T + 1); expect(after.tasks.length).toBe(0); }); + +test('NEXT TURN button is full-width in the right pane and not covered by the task strip (#52)', async ({ page }) => { + await drainQueue(page); + const world = (await (await page.request.get('/api/world')).json()) as WorldStateQ; + const currentId = world.systems.find((s) => s.current)!.id; + const edge = world.edges.find(([a, b]) => a === currentId || b === currentId)!; + const targetId = edge[0] === currentId ? edge[1] : edge[0]; + const { crew } = (await (await page.request.get('/api/crew')).json()) as { crew: CrewDto[]; tasks: QueueTaskDto[] }; + expect(crew.length).toBeGreaterThanOrEqual(2); + + // Queue a one-person JUMP and deliberately do not tick it (the #44 jump scenarios' pattern, + // the in-transit 409 one leaves it that way on purpose): the strip renders from the shell's + // system snapshot — the same queue — so a non-empty queue keeps ACTIVE TASKS visible all test. + const res = await page.request.post('/api/jump', { data: { systemId: targetId, leadCrewId: crew[0].id, helperCrewIds: [] } }); + expect(res.status()).toBe(200); + const queued = (await res.json()) as WorldStateQ; + expect(queued.tasks.find((t) => t.type === 'JUMP')!.status).toBe('ACTIVE'); + + await page.goto('/'); + await expect(page.getByText('ACTIVE TASKS', { exact: true })).toBeVisible(); // the strip is up + + // Two rects intersect when their x AND y ranges overlap (touching edges do not count). + type Box = { x: number; y: number; width: number; height: number }; + const intersects = (a: Box, b: Box) => a.x < b.x + b.width && b.x < a.x + a.width && a.y < b.y + b.height && b.y < a.y + a.height; + + const buttonBox = await page.getByRole('button', { name: 'NEXT TURN' }).boundingBox(); + expect(buttonBox).not.toBeNull(); + // The strip's root div is the parent of its 'ACTIVE TASKS' label (components/task-strip.tsx). + const stripBox = await page.getByText('ACTIVE TASKS', { exact: true }).locator('xpath=..').boundingBox(); + expect(stripBox).not.toBeNull(); + // The CARGO HOLD panel: the Panel root is the h3 heading's grandparent div (components/ui/panel.tsx). + const panelBox = await page.getByRole('heading', { name: 'CARGO HOLD // INVENTORY' }).locator('xpath=ancestor::div[2]').boundingBox(); + expect(panelBox).not.toBeNull(); + + // (a) The button spans the right pane's content width — >= 280 on its own, and within ±8px of + // the CARGO HOLD panel, a sibling full-width element in the same pane column. + expect(buttonBox!.width).toBeGreaterThanOrEqual(280); + expect(Math.abs(buttonBox!.width - panelBox!.width)).toBeLessThanOrEqual(8); + + // (b) The button's box does not intersect the active-tasks strip's box. + expect(intersects(buttonBox!, stripBox!)).toBe(false); + + await drainQueue(page); // leave the queue clean for the next scenario +}); -- 2.54.0 From 3ea2d1c38bb5e63eeffb30da76348dcd425cc0ea Mon Sep 17 00:00:00 2001 From: Kamil Krzywda Date: Tue, 6 Oct 2026 12:51:11 +0000 Subject: [PATCH 3/4] =?UTF-8?q?chore(release):=20v0.14.0=20=E2=80=94=20NEX?= =?UTF-8?q?T=20TURN=20button=20in=20the=20right=20pane=20(#52)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- CHANGELOG.md | 5 +++++ package.json | 2 +- 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1a28b8b..0b0e456 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,10 @@ # Changelog +## v0.14.0 — 2026-10-06 12:50 +- The NEXT TURN button now lives inside the right pane (full width of the pane, larger) instead of floating as a fixed bottom-right overlay — it can no longer be covered by the active-tasks strip (#52). +- The button grew from the SM to the MD size and spans the whole pane content width; it pins to the pane bottom when the content is short and scrolls with it when the cargo list is long. The `TURN ` counter, the EXEC styling and the in-flight behaviour are unchanged. +- E2E: new spec pins the button full-width in the right pane (≥ 280px, within ±8px of the CARGO HOLD panel) and asserts its bounding box does not intersect the active-tasks strip's while a task is queued. + ## v0.13.0 — 2026-10-06 12:17 - Jump spread limit (#51): jumping into an undiscovered system now mints a seeded-random 1–4 new systems (was a fixed 4). Each leg can instead attach an already-known system (closing a loop) with probability 0.35, so the chart grows slower and more circular. A frontier guarantee ensures at least one new system is always minted. - `ringEdges` small-n fix: n=1 no longer emits a `[1,1]` self-loop, n=2 no longer emits a duplicate edge. n≥3 output is byte-identical to the legacy wheel. diff --git a/package.json b/package.json index 5b1761e..c3f260b 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "space-wanderer", - "version": "0.13.0", + "version": "0.14.0", "private": true, "scripts": { "dev": "next dev -p 3000", -- 2.54.0 From 34e8fd8514e1cdcb9c6b8f3c2078b558a141aaaa Mon Sep 17 00:00:00 2001 From: Kamil Krzywda Date: Tue, 6 Oct 2026 13:03:21 +0000 Subject: [PATCH 4/4] =?UTF-8?q?chore(ui):=20address=20review=20nits=20?= =?UTF-8?q?=E2=80=94=20stale=20strip=20comment,=20test=20assertion=20(#52)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- components/task-strip.tsx | 3 ++- e2e/smoke.spec.ts | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/components/task-strip.tsx b/components/task-strip.tsx index 92294c2..d64a99d 100644 --- a/components/task-strip.tsx +++ b/components/task-strip.tsx @@ -2,7 +2,8 @@ import type { QueueDto } from '@/lib/game/save' -// TASK STRIP — the save's task queue, fixed bottom-left next to the NEXT TURN button (#44). +// TASK STRIP — the save's task queue, fixed bottom-left (#44); the NEXT TURN button moved into +// the right pane in #52, so the two can no longer overlap. // The array arrives pre-ordered from the snapshot builder (Task 6: ACTIVE block, then PENDING // block, each by (queuedAtTurn, id)) — the strip renders it as-is, dimming the PENDING rows, // and never fetches or re-sorts. Empty queue → null, no box at all. The shell re-renders it on diff --git a/e2e/smoke.spec.ts b/e2e/smoke.spec.ts index 8042a23..e7fce84 100644 --- a/e2e/smoke.spec.ts +++ b/e2e/smoke.spec.ts @@ -1054,7 +1054,7 @@ test('NEXT TURN button is full-width in the right pane and not covered by the ta const edge = world.edges.find(([a, b]) => a === currentId || b === currentId)!; const targetId = edge[0] === currentId ? edge[1] : edge[0]; const { crew } = (await (await page.request.get('/api/crew')).json()) as { crew: CrewDto[]; tasks: QueueTaskDto[] }; - expect(crew.length).toBeGreaterThanOrEqual(2); + expect(crew.length).toBeGreaterThanOrEqual(1); // Queue a one-person JUMP and deliberately do not tick it (the #44 jump scenarios' pattern, // the in-transit 409 one leaves it that way on purpose): the strip renders from the shell's -- 2.54.0