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
+64 -1
View File
@@ -56,6 +56,20 @@ export const SELECTION_CONTEXT_KEY = Symbol("selection");
export class SelectionStore {
selected: Selected | null = $state(null);
// pendingFocus is a transient one-shot request to centre the map
// camera on a selection. F8-10 tables raise it together with the
// view switch to map; map.svelte consumes it once on mount (see
// `consumePendingFocus`) and clears it. Held in memory only — not
// persisted, so an F5 after a click-through does not re-centre.
#pendingFocus: Selected | null = $state(null);
// pendingCenter is the coord-only sibling of pendingFocus: it
// asks map.svelte to centre on a world point without touching
// the selection. F8-10 uses it for in-space fleet rows (the
// `Selected` union has no "fleet" variant, but the user still
// expects the camera to find them). Also one-shot and transient.
#pendingCenter: { x: number; y: number } | null = $state(null);
private destroyed = false;
/**
@@ -77,10 +91,57 @@ export class SelectionStore {
this.selected = { kind: "shipGroup", ref };
}
/**
* focus sets the active selection to `target` and queues a one-shot
* camera-centre request for whichever next mount of the map view
* picks it up via `consumePendingFocus`. Used by the F8-10 tables to
* navigate from a row click to the map. A no-op once the store has
* been disposed.
*/
focus(target: Selected): void {
if (this.destroyed) return;
this.selected = target;
this.#pendingFocus = target;
}
/**
* consumePendingFocus returns the queued focus target (if any) and
* clears it in the same call. Designed for `map.svelte` to invoke
* once after the renderer mounts.
*/
consumePendingFocus(): Selected | null {
const target = this.#pendingFocus;
this.#pendingFocus = null;
return target;
}
/**
* focusPoint queues a one-shot camera-centre on a free-form world
* coordinate, without altering selection. Used by the fleets table
* when the user clicks an in-space fleet (there is no `"fleet"`
* variant in `Selected`, but the camera should still find it).
* A no-op once the store has been disposed.
*/
focusPoint(x: number, y: number): void {
if (this.destroyed) return;
this.#pendingCenter = { x, y };
}
/**
* consumePendingCenter returns the queued centre point (if any)
* and clears it.
*/
consumePendingCenter(): { x: number; y: number } | null {
const point = this.#pendingCenter;
this.#pendingCenter = null;
return point;
}
/**
* clear drops the current selection. The mobile sheet's close
* button calls this; otherwise selection persists across active-
* view switches.
* view switches. Does not affect any queued pending focus — that
* is a transient delivery channel, not selection state.
*/
clear(): void {
if (this.destroyed) return;
@@ -90,5 +151,7 @@ export class SelectionStore {
dispose(): void {
this.destroyed = true;
this.selected = null;
this.#pendingFocus = null;
this.#pendingCenter = null;
}
}