feat(ui): F8-10 — tables planets / ship-groups / fleets, ship-classes delete guard (#53)
Tests · UI / test (push) Waiting to run
Tests · UI / test (pull_request) Successful in 2m45s

Lights up three previously-stubbed table active views and tightens the
existing one:

  - table-planets: 4 kind checkboxes (own / foreign / uninhabited /
    unknown) + race dropdown that filters the foreign slice; row click
    selects + centres the planet on the map.
  - table-ship-groups: local + foreign groups in one grid, owner
    checkboxes, planet dropdown (destination OR origin), class
    dropdown; on-planet click focuses the destination planet, in-space
    click focuses the ship group itself (camera follows interpolated
    position).
  - table-fleets: own fleets only with the shared planet dropdown;
    on-planet click focuses the planet, in-space click centres the
    camera on the interpolated fleet position without altering the
    selection (no fleet variant in Selected).
  - table-ship-classes: per-row Delete is disabled with a count tooltip
    while at least one local ship group references the class. The
    engine refuses the removal anyway; the UI pre-empts the surface.

Wires the click → map flow through a transient `SelectionStore.focus`
/ `focusPoint` channel that `map.svelte` consumes once on mount —
in-memory only, so an F5 does not re-centre.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
Ilia Denisov
2026-05-27 20:35:38 +02:00
parent ef4cecb4b2
commit 80ed11e3b6
17 changed files with 2537 additions and 22 deletions
+60 -2
View File
@@ -17,7 +17,11 @@ import {
} from "vitest";
import { i18n } from "../src/lib/i18n/index.svelte";
import type { GameReport, ShipClassSummary } from "../src/api/game-state";
import type {
GameReport,
ReportLocalShipGroup,
ShipClassSummary,
} from "../src/api/game-state";
import {
ORDER_DRAFT_CONTEXT_KEY,
OrderDraftStore,
@@ -89,7 +93,10 @@ function shipClass(
};
}
function makeReport(localShipClass: ShipClassSummary[]): GameReport {
function makeReport(
localShipClass: ShipClassSummary[],
localShipGroups: ReportLocalShipGroup[] = [],
): GameReport {
return {
turn: 1,
mapWidth: 1000,
@@ -104,6 +111,29 @@ function makeReport(localShipClass: ShipClassSummary[]): GameReport {
localPlayerShields: 0,
localPlayerCargo: 0,
...EMPTY_SHIP_GROUPS,
localShipGroups,
};
}
function shipGroup(
overrides: Partial<ReportLocalShipGroup> &
Pick<ReportLocalShipGroup, "class">,
): ReportLocalShipGroup {
return {
id: crypto.randomUUID(),
state: "In_Orbit",
fleet: null,
count: 1,
tech: { drive: 0, weapons: 0, shields: 0, cargo: 0 },
cargo: "NONE",
load: 0,
destination: 1,
origin: null,
range: null,
speed: 0,
mass: 0,
race: "Foo",
...overrides,
};
}
@@ -210,6 +240,34 @@ describe("ship-classes table", () => {
expect(cmd.name).toBe("Drone");
});
test("delete is disabled when a local ship group uses the class", async () => {
const ui = mountTable(
makeReport(
[shipClass({ name: "Cruiser" }), shipClass({ name: "Drone" })],
[
shipGroup({ class: "Cruiser" }),
shipGroup({ class: "Cruiser", count: 4 }),
],
),
);
const buttons = ui.getAllByTestId("ship-classes-delete");
const map = new Map(
buttons.map((b) => {
const row = b.closest('[data-testid="ship-classes-row"]');
return [row?.getAttribute("data-name") ?? "", b];
}),
);
const cruiserBtn = map.get("Cruiser") as HTMLButtonElement;
const droneBtn = map.get("Drone") as HTMLButtonElement;
expect(cruiserBtn).toBeDisabled();
expect(cruiserBtn).toHaveAttribute(
"title",
expect.stringContaining("2"),
);
expect(droneBtn).not.toBeDisabled();
expect(droneBtn).toHaveAttribute("title", "");
});
test("new button requests a fresh calculator design", async () => {
const ui = mountTable(makeReport([]));
const before = calculatorLoadRequest.token;