ui/phase-13: planet inspector — read-only

Plumbs the map → inspector pathway: a click on a planet selects it
through the new SelectionStore, the sidebar Inspector tab swaps
its empty-state copy for a per-kind read-only field set, and a
mobile-only bottom-sheet mirrors the same content over the map.
Field projection in api/game-state.ts now surfaces every documented
planet field.
This commit is contained in:
Ilia Denisov
2026-05-09 08:29:03 +02:00
parent a3fdcfe9c5
commit 6364bba6fd
19 changed files with 1440 additions and 75 deletions
+71 -10
View File
@@ -48,13 +48,18 @@ The desktop sidebar hosts three tools:
| Inspector | `lib/sidebar/inspector-tab.svelte` | Phase 13 / 19 |
| Order | `lib/sidebar/order-tab.svelte` | Phase 12 / 14 |
The sidebar's selected-tab state is a `$state` rune inside
`lib/sidebar/sidebar.svelte`. The component is mounted by the layout
at `routes/games/[id]/+layout.svelte`, and SvelteKit keeps that
layout instance alive while the user navigates between child routes
(`/games/:id/map``/games/:id/report` → …). The rune therefore
survives every active-view switch automatically, with no URL coupling
needed.
The selected-tab state is a `$state` rune in
`routes/games/[id]/+layout.svelte`, bound into
`lib/sidebar/sidebar.svelte` via `$bindable()`. The layout owns the
rune so external events — Phase 13's planet click, future similar
flows — can drive the active tab from outside the sidebar without
plumbing callbacks. The component is mounted by the layout, and
SvelteKit keeps that layout instance alive while the user navigates
between child routes (`/games/:id/map``/games/:id/report` → …),
so the rune survives every active-view switch automatically with no
URL coupling needed. The URL seed and the history-mode reset
described below still live inside the sidebar — they mutate the
bindable in place; the layout sees the change through the binding.
A `?sidebar=calc|calculator|inspector|order` URL param is read once
on mount and seeds the initial tab. Later phases that want to land
@@ -95,9 +100,9 @@ Three discrete CSS modes matched to the IA section diagrams:
view-menu trigger swaps to a hamburger icon (☰) that opens the
drop-down as a full-width drawer below the header.
Inspector is intentionally unreachable on mobile in Phase 10. Per the
IA section the mobile inspector is a bottom-sheet raised by tapping a
map object, and that mechanism waits for Phase 13.
On mobile the bottom tab row does not include `Inspector`. The
inspector content is reached by tapping a map object instead, which
raises a bottom-sheet — see [Planet selection](#planet-selection-phase-13).
## Mobile bottom-tabs and tool overlay
@@ -132,6 +137,62 @@ back-stack mechanism. Phase 34 lands the back-stack alongside its
first user (multi-turn projection, range circles in the ship-class
designer).
## Planet selection (Phase 13)
The map view turns into the entry point for the inspector by
translating a renderer click into a planet selection. The flow:
1. The renderer (`src/map/render.ts`) exposes `onClick(cb)` next to
the existing `hitAt(cursor)`. It is built on `pixi-viewport`'s
`clicked` event, which already differentiates a click from a
pan-drag, so a click handler will not race the pan plugin.
2. `lib/active-view/map.svelte` wires that callback after a successful
`mountRenderer`. On a click it asks the renderer for the hit
primitive, looks the planet up by `number` in the live
`GameStateStore.report`, and calls `SelectionStore.selectPlanet(number)`.
3. `SelectionStore` (`lib/selection.svelte.ts`) is a runes store
instantiated by the layout and exposed via Svelte context under
`SELECTION_CONTEXT_KEY`. It carries a discriminated union — Phase
13 only models `{ kind: "planet"; id: number }`; Phase 19 widens
it for ship groups. Selection is in-memory only: it survives the
layout's lifetime (active-view switches inside `/games/:id/*`)
but does not persist across reloads — that contrast with the
order draft is intentional.
4. The layout watches the selection rune and, on the null → planet
transition, flips its bound `activeTab` to `inspector` and
`sidebarOpen` to `true`. Desktop already has the sidebar pinned;
tablet needs the drawer to surface; mobile is unaffected by the
tab rune because the sidebar is CSS-hidden there.
5. `lib/sidebar/inspector-tab.svelte` and
`lib/inspectors/planet-sheet.svelte` both read the selection
store, resolve it against the live report, and either render
`lib/inspectors/planet.svelte` or fall back to the empty state.
A selection that points at a planet missing from the current
report (visibility lost between turns) collapses to the empty
state instead of holding stale rows.
The mobile bottom-sheet is mounted alongside `<BottomTabs />` in the
layout. Its visibility is conditional on `effectiveTool === "map"` so
it does not stack on top of the calc / order overlays. Phase 13 ships
the minimal dismissal surface: a close button (`✕`) that calls
`SelectionStore.clear()`. Tap-outside and swipe-down dismissal from
the IA section are deferred to Phase 35 polish. A click that lands on
empty space is a no-op — selection is mutated only by an explicit
planet click or by the close button.
The planet inspector itself is a presentational component: it takes
a `ReportPlanet` snapshot as a prop and renders the documented field
set per planet kind. The wrapper in `api/game-state.ts` exposes every
field the FBS schema carries (`industryStockpile` for `capital`,
`materialsStockpile` for `material`, `industry`, `population`,
`colonists`, `production`, `freeIndustry`, plus `owner` for `other`).
Fields the FBS table does not project for a given kind read as `null`
and the inspector simply omits the row.
The selected-planet visual on the map (a ring or halo) is **not**
shipped in Phase 13. It rolls into Phase 35 polish together with the
sheet's swipe-to-dismiss gesture.
## Auth gate
The root `+layout.svelte` redirects `anonymous → /login` for any