Files
galaxy-game/ui/frontend/src/lib/inspectors/ship-group.svelte
T
Ilia Denisov 4ad96b0ef7
Tests · UI / test (push) Successful in 2m11s
Tests · UI / test (pull_request) Successful in 2m7s
feat(ui): migrate all view bodies to design tokens (F1b)
Tokenize every remaining component <style> — calculator, order tab,
inspectors, tables, report sections, lobby, auth, mail, battle viewer,
toasts, map overlays. A scripted pass handled the unambiguous core
palette (text/bg/surface/border/accent/danger/muted), the rest were
mapped to the semantic/grey tokens by role.

Remaining colour literals are the documented exceptions only: the
battle-scene SVG data-visualisation palette (fixed dark, like the WebGL
map canvas), overlay scrims (modal / map-canvas), and directional or
deliberate drop shadows. The default theme stays dark until light
coherence is signed off across the views.

Updates ui/docs/design-system.md (migration status + exceptions).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-22 07:24:02 +02:00

301 lines
9.0 KiB
Svelte
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!--
Phase 19 read-only ship-group inspector. Renders the documented field
set per group variant (local / other / incoming / unidentified). The
inspector is mounted by the sidebar `inspector-tab.svelte` when the
selection has `kind === "shipGroup"`, and on the mobile breakpoint by
the `ship-group-sheet.svelte` overlay.
Phase 20 will fold action buttons (split / send / load / unload /
modernize / dismantle / transfer / assign-to-fleet) onto the local
variant — for Phase 19 the inspector is intentionally read-only.
-->
<script lang="ts">
import type {
ReportIncomingShipGroup,
ReportLocalFleet,
ReportLocalShipGroup,
ReportOtherShipGroup,
ReportPlanet,
ReportUnidentifiedShipGroup,
ShipClassSummary,
} from "../../api/game-state";
import { i18n, type TranslationKey } from "$lib/i18n/index.svelte";
import Actions from "./ship-group/actions.svelte";
export type ShipGroupSelection =
| { variant: "local"; group: ReportLocalShipGroup }
| { variant: "other"; group: ReportOtherShipGroup }
| { variant: "incoming"; group: ReportIncomingShipGroup }
| { variant: "unidentified"; group: ReportUnidentifiedShipGroup };
type Props = {
selection: ShipGroupSelection;
planets: ReportPlanet[];
localShipClass?: ShipClassSummary[];
localFleets?: ReportLocalFleet[];
otherRaces?: string[];
mapWidth?: number;
mapHeight?: number;
localPlayerDrive?: number;
localPlayerWeapons?: number;
localPlayerShields?: number;
localPlayerCargo?: number;
};
let {
selection,
planets,
localShipClass = [],
localFleets = [],
otherRaces = [],
mapWidth = 1,
mapHeight = 1,
localPlayerDrive = 0,
localPlayerWeapons = 0,
localPlayerShields = 0,
localPlayerCargo = 0,
}: Props = $props();
const kindKeyMap: Record<ShipGroupSelection["variant"], TranslationKey> = {
local: "game.inspector.ship_group.kind.local",
other: "game.inspector.ship_group.kind.other",
incoming: "game.inspector.ship_group.kind.incoming",
unidentified: "game.inspector.ship_group.kind.unidentified",
};
const cargoLoadKeyMap = {
COL: "game.inspector.ship_group.cargo.col",
CAP: "game.inspector.ship_group.cargo.cap",
MAT: "game.inspector.ship_group.cargo.mat",
EMP: "game.inspector.ship_group.cargo.emp",
} as const;
const planetIndex = $derived.by(() => {
const m = new Map<number, ReportPlanet>();
for (const p of planets) m.set(p.number, p);
return m;
});
function planetLabel(number: number): string {
const planet = planetIndex.get(number);
if (planet === undefined) return `#${number}`;
if (planet.name === "" || planet.name === undefined) return `#${number}`;
return planet.name;
}
function formatNumber(value: number): string {
return value.toLocaleString(undefined, { maximumFractionDigits: 2 });
}
function cargoLabel(cargo: "NONE" | "COL" | "CAP" | "MAT" | "EMP"): string {
if (cargo === "NONE") {
return i18n.t("game.inspector.ship_group.cargo.none");
}
return i18n.t(cargoLoadKeyMap[cargo]);
}
const kindLabel = $derived(i18n.t(kindKeyMap[selection.variant]));
</script>
<section
class="inspector"
data-testid="inspector-ship-group"
data-variant={selection.variant}
>
<header>
<p class="kind" data-testid="inspector-ship-group-kind">{kindLabel}</p>
{#if selection.variant === "local" || selection.variant === "other"}
<h3 class="name" data-testid="inspector-ship-group-class">
{selection.group.class}
</h3>
{/if}
</header>
{#if selection.variant === "local"}
<Actions
group={selection.group}
{planets}
{localShipClass}
{localFleets}
{otherRaces}
{mapWidth}
{mapHeight}
{localPlayerDrive}
{localPlayerWeapons}
{localPlayerShields}
{localPlayerCargo}
/>
{/if}
{#if selection.variant === "local" || selection.variant === "other"}
{@const g = selection.group}
{@const onPlanet = g.origin === null || g.range === null}
<dl class="fields">
<div class="field" data-testid="inspector-ship-group-field-count">
<dt>{i18n.t("game.inspector.ship_group.field.count")}</dt>
<dd>{g.count}</dd>
</div>
<div class="field" data-testid="inspector-ship-group-field-drive">
<dt>{i18n.t("game.inspector.ship_group.field.drive")}</dt>
<dd>{formatNumber(g.tech.drive)}</dd>
</div>
<div class="field" data-testid="inspector-ship-group-field-weapons">
<dt>{i18n.t("game.inspector.ship_group.field.weapons")}</dt>
<dd>{formatNumber(g.tech.weapons)}</dd>
</div>
<div class="field" data-testid="inspector-ship-group-field-shields">
<dt>{i18n.t("game.inspector.ship_group.field.shields")}</dt>
<dd>{formatNumber(g.tech.shields)}</dd>
</div>
<div class="field" data-testid="inspector-ship-group-field-cargo-tech">
<dt>{i18n.t("game.inspector.ship_group.field.cargo_tech")}</dt>
<dd>{formatNumber(g.tech.cargo)}</dd>
</div>
<div class="field" data-testid="inspector-ship-group-field-mass">
<dt>{i18n.t("game.inspector.ship_group.field.mass")}</dt>
<dd>{formatNumber(g.mass)}</dd>
</div>
<div class="field" data-testid="inspector-ship-group-field-cargo-load">
<dt>{i18n.t("game.inspector.ship_group.field.cargo_load")}</dt>
<dd>
{#if g.cargo === "NONE"}
{cargoLabel(g.cargo)}
{:else}
{cargoLabel(g.cargo)} × {formatNumber(g.load)}
{/if}
</dd>
</div>
{#if onPlanet}
<div class="field" data-testid="inspector-ship-group-field-location">
<dt>{i18n.t("game.inspector.ship_group.field.location")}</dt>
<dd>{planetLabel(g.destination)}</dd>
</div>
{:else}
<div class="field" data-testid="inspector-ship-group-field-from">
<dt>{i18n.t("game.inspector.ship_group.field.from")}</dt>
<dd>{planetLabel(g.origin!)}</dd>
</div>
<div class="field" data-testid="inspector-ship-group-field-to">
<dt>{i18n.t("game.inspector.ship_group.field.to")}</dt>
<dd>{planetLabel(g.destination)}</dd>
</div>
<div class="field" data-testid="inspector-ship-group-field-distance">
<dt>{i18n.t("game.inspector.ship_group.field.distance")}</dt>
<dd>{formatNumber(g.range!)}</dd>
</div>
{/if}
{#if selection.variant === "local"}
<div class="field" data-testid="inspector-ship-group-field-fleet">
<dt>{i18n.t("game.inspector.ship_group.field.fleet")}</dt>
<dd>
{selection.group.fleet ?? i18n.t("game.inspector.ship_group.fleet.none")}
</dd>
</div>
<div class="field" data-testid="inspector-ship-group-field-state">
<dt>{i18n.t("game.inspector.ship_group.field.state")}</dt>
<dd>{selection.group.state}</dd>
</div>
{/if}
</dl>
{:else if selection.variant === "incoming"}
{@const g = selection.group}
{@const eta = g.speed > 0 ? Math.ceil(g.distance / g.speed) : null}
<dl class="fields">
<div class="field" data-testid="inspector-ship-group-field-from">
<dt>{i18n.t("game.inspector.ship_group.field.from")}</dt>
<dd>{planetLabel(g.origin)}</dd>
</div>
<div class="field" data-testid="inspector-ship-group-field-to">
<dt>{i18n.t("game.inspector.ship_group.field.to")}</dt>
<dd>{planetLabel(g.destination)}</dd>
</div>
<div class="field" data-testid="inspector-ship-group-field-distance">
<dt>{i18n.t("game.inspector.ship_group.field.distance")}</dt>
<dd>{formatNumber(g.distance)}</dd>
</div>
<div class="field" data-testid="inspector-ship-group-field-speed">
<dt>{i18n.t("game.inspector.ship_group.field.speed")}</dt>
<dd>{formatNumber(g.speed)}</dd>
</div>
<div class="field" data-testid="inspector-ship-group-field-eta">
<dt>{i18n.t("game.inspector.ship_group.field.eta")}</dt>
<dd>
{eta === null
? i18n.t("game.designer.ship_class.preview.unavailable")
: eta}
</dd>
</div>
<div class="field" data-testid="inspector-ship-group-field-mass">
<dt>{i18n.t("game.inspector.ship_group.field.mass")}</dt>
<dd>{formatNumber(g.mass)}</dd>
</div>
</dl>
{:else}
<dl class="fields">
<div
class="field"
data-testid="inspector-ship-group-field-coordinates"
>
<dt>{i18n.t("game.inspector.ship_group.field.coordinates")}</dt>
<dd>
({formatNumber(selection.group.x)}, {formatNumber(selection.group.y)})
</dd>
</div>
</dl>
<p class="hint" data-testid="inspector-ship-group-no-data">
{i18n.t("game.inspector.ship_group.unidentified_no_data")}
</p>
{/if}
</section>
<style>
.inspector {
padding: 1rem;
font-family: system-ui, sans-serif;
display: flex;
flex-direction: column;
gap: 0.75rem;
}
header {
display: flex;
flex-direction: column;
gap: 0.15rem;
}
.kind {
margin: 0;
font-size: 0.75rem;
text-transform: uppercase;
letter-spacing: 0.05em;
color: var(--color-text-muted);
}
.name {
margin: 0;
font-size: 1.05rem;
}
.fields {
margin: 0;
display: grid;
grid-template-columns: max-content 1fr;
row-gap: 0.25rem;
column-gap: 0.75rem;
}
.field {
display: contents;
}
.field dt {
color: var(--color-text-muted);
font-size: 0.85rem;
}
.field dd {
margin: 0;
font-variant-numeric: tabular-nums;
font-size: 0.9rem;
}
.hint {
margin: 0;
color: var(--color-text-muted);
font-size: 0.85rem;
}
</style>