Merge pull request 'fix: NEXT TURN button into the right pane — full width, no longer overlapped (#52, v0.14.0)' (#54) from feat/next-turn-button into master
CI / build + unit + e2e (push) Successful in 3m5s
CI / build + unit + e2e (push) Successful in 3m5s
This commit was merged in pull request #54.
This commit is contained in:
@@ -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 <n>` 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.
|
||||
|
||||
@@ -529,6 +529,14 @@ export function MissionControl() {
|
||||
)}
|
||||
</PanelContent>
|
||||
</Panel>
|
||||
|
||||
{/* #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. */}
|
||||
<NextTurnButton turn={ship?.turn ?? null} onTicked={refreshAll} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -567,13 +575,9 @@ export function MissionControl() {
|
||||
<Badge variant="ACTIVE">SYS ONLINE</Badge>
|
||||
</footer>
|
||||
|
||||
{/* #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) */}
|
||||
<NextTurnButton turn={ship?.turn ?? null} onTicked={refreshAll} />
|
||||
|
||||
{/* #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. */}
|
||||
<TaskStrip tasks={system?.tasks ?? []} />
|
||||
|
||||
@@ -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 (
|
||||
<div
|
||||
style={{
|
||||
position: 'fixed',
|
||||
right: '1rem',
|
||||
bottom: '1rem',
|
||||
zIndex: 60, // above the header/footer's 50
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
alignItems: 'flex-end',
|
||||
gap: '0.375rem',
|
||||
gap: '0.5rem',
|
||||
width: '100%',
|
||||
marginTop: 'auto',
|
||||
}}
|
||||
>
|
||||
{error && (
|
||||
@@ -46,7 +47,7 @@ export function NextTurnButton({ turn, onTicked }: { turn: number | null; onTick
|
||||
<span style={{ fontSize: '0.62rem', color: 'var(--text-muted)', letterSpacing: '0.12em' }}>
|
||||
TURN {turn ?? '—'}
|
||||
</span>
|
||||
<Button variant="EXEC" size="SM" onClick={tick}>
|
||||
<Button variant="EXEC" size="MD" className="w-full" onClick={tick}>
|
||||
NEXT TURN
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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(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
|
||||
// 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
|
||||
});
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "space-wanderer",
|
||||
"version": "0.13.0",
|
||||
"version": "0.14.0",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"dev": "next dev -p 3000",
|
||||
|
||||
Reference in New Issue
Block a user