fix(ui): F8-08 unified number format — mono, fixed 3-decimal, no separators
Tests · UI / test (push) Waiting to run
Tests · UI / test (pull_request) Waiting to run

Engine emits Floats at Fixed3 quantisation; UI now renders them as 3-decimal
fixed-point strings without thousand separators, monospaced via var(--font-mono)
on .numeric cells, and right-aligned in tables so columns line up on the
decimal point. Integer counts render with 0 decimals and no separators;
science fractions render as 1-decimal percent (matches the engine's third
decimal of precision).

Bug fixes from #51 (umbrella #43):
  - Player Status drive/weapons/shields/cargo: were tech LEVELS rendered
    through formatPercent (x100) — now use formatFloat (raw level).
  - Races table: same bug, same fix.

Style/UX cleanups:
  - Inspector field labels lose "stockpile" word ($ / M suffix carries it).
  - Coordinates drop the parentheses (just "x, y").
  - Inspector + report tables unify font sizes with calculator-tab
    (values 0.85rem mono, labels 0.8rem).

Files:
  - new util: ui/frontend/src/lib/util/number-format.ts
  - report/format.ts becomes a thin re-export to keep section imports compact
  - inspector planet / ship-group / actions: drop inline formatNumber,
    mark numeric <dd> with class="numeric"
  - table-races (+ bug fix), table-sciences, table-ship-classes,
    designer-science: drop inline formatters, switch to util, add
    class="numeric" on numeric <th>/<td>
  - 17 report section files: class="numeric" on numeric th/td +
    scoped CSS rule for mono+right-align
  - i18n en/ru: drop "stockpile" word, drop "%" from tech-level column
    headers in races + player_status (the "%" was the misleading bit
    from the bug)
  - tests/inspector-planet + tests/table-races: update assertions to
    match the new format

Verification: pnpm test (814 passed), pnpm check (0 errors/warnings),
pnpm build clean.

Refs: #51 (#43 umbrella).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Ilia Denisov
2026-05-27 11:08:22 +02:00
parent 208d30073b
commit b31d9f4c45
33 changed files with 484 additions and 379 deletions
@@ -43,6 +43,7 @@ fractions is a Phase 21 decision documented in
validateScience, validateScience,
type ScienceInvalidReason, type ScienceInvalidReason,
} from "$lib/util/science-validation"; } from "$lib/util/science-validation";
import { formatPercent } from "$lib/util/number-format";
const rendered = getContext<RenderedReportSource | undefined>( const rendered = getContext<RenderedReportSource | undefined>(
RENDERED_REPORT_CONTEXT_KEY, RENDERED_REPORT_CONTEXT_KEY,
@@ -106,12 +107,7 @@ fractions is a Phase 21 decision documented in
const canSave = $derived(validation.ok && draft !== undefined); const canSave = $derived(validation.ok && draft !== undefined);
const sumPercent = $derived(drive + weapons + shields + cargo); const sumPercent = $derived(drive + weapons + shields + cargo);
const sumDisplay = $derived( const sumDisplay = $derived(sumPercent.toFixed(1));
sumPercent.toLocaleString(undefined, {
minimumFractionDigits: 0,
maximumFractionDigits: 1,
}),
);
$effect(() => { $effect(() => {
if (!isViewMode) { if (!isViewMode) {
@@ -119,13 +115,6 @@ fractions is a Phase 21 decision documented in
} }
}); });
function formatPercent(fraction: number): string {
return (fraction * 100).toLocaleString(undefined, {
minimumFractionDigits: 0,
maximumFractionDigits: 1,
});
}
function backToTable(): void { function backToTable(): void {
activeView.select("table", { tableEntity: "sciences" }); activeView.select("table", { tableEntity: "sciences" });
} }
@@ -1,61 +1,44 @@
// Shared number / planet formatters for the Phase 23 Report View // Number formatters and lookup helpers reused across the report-section
// sections. Inlined in 10+ components, so factoring keeps each // components. The numeric formatters delegate to the project-wide
// section component focused on its data shape. The formatters // `lib/util/number-format` so inspector, report tables, and the
// match the conventions of the per-entity tables (tabular numerals, // calculator panel all render numbers identically.
// one-decimal percent without a `%` suffix — the header carries the
// unit) so the report's grids read the same way as the
// table-races / table-sciences views.
import type { ReportPlanet } from "../../../api/game-state"; import type { ReportPlanet } from "../../../api/game-state";
import {
formatFloat as formatFloatBase,
formatInt,
formatPercent as formatPercentBase,
} from "$lib/util/number-format";
/** /**
* formatPercent renders a `[0, 1]` fraction as a one-decimal * formatPercent renders a `[0, 1]` fraction as a one-decimal percent
* percent (without a `%` suffix — the column header carries the * (without a `%` suffix — the column header carries the unit).
* unit). Matches the convention used by `table-races.svelte` and * Re-exported from the shared util for backwards-compatible imports
* `table-sciences.svelte`. * across the report sections.
*/ */
export function formatPercent(fraction: number): string { export const formatPercent = formatPercentBase;
return (fraction * 100).toLocaleString(undefined, {
minimumFractionDigits: 0,
maximumFractionDigits: 1,
});
}
/** /**
* formatCount renders an integer-ish value (population, industry, * formatCount renders an integer-ish value (population, industry,
* planet count, …) without fractional digits and with locale-aware * planet count, …) with zero fractional digits and no thousand
* thousand separators. * separators. Alias of the shared `formatInt`.
*/ */
export function formatCount(value: number): string { export const formatCount = formatInt;
return value.toLocaleString(undefined, {
minimumFractionDigits: 0,
maximumFractionDigits: 0,
});
}
/** /**
* formatFloat renders a floating-point value with up to two * formatFloat renders an engine `Float` (Fixed3-quantised) with three
* fractional digits. Used for stockpiles, distances, cost, mass — * fractional digits and no thousand separators. Used for stockpiles,
* everything the engine emits as a `Float` that is not a fraction. * distances, cost, mass, tech levels — every report payload that is
* neither an integer count nor a `[0, 1]` fraction.
*/ */
export function formatFloat(value: number): string { export const formatFloat = formatFloatBase;
return value.toLocaleString(undefined, {
minimumFractionDigits: 0,
maximumFractionDigits: 2,
});
}
/** /**
* formatVotes renders a vote weight with up to two decimal digits — * formatVotes renders a vote weight. Votes travel as the same `Float`
* mirrors the races table's column convention so the cumulative * shape as every other float field, so this is a semantic alias of
* vote totals line up across views. * `formatFloat` kept for readability at the call site.
*/ */
export function formatVotes(value: number): string { export const formatVotes = formatFloatBase;
return value.toLocaleString(undefined, {
minimumFractionDigits: 0,
maximumFractionDigits: 2,
});
}
/** /**
* planetLabel renders a planet reference as `#<number> (<name>)` if * planetLabel renders a planet reference as `#<number> (<name>)` if
@@ -42,11 +42,11 @@ class when the group lands and a battle roster forms.
<tr> <tr>
<th>{i18n.t("game.report.section.approaching_groups.column.from")}</th> <th>{i18n.t("game.report.section.approaching_groups.column.from")}</th>
<th>{i18n.t("game.report.section.approaching_groups.column.to")}</th> <th>{i18n.t("game.report.section.approaching_groups.column.to")}</th>
<th> <th class="numeric">
{i18n.t("game.report.section.approaching_groups.column.distance")} {i18n.t("game.report.section.approaching_groups.column.distance")}
</th> </th>
<th>{i18n.t("game.report.section.approaching_groups.column.speed")}</th> <th class="numeric">{i18n.t("game.report.section.approaching_groups.column.speed")}</th>
<th>{i18n.t("game.report.section.approaching_groups.column.mass")}</th> <th class="numeric">{i18n.t("game.report.section.approaching_groups.column.mass")}</th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
@@ -54,9 +54,9 @@ class when the group lands and a battle roster forms.
<tr data-testid="approaching-groups-row"> <tr data-testid="approaching-groups-row">
<td>{planetLabel(r.origin, planets)}</td> <td>{planetLabel(r.origin, planets)}</td>
<td>{planetLabel(r.destination, planets)}</td> <td>{planetLabel(r.destination, planets)}</td>
<td>{formatFloat(r.distance)}</td> <td class="numeric">{formatFloat(r.distance)}</td>
<td>{formatFloat(r.speed)}</td> <td class="numeric">{formatFloat(r.speed)}</td>
<td>{formatFloat(r.mass)}</td> <td class="numeric">{formatFloat(r.mass)}</td>
</tr> </tr>
{/each} {/each}
</tbody> </tbody>
@@ -79,7 +79,7 @@ class when the group lands and a battle roster forms.
border-collapse: collapse; border-collapse: collapse;
width: 100%; width: 100%;
font-variant-numeric: tabular-nums; font-variant-numeric: tabular-nums;
font-size: 0.9rem; font-size: 0.85rem;
} }
.grid th, .grid th,
.grid td { .grid td {
@@ -93,6 +93,11 @@ class when the group lands and a battle roster forms.
letter-spacing: 0.04em; letter-spacing: 0.04em;
font-size: 0.75rem; font-size: 0.75rem;
} }
.grid th.numeric,
.grid td.numeric {
font-family: var(--font-mono);
text-align: right;
}
.grid tbody tr:hover { .grid tbody tr:hover {
background: var(--color-surface); background: var(--color-surface);
} }
@@ -42,16 +42,16 @@ Decoder sorts by `planetNumber` already.
<th>{i18n.t("game.report.section.bombings.column.owner")}</th> <th>{i18n.t("game.report.section.bombings.column.owner")}</th>
<th>{i18n.t("game.report.section.bombings.column.attacker")}</th> <th>{i18n.t("game.report.section.bombings.column.attacker")}</th>
<th>{i18n.t("game.report.section.bombings.column.production")}</th> <th>{i18n.t("game.report.section.bombings.column.production")}</th>
<th>{i18n.t("game.report.section.bombings.column.industry")}</th> <th class="numeric">{i18n.t("game.report.section.bombings.column.industry")}</th>
<th>{i18n.t("game.report.section.bombings.column.population")}</th> <th class="numeric">{i18n.t("game.report.section.bombings.column.population")}</th>
<th>{i18n.t("game.report.section.bombings.column.colonists")}</th> <th class="numeric">{i18n.t("game.report.section.bombings.column.colonists")}</th>
<th> <th class="numeric">
{i18n.t("game.report.section.bombings.column.industry_stockpile")} {i18n.t("game.report.section.bombings.column.industry_stockpile")}
</th> </th>
<th> <th class="numeric">
{i18n.t("game.report.section.bombings.column.materials_stockpile")} {i18n.t("game.report.section.bombings.column.materials_stockpile")}
</th> </th>
<th>{i18n.t("game.report.section.bombings.column.attack_power")}</th> <th class="numeric">{i18n.t("game.report.section.bombings.column.attack_power")}</th>
<th></th> <th></th>
</tr> </tr>
</thead> </thead>
@@ -67,12 +67,12 @@ Decoder sorts by `planetNumber` already.
<td>{b.owner}</td> <td>{b.owner}</td>
<td>{b.attacker}</td> <td>{b.attacker}</td>
<td>{b.production}</td> <td>{b.production}</td>
<td>{formatFloat(b.industry)}</td> <td class="numeric">{formatFloat(b.industry)}</td>
<td>{formatFloat(b.population)}</td> <td class="numeric">{formatFloat(b.population)}</td>
<td>{formatFloat(b.colonists)}</td> <td class="numeric">{formatFloat(b.colonists)}</td>
<td>{formatFloat(b.industryStockpile)}</td> <td class="numeric">{formatFloat(b.industryStockpile)}</td>
<td>{formatFloat(b.materialsStockpile)}</td> <td class="numeric">{formatFloat(b.materialsStockpile)}</td>
<td>{formatCount(b.attackPower)}</td> <td class="numeric">{formatCount(b.attackPower)}</td>
<td> <td>
{#if b.wiped} {#if b.wiped}
<span <span
@@ -105,7 +105,7 @@ Decoder sorts by `planetNumber` already.
border-collapse: collapse; border-collapse: collapse;
width: 100%; width: 100%;
font-variant-numeric: tabular-nums; font-variant-numeric: tabular-nums;
font-size: 0.9rem; font-size: 0.85rem;
} }
.grid th, .grid th,
.grid td { .grid td {
@@ -119,6 +119,11 @@ Decoder sorts by `planetNumber` already.
letter-spacing: 0.04em; letter-spacing: 0.04em;
font-size: 0.75rem; font-size: 0.75rem;
} }
.grid th.numeric,
.grid td.numeric {
font-family: var(--font-mono);
text-align: right;
}
.grid tbody tr:hover { .grid tbody tr:hover {
background: var(--color-surface); background: var(--color-surface);
} }
@@ -94,7 +94,7 @@ has many routes.
border-collapse: collapse; border-collapse: collapse;
width: 100%; width: 100%;
font-variant-numeric: tabular-nums; font-variant-numeric: tabular-nums;
font-size: 0.9rem; font-size: 0.85rem;
} }
.grid th, .grid th,
.grid td { .grid td {
@@ -42,20 +42,20 @@ as the local planets table plus an `owner` column.
<th>{i18n.t("game.report.section.my_planets.column.number")}</th> <th>{i18n.t("game.report.section.my_planets.column.number")}</th>
<th>{i18n.t("game.report.section.my_planets.column.name")}</th> <th>{i18n.t("game.report.section.my_planets.column.name")}</th>
<th>{i18n.t("game.report.section.foreign_planets.column.owner")}</th> <th>{i18n.t("game.report.section.foreign_planets.column.owner")}</th>
<th>{i18n.t("game.report.section.my_planets.column.coordinates")}</th> <th class="numeric">{i18n.t("game.report.section.my_planets.column.coordinates")}</th>
<th>{i18n.t("game.report.section.my_planets.column.size")}</th> <th class="numeric">{i18n.t("game.report.section.my_planets.column.size")}</th>
<th>{i18n.t("game.report.section.my_planets.column.resources")}</th> <th class="numeric">{i18n.t("game.report.section.my_planets.column.resources")}</th>
<th>{i18n.t("game.report.section.my_planets.column.population")}</th> <th class="numeric">{i18n.t("game.report.section.my_planets.column.population")}</th>
<th>{i18n.t("game.report.section.my_planets.column.industry")}</th> <th class="numeric">{i18n.t("game.report.section.my_planets.column.industry")}</th>
<th> <th class="numeric">
{i18n.t("game.report.section.my_planets.column.industry_stockpile")} {i18n.t("game.report.section.my_planets.column.industry_stockpile")}
</th> </th>
<th> <th class="numeric">
{i18n.t("game.report.section.my_planets.column.materials_stockpile")} {i18n.t("game.report.section.my_planets.column.materials_stockpile")}
</th> </th>
<th>{i18n.t("game.report.section.my_planets.column.colonists")}</th> <th class="numeric">{i18n.t("game.report.section.my_planets.column.colonists")}</th>
<th>{i18n.t("game.report.section.my_planets.column.production")}</th> <th>{i18n.t("game.report.section.my_planets.column.production")}</th>
<th>{i18n.t("game.report.section.my_planets.column.free_industry")}</th> <th class="numeric">{i18n.t("game.report.section.my_planets.column.free_industry")}</th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
@@ -64,16 +64,16 @@ as the local planets table plus an `owner` column.
<td>{p.number}</td> <td>{p.number}</td>
<td>{p.name}</td> <td>{p.name}</td>
<td>{p.owner ?? ""}</td> <td>{p.owner ?? ""}</td>
<td>{formatFloat(p.x)}, {formatFloat(p.y)}</td> <td class="numeric">{formatFloat(p.x)}, {formatFloat(p.y)}</td>
<td>{formatFloat(p.size ?? 0)}</td> <td class="numeric">{formatFloat(p.size ?? 0)}</td>
<td>{formatFloat(p.resources ?? 0)}</td> <td class="numeric">{formatFloat(p.resources ?? 0)}</td>
<td>{formatFloat(p.population ?? 0)}</td> <td class="numeric">{formatFloat(p.population ?? 0)}</td>
<td>{formatFloat(p.industry ?? 0)}</td> <td class="numeric">{formatFloat(p.industry ?? 0)}</td>
<td>{formatFloat(p.industryStockpile ?? 0)}</td> <td class="numeric">{formatFloat(p.industryStockpile ?? 0)}</td>
<td>{formatFloat(p.materialsStockpile ?? 0)}</td> <td class="numeric">{formatFloat(p.materialsStockpile ?? 0)}</td>
<td>{formatFloat(p.colonists ?? 0)}</td> <td class="numeric">{formatFloat(p.colonists ?? 0)}</td>
<td>{p.production ?? "—"}</td> <td>{p.production ?? "—"}</td>
<td>{formatFloat(p.freeIndustry ?? 0)}</td> <td class="numeric">{formatFloat(p.freeIndustry ?? 0)}</td>
</tr> </tr>
{/each} {/each}
</tbody> </tbody>
@@ -96,7 +96,7 @@ as the local planets table plus an `owner` column.
border-collapse: collapse; border-collapse: collapse;
width: 100%; width: 100%;
font-variant-numeric: tabular-nums; font-variant-numeric: tabular-nums;
font-size: 0.9rem; font-size: 0.85rem;
} }
.grid th, .grid th,
.grid td { .grid td {
@@ -110,6 +110,11 @@ as the local planets table plus an `owner` column.
letter-spacing: 0.04em; letter-spacing: 0.04em;
font-size: 0.75rem; font-size: 0.75rem;
} }
.grid th.numeric,
.grid td.numeric {
font-family: var(--font-mono);
text-align: right;
}
.grid tbody tr:hover { .grid tbody tr:hover {
background: var(--color-surface); background: var(--color-surface);
} }
@@ -67,10 +67,10 @@ unit even when the section spans many races.
<thead> <thead>
<tr> <tr>
<th>{i18n.t("game.report.section.my_sciences.column.name")}</th> <th>{i18n.t("game.report.section.my_sciences.column.name")}</th>
<th>{i18n.t("game.report.section.my_sciences.column.drive")}</th> <th class="numeric">{i18n.t("game.report.section.my_sciences.column.drive")}</th>
<th>{i18n.t("game.report.section.my_sciences.column.weapons")}</th> <th class="numeric">{i18n.t("game.report.section.my_sciences.column.weapons")}</th>
<th>{i18n.t("game.report.section.my_sciences.column.shields")}</th> <th class="numeric">{i18n.t("game.report.section.my_sciences.column.shields")}</th>
<th>{i18n.t("game.report.section.my_sciences.column.cargo")}</th> <th class="numeric">{i18n.t("game.report.section.my_sciences.column.cargo")}</th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
@@ -81,10 +81,10 @@ unit even when the section spans many races.
data-name={r.name} data-name={r.name}
> >
<td>{r.name}</td> <td>{r.name}</td>
<td>{formatPercent(r.drive)}</td> <td class="numeric">{formatPercent(r.drive)}</td>
<td>{formatPercent(r.weapons)}</td> <td class="numeric">{formatPercent(r.weapons)}</td>
<td>{formatPercent(r.shields)}</td> <td class="numeric">{formatPercent(r.shields)}</td>
<td>{formatPercent(r.cargo)}</td> <td class="numeric">{formatPercent(r.cargo)}</td>
</tr> </tr>
{/each} {/each}
</tbody> </tbody>
@@ -115,7 +115,7 @@ unit even when the section spans many races.
border-collapse: collapse; border-collapse: collapse;
width: 100%; width: 100%;
font-variant-numeric: tabular-nums; font-variant-numeric: tabular-nums;
font-size: 0.9rem; font-size: 0.85rem;
} }
.grid th, .grid th,
.grid td { .grid td {
@@ -129,6 +129,11 @@ unit even when the section spans many races.
letter-spacing: 0.04em; letter-spacing: 0.04em;
font-size: 0.75rem; font-size: 0.75rem;
} }
.grid th.numeric,
.grid td.numeric {
font-family: var(--font-mono);
text-align: right;
}
.grid tbody tr:hover { .grid tbody tr:hover {
background: var(--color-surface); background: var(--color-surface);
} }
@@ -65,12 +65,12 @@ incoming groups.
<thead> <thead>
<tr> <tr>
<th>{i18n.t("game.report.section.my_ship_classes.column.name")}</th> <th>{i18n.t("game.report.section.my_ship_classes.column.name")}</th>
<th>{i18n.t("game.report.section.my_ship_classes.column.drive")}</th> <th class="numeric">{i18n.t("game.report.section.my_ship_classes.column.drive")}</th>
<th>{i18n.t("game.report.section.my_ship_classes.column.armament")}</th> <th>{i18n.t("game.report.section.my_ship_classes.column.armament")}</th>
<th>{i18n.t("game.report.section.my_ship_classes.column.weapons")}</th> <th class="numeric">{i18n.t("game.report.section.my_ship_classes.column.weapons")}</th>
<th>{i18n.t("game.report.section.my_ship_classes.column.shields")}</th> <th class="numeric">{i18n.t("game.report.section.my_ship_classes.column.shields")}</th>
<th>{i18n.t("game.report.section.my_ship_classes.column.cargo")}</th> <th class="numeric">{i18n.t("game.report.section.my_ship_classes.column.cargo")}</th>
<th>{i18n.t("game.report.section.foreign_ship_classes.column.mass")}</th> <th class="numeric">{i18n.t("game.report.section.foreign_ship_classes.column.mass")}</th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
@@ -81,12 +81,12 @@ incoming groups.
data-name={r.name} data-name={r.name}
> >
<td>{r.name}</td> <td>{r.name}</td>
<td>{formatFloat(r.drive)}</td> <td class="numeric">{formatFloat(r.drive)}</td>
<td>{r.armament}</td> <td>{r.armament}</td>
<td>{formatFloat(r.weapons)}</td> <td class="numeric">{formatFloat(r.weapons)}</td>
<td>{formatFloat(r.shields)}</td> <td class="numeric">{formatFloat(r.shields)}</td>
<td>{formatFloat(r.cargo)}</td> <td class="numeric">{formatFloat(r.cargo)}</td>
<td>{formatFloat(r.mass)}</td> <td class="numeric">{formatFloat(r.mass)}</td>
</tr> </tr>
{/each} {/each}
</tbody> </tbody>
@@ -117,7 +117,7 @@ incoming groups.
border-collapse: collapse; border-collapse: collapse;
width: 100%; width: 100%;
font-variant-numeric: tabular-nums; font-variant-numeric: tabular-nums;
font-size: 0.9rem; font-size: 0.85rem;
} }
.grid th, .grid th,
.grid td { .grid td {
@@ -131,6 +131,11 @@ incoming groups.
letter-spacing: 0.04em; letter-spacing: 0.04em;
font-size: 0.75rem; font-size: 0.75rem;
} }
.grid th.numeric,
.grid td.numeric {
font-family: var(--font-mono);
text-align: right;
}
.grid tbody tr:hover { .grid tbody tr:hover {
background: var(--color-surface); background: var(--color-surface);
} }
@@ -49,8 +49,8 @@ to groups the player doesn't own.
<th>{i18n.t("game.report.section.my_ship_groups.column.destination")}</th> <th>{i18n.t("game.report.section.my_ship_groups.column.destination")}</th>
<th>{i18n.t("game.report.section.my_ship_groups.column.origin")}</th> <th>{i18n.t("game.report.section.my_ship_groups.column.origin")}</th>
<th>{i18n.t("game.report.section.my_ship_groups.column.range")}</th> <th>{i18n.t("game.report.section.my_ship_groups.column.range")}</th>
<th>{i18n.t("game.report.section.my_ship_groups.column.speed")}</th> <th class="numeric">{i18n.t("game.report.section.my_ship_groups.column.speed")}</th>
<th>{i18n.t("game.report.section.my_ship_groups.column.mass")}</th> <th class="numeric">{i18n.t("game.report.section.my_ship_groups.column.mass")}</th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
@@ -64,8 +64,8 @@ to groups the player doesn't own.
{g.origin === null ? "—" : planetLabel(g.origin, planets)} {g.origin === null ? "—" : planetLabel(g.origin, planets)}
</td> </td>
<td>{g.range === null ? "—" : formatFloat(g.range)}</td> <td>{g.range === null ? "—" : formatFloat(g.range)}</td>
<td>{formatFloat(g.speed)}</td> <td class="numeric">{formatFloat(g.speed)}</td>
<td>{formatFloat(g.mass)}</td> <td class="numeric">{formatFloat(g.mass)}</td>
</tr> </tr>
{/each} {/each}
</tbody> </tbody>
@@ -88,7 +88,7 @@ to groups the player doesn't own.
border-collapse: collapse; border-collapse: collapse;
width: 100%; width: 100%;
font-variant-numeric: tabular-nums; font-variant-numeric: tabular-nums;
font-size: 0.9rem; font-size: 0.85rem;
} }
.grid th, .grid th,
.grid td { .grid td {
@@ -102,6 +102,11 @@ to groups the player doesn't own.
letter-spacing: 0.04em; letter-spacing: 0.04em;
font-size: 0.75rem; font-size: 0.75rem;
} }
.grid th.numeric,
.grid td.numeric {
font-family: var(--font-mono);
text-align: right;
}
.grid tbody tr:hover { .grid tbody tr:hover {
background: var(--color-surface); background: var(--color-surface);
} }
@@ -31,13 +31,13 @@ section is never empty as long as the report has loaded.
{:else} {:else}
<dl class="kv"> <dl class="kv">
<dt>{i18n.t("game.report.section.galaxy_summary.field.turn")}</dt> <dt>{i18n.t("game.report.section.galaxy_summary.field.turn")}</dt>
<dd data-testid="galaxy-summary-field-turn">{report.turn}</dd> <dd class="numeric" data-testid="galaxy-summary-field-turn">{report.turn}</dd>
<dt>{i18n.t("game.report.section.galaxy_summary.field.size")}</dt> <dt>{i18n.t("game.report.section.galaxy_summary.field.size")}</dt>
<dd data-testid="galaxy-summary-field-size"> <dd class="numeric" data-testid="galaxy-summary-field-size">
{report.mapWidth} × {report.mapHeight} {report.mapWidth} × {report.mapHeight}
</dd> </dd>
<dt>{i18n.t("game.report.section.galaxy_summary.field.planets")}</dt> <dt>{i18n.t("game.report.section.galaxy_summary.field.planets")}</dt>
<dd data-testid="galaxy-summary-field-planets">{report.planetCount}</dd> <dd class="numeric" data-testid="galaxy-summary-field-planets">{report.planetCount}</dd>
<dt>{i18n.t("game.report.section.galaxy_summary.field.race")}</dt> <dt>{i18n.t("game.report.section.galaxy_summary.field.race")}</dt>
<dd data-testid="galaxy-summary-field-race">{report.race}</dd> <dd data-testid="galaxy-summary-field-race">{report.race}</dd>
</dl> </dl>
@@ -60,7 +60,7 @@ section is never empty as long as the report has loaded.
grid-template-columns: max-content 1fr; grid-template-columns: max-content 1fr;
gap: 0.3rem 1rem; gap: 0.3rem 1rem;
margin: 0; margin: 0;
font-size: 0.9rem; font-size: 0.85rem;
} }
.kv dt { .kv dt {
color: var(--color-text-muted); color: var(--color-text-muted);
@@ -73,4 +73,7 @@ section is never empty as long as the report has loaded.
color: var(--color-text); color: var(--color-text);
font-variant-numeric: tabular-nums; font-variant-numeric: tabular-nums;
} }
.kv dd.numeric {
font-family: var(--font-mono);
}
</style> </style>
@@ -44,7 +44,7 @@ in orbit has neither); empty cells in those columns are normal.
<th>{i18n.t("game.report.section.my_fleets.column.destination")}</th> <th>{i18n.t("game.report.section.my_fleets.column.destination")}</th>
<th>{i18n.t("game.report.section.my_fleets.column.origin")}</th> <th>{i18n.t("game.report.section.my_fleets.column.origin")}</th>
<th>{i18n.t("game.report.section.my_fleets.column.range")}</th> <th>{i18n.t("game.report.section.my_fleets.column.range")}</th>
<th>{i18n.t("game.report.section.my_fleets.column.speed")}</th> <th class="numeric">{i18n.t("game.report.section.my_fleets.column.speed")}</th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
@@ -58,7 +58,7 @@ in orbit has neither); empty cells in those columns are normal.
{f.origin === null ? "—" : planetLabel(f.origin, planets)} {f.origin === null ? "—" : planetLabel(f.origin, planets)}
</td> </td>
<td>{f.range === null ? "—" : formatFloat(f.range)}</td> <td>{f.range === null ? "—" : formatFloat(f.range)}</td>
<td>{formatFloat(f.speed)}</td> <td class="numeric">{formatFloat(f.speed)}</td>
</tr> </tr>
{/each} {/each}
</tbody> </tbody>
@@ -81,7 +81,7 @@ in orbit has neither); empty cells in those columns are normal.
border-collapse: collapse; border-collapse: collapse;
width: 100%; width: 100%;
font-variant-numeric: tabular-nums; font-variant-numeric: tabular-nums;
font-size: 0.9rem; font-size: 0.85rem;
} }
.grid th, .grid th,
.grid td { .grid td {
@@ -95,6 +95,11 @@ in orbit has neither); empty cells in those columns are normal.
letter-spacing: 0.04em; letter-spacing: 0.04em;
font-size: 0.75rem; font-size: 0.75rem;
} }
.grid th.numeric,
.grid td.numeric {
font-family: var(--font-mono);
text-align: right;
}
.grid tbody tr:hover { .grid tbody tr:hover {
background: var(--color-surface); background: var(--color-surface);
} }
@@ -41,20 +41,20 @@ column set (matches `ReportPlanet` shape).
<tr> <tr>
<th>{i18n.t("game.report.section.my_planets.column.number")}</th> <th>{i18n.t("game.report.section.my_planets.column.number")}</th>
<th>{i18n.t("game.report.section.my_planets.column.name")}</th> <th>{i18n.t("game.report.section.my_planets.column.name")}</th>
<th>{i18n.t("game.report.section.my_planets.column.coordinates")}</th> <th class="numeric">{i18n.t("game.report.section.my_planets.column.coordinates")}</th>
<th>{i18n.t("game.report.section.my_planets.column.size")}</th> <th class="numeric">{i18n.t("game.report.section.my_planets.column.size")}</th>
<th>{i18n.t("game.report.section.my_planets.column.resources")}</th> <th class="numeric">{i18n.t("game.report.section.my_planets.column.resources")}</th>
<th>{i18n.t("game.report.section.my_planets.column.population")}</th> <th class="numeric">{i18n.t("game.report.section.my_planets.column.population")}</th>
<th>{i18n.t("game.report.section.my_planets.column.industry")}</th> <th class="numeric">{i18n.t("game.report.section.my_planets.column.industry")}</th>
<th> <th class="numeric">
{i18n.t("game.report.section.my_planets.column.industry_stockpile")} {i18n.t("game.report.section.my_planets.column.industry_stockpile")}
</th> </th>
<th> <th class="numeric">
{i18n.t("game.report.section.my_planets.column.materials_stockpile")} {i18n.t("game.report.section.my_planets.column.materials_stockpile")}
</th> </th>
<th>{i18n.t("game.report.section.my_planets.column.colonists")}</th> <th class="numeric">{i18n.t("game.report.section.my_planets.column.colonists")}</th>
<th>{i18n.t("game.report.section.my_planets.column.production")}</th> <th>{i18n.t("game.report.section.my_planets.column.production")}</th>
<th>{i18n.t("game.report.section.my_planets.column.free_industry")}</th> <th class="numeric">{i18n.t("game.report.section.my_planets.column.free_industry")}</th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
@@ -62,16 +62,16 @@ column set (matches `ReportPlanet` shape).
<tr data-testid="my-planets-row" data-number={p.number}> <tr data-testid="my-planets-row" data-number={p.number}>
<td>{p.number}</td> <td>{p.number}</td>
<td>{p.name}</td> <td>{p.name}</td>
<td>{formatFloat(p.x)}, {formatFloat(p.y)}</td> <td class="numeric">{formatFloat(p.x)}, {formatFloat(p.y)}</td>
<td>{formatFloat(p.size ?? 0)}</td> <td class="numeric">{formatFloat(p.size ?? 0)}</td>
<td>{formatFloat(p.resources ?? 0)}</td> <td class="numeric">{formatFloat(p.resources ?? 0)}</td>
<td>{formatFloat(p.population ?? 0)}</td> <td class="numeric">{formatFloat(p.population ?? 0)}</td>
<td>{formatFloat(p.industry ?? 0)}</td> <td class="numeric">{formatFloat(p.industry ?? 0)}</td>
<td>{formatFloat(p.industryStockpile ?? 0)}</td> <td class="numeric">{formatFloat(p.industryStockpile ?? 0)}</td>
<td>{formatFloat(p.materialsStockpile ?? 0)}</td> <td class="numeric">{formatFloat(p.materialsStockpile ?? 0)}</td>
<td>{formatFloat(p.colonists ?? 0)}</td> <td class="numeric">{formatFloat(p.colonists ?? 0)}</td>
<td>{p.production ?? "—"}</td> <td>{p.production ?? "—"}</td>
<td>{formatFloat(p.freeIndustry ?? 0)}</td> <td class="numeric">{formatFloat(p.freeIndustry ?? 0)}</td>
</tr> </tr>
{/each} {/each}
</tbody> </tbody>
@@ -94,7 +94,7 @@ column set (matches `ReportPlanet` shape).
border-collapse: collapse; border-collapse: collapse;
width: 100%; width: 100%;
font-variant-numeric: tabular-nums; font-variant-numeric: tabular-nums;
font-size: 0.9rem; font-size: 0.85rem;
} }
.grid th, .grid th,
.grid td { .grid td {
@@ -108,6 +108,11 @@ column set (matches `ReportPlanet` shape).
letter-spacing: 0.04em; letter-spacing: 0.04em;
font-size: 0.75rem; font-size: 0.75rem;
} }
.grid th.numeric,
.grid td.numeric {
font-family: var(--font-mono);
text-align: right;
}
.grid tbody tr:hover { .grid tbody tr:hover {
background: var(--color-surface); background: var(--color-surface);
} }
@@ -39,20 +39,20 @@ table).
<thead> <thead>
<tr> <tr>
<th>{i18n.t("game.report.section.my_sciences.column.name")}</th> <th>{i18n.t("game.report.section.my_sciences.column.name")}</th>
<th>{i18n.t("game.report.section.my_sciences.column.drive")}</th> <th class="numeric">{i18n.t("game.report.section.my_sciences.column.drive")}</th>
<th>{i18n.t("game.report.section.my_sciences.column.weapons")}</th> <th class="numeric">{i18n.t("game.report.section.my_sciences.column.weapons")}</th>
<th>{i18n.t("game.report.section.my_sciences.column.shields")}</th> <th class="numeric">{i18n.t("game.report.section.my_sciences.column.shields")}</th>
<th>{i18n.t("game.report.section.my_sciences.column.cargo")}</th> <th class="numeric">{i18n.t("game.report.section.my_sciences.column.cargo")}</th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
{#each rows as r (r.name)} {#each rows as r (r.name)}
<tr data-testid="my-sciences-row" data-name={r.name}> <tr data-testid="my-sciences-row" data-name={r.name}>
<td>{r.name}</td> <td>{r.name}</td>
<td>{formatPercent(r.drive)}</td> <td class="numeric">{formatPercent(r.drive)}</td>
<td>{formatPercent(r.weapons)}</td> <td class="numeric">{formatPercent(r.weapons)}</td>
<td>{formatPercent(r.shields)}</td> <td class="numeric">{formatPercent(r.shields)}</td>
<td>{formatPercent(r.cargo)}</td> <td class="numeric">{formatPercent(r.cargo)}</td>
</tr> </tr>
{/each} {/each}
</tbody> </tbody>
@@ -75,7 +75,7 @@ table).
border-collapse: collapse; border-collapse: collapse;
width: 100%; width: 100%;
font-variant-numeric: tabular-nums; font-variant-numeric: tabular-nums;
font-size: 0.9rem; font-size: 0.85rem;
} }
.grid th, .grid th,
.grid td { .grid td {
@@ -89,6 +89,11 @@ table).
letter-spacing: 0.04em; letter-spacing: 0.04em;
font-size: 0.75rem; font-size: 0.75rem;
} }
.grid th.numeric,
.grid td.numeric {
font-family: var(--font-mono);
text-align: right;
}
.grid tbody tr:hover { .grid tbody tr:hover {
background: var(--color-surface); background: var(--color-surface);
} }
@@ -40,22 +40,22 @@ drafts immediately, matching the ship-class designer's behaviour.
<thead> <thead>
<tr> <tr>
<th>{i18n.t("game.report.section.my_ship_classes.column.name")}</th> <th>{i18n.t("game.report.section.my_ship_classes.column.name")}</th>
<th>{i18n.t("game.report.section.my_ship_classes.column.drive")}</th> <th class="numeric">{i18n.t("game.report.section.my_ship_classes.column.drive")}</th>
<th>{i18n.t("game.report.section.my_ship_classes.column.armament")}</th> <th>{i18n.t("game.report.section.my_ship_classes.column.armament")}</th>
<th>{i18n.t("game.report.section.my_ship_classes.column.weapons")}</th> <th class="numeric">{i18n.t("game.report.section.my_ship_classes.column.weapons")}</th>
<th>{i18n.t("game.report.section.my_ship_classes.column.shields")}</th> <th class="numeric">{i18n.t("game.report.section.my_ship_classes.column.shields")}</th>
<th>{i18n.t("game.report.section.my_ship_classes.column.cargo")}</th> <th class="numeric">{i18n.t("game.report.section.my_ship_classes.column.cargo")}</th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
{#each rows as r (r.name)} {#each rows as r (r.name)}
<tr data-testid="my-ship-classes-row" data-name={r.name}> <tr data-testid="my-ship-classes-row" data-name={r.name}>
<td>{r.name}</td> <td>{r.name}</td>
<td>{formatFloat(r.drive)}</td> <td class="numeric">{formatFloat(r.drive)}</td>
<td>{r.armament}</td> <td>{r.armament}</td>
<td>{formatFloat(r.weapons)}</td> <td class="numeric">{formatFloat(r.weapons)}</td>
<td>{formatFloat(r.shields)}</td> <td class="numeric">{formatFloat(r.shields)}</td>
<td>{formatFloat(r.cargo)}</td> <td class="numeric">{formatFloat(r.cargo)}</td>
</tr> </tr>
{/each} {/each}
</tbody> </tbody>
@@ -78,7 +78,7 @@ drafts immediately, matching the ship-class designer's behaviour.
border-collapse: collapse; border-collapse: collapse;
width: 100%; width: 100%;
font-variant-numeric: tabular-nums; font-variant-numeric: tabular-nums;
font-size: 0.9rem; font-size: 0.85rem;
} }
.grid th, .grid th,
.grid td { .grid td {
@@ -92,6 +92,11 @@ drafts immediately, matching the ship-class designer's behaviour.
letter-spacing: 0.04em; letter-spacing: 0.04em;
font-size: 0.75rem; font-size: 0.75rem;
} }
.grid th.numeric,
.grid td.numeric {
font-family: var(--font-mono);
text-align: right;
}
.grid tbody tr:hover { .grid tbody tr:hover {
background: var(--color-surface); background: var(--color-surface);
} }
@@ -59,8 +59,8 @@ shown together with `load` when carrying.
<th>{i18n.t("game.report.section.my_ship_groups.column.destination")}</th> <th>{i18n.t("game.report.section.my_ship_groups.column.destination")}</th>
<th>{i18n.t("game.report.section.my_ship_groups.column.origin")}</th> <th>{i18n.t("game.report.section.my_ship_groups.column.origin")}</th>
<th>{i18n.t("game.report.section.my_ship_groups.column.range")}</th> <th>{i18n.t("game.report.section.my_ship_groups.column.range")}</th>
<th>{i18n.t("game.report.section.my_ship_groups.column.speed")}</th> <th class="numeric">{i18n.t("game.report.section.my_ship_groups.column.speed")}</th>
<th>{i18n.t("game.report.section.my_ship_groups.column.mass")}</th> <th class="numeric">{i18n.t("game.report.section.my_ship_groups.column.mass")}</th>
<th>{i18n.t("game.report.section.my_ship_groups.column.fleet")}</th> <th>{i18n.t("game.report.section.my_ship_groups.column.fleet")}</th>
</tr> </tr>
</thead> </thead>
@@ -77,8 +77,8 @@ shown together with `load` when carrying.
{g.origin === null ? "—" : planetLabel(g.origin, planets)} {g.origin === null ? "—" : planetLabel(g.origin, planets)}
</td> </td>
<td>{g.range === null ? "—" : formatFloat(g.range)}</td> <td>{g.range === null ? "—" : formatFloat(g.range)}</td>
<td>{formatFloat(g.speed)}</td> <td class="numeric">{formatFloat(g.speed)}</td>
<td>{formatFloat(g.mass)}</td> <td class="numeric">{formatFloat(g.mass)}</td>
<td>{g.fleet ?? "—"}</td> <td>{g.fleet ?? "—"}</td>
</tr> </tr>
{/each} {/each}
@@ -102,7 +102,7 @@ shown together with `load` when carrying.
border-collapse: collapse; border-collapse: collapse;
width: 100%; width: 100%;
font-variant-numeric: tabular-nums; font-variant-numeric: tabular-nums;
font-size: 0.9rem; font-size: 0.85rem;
} }
.grid th, .grid th,
.grid td { .grid td {
@@ -116,6 +116,11 @@ shown together with `load` when carrying.
letter-spacing: 0.04em; letter-spacing: 0.04em;
font-size: 0.75rem; font-size: 0.75rem;
} }
.grid th.numeric,
.grid td.numeric {
font-family: var(--font-mono);
text-align: right;
}
.grid tbody tr:hover { .grid tbody tr:hover {
background: var(--color-surface); background: var(--color-surface);
} }
@@ -14,7 +14,7 @@ highlight so the user can locate themselves quickly.
RENDERED_REPORT_CONTEXT_KEY, RENDERED_REPORT_CONTEXT_KEY,
type RenderedReportSource, type RenderedReportSource,
} from "$lib/rendered-report.svelte"; } from "$lib/rendered-report.svelte";
import { formatCount, formatPercent, formatVotes } from "./format"; import { formatCount, formatFloat, formatVotes } from "./format";
const rendered = getContext<RenderedReportSource | undefined>( const rendered = getContext<RenderedReportSource | undefined>(
RENDERED_REPORT_CONTEXT_KEY, RENDERED_REPORT_CONTEXT_KEY,
@@ -37,14 +37,14 @@ highlight so the user can locate themselves quickly.
<thead> <thead>
<tr> <tr>
<th>{i18n.t("game.report.section.player_status.column.name")}</th> <th>{i18n.t("game.report.section.player_status.column.name")}</th>
<th>{i18n.t("game.report.section.player_status.column.drive")}</th> <th class="numeric">{i18n.t("game.report.section.player_status.column.drive")}</th>
<th>{i18n.t("game.report.section.player_status.column.weapons")}</th> <th class="numeric">{i18n.t("game.report.section.player_status.column.weapons")}</th>
<th>{i18n.t("game.report.section.player_status.column.shields")}</th> <th class="numeric">{i18n.t("game.report.section.player_status.column.shields")}</th>
<th>{i18n.t("game.report.section.player_status.column.cargo")}</th> <th class="numeric">{i18n.t("game.report.section.player_status.column.cargo")}</th>
<th>{i18n.t("game.report.section.player_status.column.population")}</th> <th class="numeric">{i18n.t("game.report.section.player_status.column.population")}</th>
<th>{i18n.t("game.report.section.player_status.column.industry")}</th> <th class="numeric">{i18n.t("game.report.section.player_status.column.industry")}</th>
<th>{i18n.t("game.report.section.player_status.column.planets")}</th> <th class="numeric">{i18n.t("game.report.section.player_status.column.planets")}</th>
<th>{i18n.t("game.report.section.player_status.column.votes")}</th> <th class="numeric">{i18n.t("game.report.section.player_status.column.votes")}</th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
@@ -73,14 +73,14 @@ highlight so the user can locate themselves quickly.
</span> </span>
{/if} {/if}
</td> </td>
<td>{formatPercent(p.drive)}</td> <td class="numeric">{formatFloat(p.drive)}</td>
<td>{formatPercent(p.weapons)}</td> <td class="numeric">{formatFloat(p.weapons)}</td>
<td>{formatPercent(p.shields)}</td> <td class="numeric">{formatFloat(p.shields)}</td>
<td>{formatPercent(p.cargo)}</td> <td class="numeric">{formatFloat(p.cargo)}</td>
<td>{formatCount(p.population)}</td> <td class="numeric">{formatCount(p.population)}</td>
<td>{formatCount(p.industry)}</td> <td class="numeric">{formatCount(p.industry)}</td>
<td>{formatCount(p.planets)}</td> <td class="numeric">{formatCount(p.planets)}</td>
<td>{formatVotes(p.votesReceived)}</td> <td class="numeric">{formatVotes(p.votesReceived)}</td>
</tr> </tr>
{/each} {/each}
</tbody> </tbody>
@@ -103,7 +103,7 @@ highlight so the user can locate themselves quickly.
border-collapse: collapse; border-collapse: collapse;
width: 100%; width: 100%;
font-variant-numeric: tabular-nums; font-variant-numeric: tabular-nums;
font-size: 0.9rem; font-size: 0.85rem;
} }
.grid th, .grid th,
.grid td { .grid td {
@@ -117,6 +117,11 @@ highlight so the user can locate themselves quickly.
letter-spacing: 0.04em; letter-spacing: 0.04em;
font-size: 0.75rem; font-size: 0.75rem;
} }
.grid th.numeric,
.grid td.numeric {
font-family: var(--font-mono);
text-align: right;
}
.grid tbody tr:hover { .grid tbody tr:hover {
background: var(--color-surface); background: var(--color-surface);
} }
@@ -41,12 +41,12 @@ reads `#17 (Castle)` rather than just `#17`.
<tr> <tr>
<th>{i18n.t("game.report.section.ships_in_production.column.planet")}</th> <th>{i18n.t("game.report.section.ships_in_production.column.planet")}</th>
<th>{i18n.t("game.report.section.ships_in_production.column.class")}</th> <th>{i18n.t("game.report.section.ships_in_production.column.class")}</th>
<th>{i18n.t("game.report.section.ships_in_production.column.cost")}</th> <th class="numeric">{i18n.t("game.report.section.ships_in_production.column.cost")}</th>
<th> <th class="numeric">
{i18n.t("game.report.section.ships_in_production.column.prod_used")} {i18n.t("game.report.section.ships_in_production.column.prod_used")}
</th> </th>
<th>{i18n.t("game.report.section.ships_in_production.column.percent")}</th> <th>{i18n.t("game.report.section.ships_in_production.column.percent")}</th>
<th>{i18n.t("game.report.section.ships_in_production.column.free")}</th> <th class="numeric">{i18n.t("game.report.section.ships_in_production.column.free")}</th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
@@ -58,10 +58,10 @@ reads `#17 (Castle)` rather than just `#17`.
> >
<td>{planetLabel(r.planetNumber, planets)}</td> <td>{planetLabel(r.planetNumber, planets)}</td>
<td>{r.class}</td> <td>{r.class}</td>
<td>{formatFloat(r.cost)}</td> <td class="numeric">{formatFloat(r.cost)}</td>
<td>{formatFloat(r.prodUsed)}</td> <td class="numeric">{formatFloat(r.prodUsed)}</td>
<td>{(r.percent * 100).toFixed(1)}</td> <td>{(r.percent * 100).toFixed(1)}</td>
<td>{formatFloat(r.freeIndustry)}</td> <td class="numeric">{formatFloat(r.freeIndustry)}</td>
</tr> </tr>
{/each} {/each}
</tbody> </tbody>
@@ -84,7 +84,7 @@ reads `#17 (Castle)` rather than just `#17`.
border-collapse: collapse; border-collapse: collapse;
width: 100%; width: 100%;
font-variant-numeric: tabular-nums; font-variant-numeric: tabular-nums;
font-size: 0.9rem; font-size: 0.85rem;
} }
.grid th, .grid th,
.grid td { .grid td {
@@ -98,6 +98,11 @@ reads `#17 (Castle)` rather than just `#17`.
letter-spacing: 0.04em; letter-spacing: 0.04em;
font-size: 0.75rem; font-size: 0.75rem;
} }
.grid th.numeric,
.grid td.numeric {
font-family: var(--font-mono);
text-align: right;
}
.grid tbody tr:hover { .grid tbody tr:hover {
background: var(--color-surface); background: var(--color-surface);
} }
@@ -37,15 +37,15 @@ radar that doesn't even resolve to a planet.
<table class="grid" data-testid="unidentified-groups-table"> <table class="grid" data-testid="unidentified-groups-table">
<thead> <thead>
<tr> <tr>
<th>{i18n.t("game.report.section.unidentified_groups.column.x")}</th> <th class="numeric">{i18n.t("game.report.section.unidentified_groups.column.x")}</th>
<th>{i18n.t("game.report.section.unidentified_groups.column.y")}</th> <th class="numeric">{i18n.t("game.report.section.unidentified_groups.column.y")}</th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
{#each rows as g, i (i)} {#each rows as g, i (i)}
<tr data-testid="unidentified-groups-row"> <tr data-testid="unidentified-groups-row">
<td>{formatFloat(g.x)}</td> <td class="numeric">{formatFloat(g.x)}</td>
<td>{formatFloat(g.y)}</td> <td class="numeric">{formatFloat(g.y)}</td>
</tr> </tr>
{/each} {/each}
</tbody> </tbody>
@@ -68,7 +68,7 @@ radar that doesn't even resolve to a planet.
border-collapse: collapse; border-collapse: collapse;
width: 100%; width: 100%;
font-variant-numeric: tabular-nums; font-variant-numeric: tabular-nums;
font-size: 0.9rem; font-size: 0.85rem;
} }
.grid th, .grid th,
.grid td { .grid td {
@@ -82,6 +82,11 @@ radar that doesn't even resolve to a planet.
letter-spacing: 0.04em; letter-spacing: 0.04em;
font-size: 0.75rem; font-size: 0.75rem;
} }
.grid th.numeric,
.grid td.numeric {
font-family: var(--font-mono);
text-align: right;
}
.grid tbody tr:hover { .grid tbody tr:hover {
background: var(--color-surface); background: var(--color-surface);
} }
@@ -42,13 +42,13 @@ are intentionally omitted.
<tr> <tr>
<th>{i18n.t("game.report.section.my_planets.column.number")}</th> <th>{i18n.t("game.report.section.my_planets.column.number")}</th>
<th>{i18n.t("game.report.section.my_planets.column.name")}</th> <th>{i18n.t("game.report.section.my_planets.column.name")}</th>
<th>{i18n.t("game.report.section.my_planets.column.coordinates")}</th> <th class="numeric">{i18n.t("game.report.section.my_planets.column.coordinates")}</th>
<th>{i18n.t("game.report.section.my_planets.column.size")}</th> <th class="numeric">{i18n.t("game.report.section.my_planets.column.size")}</th>
<th>{i18n.t("game.report.section.my_planets.column.resources")}</th> <th class="numeric">{i18n.t("game.report.section.my_planets.column.resources")}</th>
<th> <th class="numeric">
{i18n.t("game.report.section.my_planets.column.industry_stockpile")} {i18n.t("game.report.section.my_planets.column.industry_stockpile")}
</th> </th>
<th> <th class="numeric">
{i18n.t("game.report.section.my_planets.column.materials_stockpile")} {i18n.t("game.report.section.my_planets.column.materials_stockpile")}
</th> </th>
</tr> </tr>
@@ -58,11 +58,11 @@ are intentionally omitted.
<tr data-testid="uninhabited-planets-row" data-number={p.number}> <tr data-testid="uninhabited-planets-row" data-number={p.number}>
<td>{p.number}</td> <td>{p.number}</td>
<td>{p.name}</td> <td>{p.name}</td>
<td>{formatFloat(p.x)}, {formatFloat(p.y)}</td> <td class="numeric">{formatFloat(p.x)}, {formatFloat(p.y)}</td>
<td>{formatFloat(p.size ?? 0)}</td> <td class="numeric">{formatFloat(p.size ?? 0)}</td>
<td>{formatFloat(p.resources ?? 0)}</td> <td class="numeric">{formatFloat(p.resources ?? 0)}</td>
<td>{formatFloat(p.industryStockpile ?? 0)}</td> <td class="numeric">{formatFloat(p.industryStockpile ?? 0)}</td>
<td>{formatFloat(p.materialsStockpile ?? 0)}</td> <td class="numeric">{formatFloat(p.materialsStockpile ?? 0)}</td>
</tr> </tr>
{/each} {/each}
</tbody> </tbody>
@@ -85,7 +85,7 @@ are intentionally omitted.
border-collapse: collapse; border-collapse: collapse;
width: 100%; width: 100%;
font-variant-numeric: tabular-nums; font-variant-numeric: tabular-nums;
font-size: 0.9rem; font-size: 0.85rem;
} }
.grid th, .grid th,
.grid td { .grid td {
@@ -99,6 +99,11 @@ are intentionally omitted.
letter-spacing: 0.04em; letter-spacing: 0.04em;
font-size: 0.75rem; font-size: 0.75rem;
} }
.grid th.numeric,
.grid td.numeric {
font-family: var(--font-mono);
text-align: right;
}
.grid tbody tr:hover { .grid tbody tr:hover {
background: var(--color-surface); background: var(--color-surface);
} }
@@ -40,14 +40,14 @@ else is known.
<thead> <thead>
<tr> <tr>
<th>{i18n.t("game.report.section.my_planets.column.number")}</th> <th>{i18n.t("game.report.section.my_planets.column.number")}</th>
<th>{i18n.t("game.report.section.my_planets.column.coordinates")}</th> <th class="numeric">{i18n.t("game.report.section.my_planets.column.coordinates")}</th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
{#each rows as p (p.number)} {#each rows as p (p.number)}
<tr data-testid="unknown-planets-row" data-number={p.number}> <tr data-testid="unknown-planets-row" data-number={p.number}>
<td>{p.number}</td> <td>{p.number}</td>
<td>{formatFloat(p.x)}, {formatFloat(p.y)}</td> <td class="numeric">{formatFloat(p.x)}, {formatFloat(p.y)}</td>
</tr> </tr>
{/each} {/each}
</tbody> </tbody>
@@ -70,7 +70,7 @@ else is known.
border-collapse: collapse; border-collapse: collapse;
width: 100%; width: 100%;
font-variant-numeric: tabular-nums; font-variant-numeric: tabular-nums;
font-size: 0.9rem; font-size: 0.85rem;
} }
.grid th, .grid th,
.grid td { .grid td {
@@ -84,6 +84,11 @@ else is known.
letter-spacing: 0.04em; letter-spacing: 0.04em;
font-size: 0.75rem; font-size: 0.75rem;
} }
.grid th.numeric,
.grid td.numeric {
font-family: var(--font-mono);
text-align: right;
}
.grid tbody tr:hover { .grid tbody tr:hover {
background: var(--color-surface); background: var(--color-surface);
} }
@@ -58,14 +58,14 @@ explanatory text on the races table.
<thead> <thead>
<tr> <tr>
<th>{i18n.t("game.report.section.votes.column.race")}</th> <th>{i18n.t("game.report.section.votes.column.race")}</th>
<th>{i18n.t("game.report.section.votes.column.votes")}</th> <th class="numeric">{i18n.t("game.report.section.votes.column.votes")}</th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
{#each races as r (r.name)} {#each races as r (r.name)}
<tr data-testid="votes-received-row" data-race={r.name}> <tr data-testid="votes-received-row" data-race={r.name}>
<td>{r.name}</td> <td>{r.name}</td>
<td>{formatVotes(r.votesReceived)}</td> <td class="numeric">{formatVotes(r.votesReceived)}</td>
</tr> </tr>
{/each} {/each}
</tbody> </tbody>
@@ -113,7 +113,7 @@ explanatory text on the races table.
.grid { .grid {
border-collapse: collapse; border-collapse: collapse;
font-variant-numeric: tabular-nums; font-variant-numeric: tabular-nums;
font-size: 0.9rem; font-size: 0.85rem;
} }
.grid th, .grid th,
.grid td { .grid td {
@@ -35,6 +35,10 @@ data fetching is performed here — the layout is responsible.
} from "../../sync/order-draft.svelte"; } from "../../sync/order-draft.svelte";
import type { Relation } from "../../sync/order-types"; import type { Relation } from "../../sync/order-types";
import ViewState from "$lib/ui/view-state.svelte"; import ViewState from "$lib/ui/view-state.svelte";
import {
formatFloat,
formatInt,
} from "$lib/util/number-format";
type SortColumn = type SortColumn =
| "name" | "name"
@@ -122,31 +126,6 @@ data fetching is performed here — the layout is responsible.
return sortDirection === "asc" ? "ascending" : "descending"; return sortDirection === "asc" ? "ascending" : "descending";
} }
// Render a fraction in `[0, 1]` as a one-decimal percent
// (`0.225` → `"22.5"`). The conversion is value-only — no `%`
// suffix — so the column header carries the unit. Matches the
// sciences-table convention.
function formatPercent(fraction: number): string {
return (fraction * 100).toLocaleString(undefined, {
minimumFractionDigits: 0,
maximumFractionDigits: 1,
});
}
function formatCount(value: number): string {
return value.toLocaleString(undefined, {
minimumFractionDigits: 0,
maximumFractionDigits: 0,
});
}
function formatVotes(value: number): string {
return value.toLocaleString(undefined, {
minimumFractionDigits: 0,
maximumFractionDigits: 2,
});
}
async function setStance(acceptor: string, relation: Relation): Promise<void> { async function setStance(acceptor: string, relation: Relation): Promise<void> {
if (draft === undefined) return; if (draft === undefined) return;
// No-op when the row already reflects the requested stance — the // No-op when the row already reflects the requested stance — the
@@ -192,7 +171,7 @@ data fetching is performed here — the layout is responsible.
<span class="summary-label"> <span class="summary-label">
{i18n.t("game.table.races.votes.mine")}: {i18n.t("game.table.races.votes.mine")}:
</span> </span>
<span class="summary-value">{formatVotes(myVotes)}</span> <span class="summary-value numeric">{formatFloat(myVotes)}</span>
</span> </span>
<label class="summary-cell vote-picker"> <label class="summary-cell vote-picker">
<span class="summary-label"> <span class="summary-label">
@@ -244,7 +223,7 @@ data fetching is performed here — the layout is responsible.
<thead> <thead>
<tr> <tr>
{#each COLUMNS as column (column)} {#each COLUMNS as column (column)}
<th aria-sort={ariaSort(column)}> <th aria-sort={ariaSort(column)} class:numeric={column !== "name"}>
<button <button
type="button" type="button"
class="sort" class="sort"
@@ -267,23 +246,29 @@ data fetching is performed here — the layout is responsible.
{#each sorted as r (r.name)} {#each sorted as r (r.name)}
<tr data-testid="races-row" data-name={r.name}> <tr data-testid="races-row" data-name={r.name}>
<td data-testid="races-cell-name">{r.name}</td> <td data-testid="races-cell-name">{r.name}</td>
<td data-testid="races-cell-drive">{formatPercent(r.drive)}</td> <td class="numeric" data-testid="races-cell-drive">
<td data-testid="races-cell-weapons"> {formatFloat(r.drive)}
{formatPercent(r.weapons)}
</td> </td>
<td data-testid="races-cell-shields"> <td class="numeric" data-testid="races-cell-weapons">
{formatPercent(r.shields)} {formatFloat(r.weapons)}
</td> </td>
<td data-testid="races-cell-cargo">{formatPercent(r.cargo)}</td> <td class="numeric" data-testid="races-cell-shields">
<td data-testid="races-cell-population"> {formatFloat(r.shields)}
{formatCount(r.population)}
</td> </td>
<td data-testid="races-cell-industry"> <td class="numeric" data-testid="races-cell-cargo">
{formatCount(r.industry)} {formatFloat(r.cargo)}
</td> </td>
<td data-testid="races-cell-planets">{formatCount(r.planets)}</td> <td class="numeric" data-testid="races-cell-population">
<td data-testid="races-cell-votes"> {formatInt(r.population)}
{formatVotes(r.votesReceived)} </td>
<td class="numeric" data-testid="races-cell-industry">
{formatInt(r.industry)}
</td>
<td class="numeric" data-testid="races-cell-planets">
{formatInt(r.planets)}
</td>
<td class="numeric" data-testid="races-cell-votes">
{formatFloat(r.votesReceived)}
</td> </td>
<td> <td>
<div <div
@@ -359,6 +344,9 @@ data fetching is performed here — the layout is responsible.
color: var(--color-text); color: var(--color-text);
font-variant-numeric: tabular-nums; font-variant-numeric: tabular-nums;
} }
.summary-value.numeric {
font-family: var(--font-mono);
}
.vote-picker select { .vote-picker select {
font: inherit; font: inherit;
padding: 0.2rem 0.4rem; padding: 0.2rem 0.4rem;
@@ -403,7 +391,7 @@ data fetching is performed here — the layout is responsible.
padding: 0.4rem 0.6rem; padding: 0.4rem 0.6rem;
text-align: left; text-align: left;
border-bottom: 1px solid var(--color-border-subtle); border-bottom: 1px solid var(--color-border-subtle);
font-size: 0.9rem; font-size: 0.85rem;
} }
.grid th { .grid th {
color: var(--color-text-muted); color: var(--color-text-muted);
@@ -411,6 +399,14 @@ data fetching is performed here — the layout is responsible.
letter-spacing: 0.04em; letter-spacing: 0.04em;
font-size: 0.75rem; font-size: 0.75rem;
} }
.grid th.numeric,
.grid td.numeric {
font-family: var(--font-mono);
text-align: right;
}
.grid th.numeric .sort {
justify-content: flex-end;
}
.grid tbody tr:hover { .grid tbody tr:hover {
background: var(--color-surface); background: var(--color-surface);
} }
@@ -31,6 +31,7 @@ data fetching is performed here — the shell is responsible.
ORDER_DRAFT_CONTEXT_KEY, ORDER_DRAFT_CONTEXT_KEY,
OrderDraftStore, OrderDraftStore,
} from "../../sync/order-draft.svelte"; } from "../../sync/order-draft.svelte";
import { formatPercent } from "$lib/util/number-format";
type SortColumn = "name" | "drive" | "weapons" | "shields" | "cargo"; type SortColumn = "name" | "drive" | "weapons" | "shields" | "cargo";
type SortDirection = "asc" | "desc"; type SortDirection = "asc" | "desc";
@@ -103,16 +104,6 @@ data fetching is performed here — the shell is responsible.
return sortDirection === "asc" ? "ascending" : "descending"; return sortDirection === "asc" ? "ascending" : "descending";
} }
// Render a fraction in `[0, 1]` as a one-decimal percent
// (`0.225` → `"22.5"`). The conversion is value-only — no `%`
// suffix — so callers can decorate independently.
function formatPercent(fraction: number): string {
return (fraction * 100).toLocaleString(undefined, {
minimumFractionDigits: 0,
maximumFractionDigits: 1,
});
}
function openDesigner(name: string): void { function openDesigner(name: string): void {
activeView.select("designer-science", { scienceId: name }); activeView.select("designer-science", { scienceId: name });
} }
@@ -174,7 +165,7 @@ data fetching is performed here — the shell is responsible.
<thead> <thead>
<tr> <tr>
{#each COLUMNS as column (column)} {#each COLUMNS as column (column)}
<th aria-sort={ariaSort(column)}> <th aria-sort={ariaSort(column)} class:numeric={column !== "name"}>
<button <button
type="button" type="button"
class="sort" class="sort"
@@ -201,14 +192,18 @@ data fetching is performed here — the shell is responsible.
ondblclick={() => openDesigner(sci.name)} ondblclick={() => openDesigner(sci.name)}
> >
<td data-testid="sciences-cell-name">{sci.name}</td> <td data-testid="sciences-cell-name">{sci.name}</td>
<td data-testid="sciences-cell-drive">{formatPercent(sci.drive)}</td> <td class="numeric" data-testid="sciences-cell-drive">
<td data-testid="sciences-cell-weapons"> {formatPercent(sci.drive)}
</td>
<td class="numeric" data-testid="sciences-cell-weapons">
{formatPercent(sci.weapons)} {formatPercent(sci.weapons)}
</td> </td>
<td data-testid="sciences-cell-shields"> <td class="numeric" data-testid="sciences-cell-shields">
{formatPercent(sci.shields)} {formatPercent(sci.shields)}
</td> </td>
<td data-testid="sciences-cell-cargo">{formatPercent(sci.cargo)}</td> <td class="numeric" data-testid="sciences-cell-cargo">
{formatPercent(sci.cargo)}
</td>
<td> <td>
<button <button
type="button" type="button"
@@ -283,7 +278,7 @@ data fetching is performed here — the shell is responsible.
padding: 0.4rem 0.6rem; padding: 0.4rem 0.6rem;
text-align: left; text-align: left;
border-bottom: 1px solid var(--color-border-subtle); border-bottom: 1px solid var(--color-border-subtle);
font-size: 0.9rem; font-size: 0.85rem;
} }
.grid th { .grid th {
color: var(--color-text-muted); color: var(--color-text-muted);
@@ -291,6 +286,14 @@ data fetching is performed here — the shell is responsible.
letter-spacing: 0.04em; letter-spacing: 0.04em;
font-size: 0.75rem; font-size: 0.75rem;
} }
.grid th.numeric,
.grid td.numeric {
font-family: var(--font-mono);
text-align: right;
}
.grid th.numeric .sort {
justify-content: flex-end;
}
.grid tbody tr { .grid tbody tr {
cursor: pointer; cursor: pointer;
} }
@@ -27,6 +27,7 @@ data fetching is performed here — the layout is responsible.
} from "../../sync/order-draft.svelte"; } from "../../sync/order-draft.svelte";
import { calculatorLoadRequest } from "$lib/calculator/load-request.svelte"; import { calculatorLoadRequest } from "$lib/calculator/load-request.svelte";
import ViewState from "$lib/ui/view-state.svelte"; import ViewState from "$lib/ui/view-state.svelte";
import { formatFloat } from "$lib/util/number-format";
type SortColumn = type SortColumn =
| "name" | "name"
@@ -106,10 +107,6 @@ data fetching is performed here — the layout is responsible.
return sortDirection === "asc" ? "ascending" : "descending"; return sortDirection === "asc" ? "ascending" : "descending";
} }
function formatNumber(value: number): string {
return value.toLocaleString(undefined, { maximumFractionDigits: 2 });
}
function openInCalculator(name: string): void { function openInCalculator(name: string): void {
calculatorLoadRequest.request(name); calculatorLoadRequest.request(name);
} }
@@ -171,7 +168,7 @@ data fetching is performed here — the layout is responsible.
<thead> <thead>
<tr> <tr>
{#each COLUMNS as column (column)} {#each COLUMNS as column (column)}
<th aria-sort={ariaSort(column)}> <th aria-sort={ariaSort(column)} class:numeric={column !== "name"}>
<button <button
type="button" type="button"
class="sort" class="sort"
@@ -198,11 +195,21 @@ data fetching is performed here — the layout is responsible.
ondblclick={() => openInCalculator(cls.name)} ondblclick={() => openInCalculator(cls.name)}
> >
<td data-testid="ship-classes-cell-name">{cls.name}</td> <td data-testid="ship-classes-cell-name">{cls.name}</td>
<td data-testid="ship-classes-cell-drive">{formatNumber(cls.drive)}</td> <td class="numeric" data-testid="ship-classes-cell-drive">
<td data-testid="ship-classes-cell-armament">{cls.armament}</td> {formatFloat(cls.drive)}
<td data-testid="ship-classes-cell-weapons">{formatNumber(cls.weapons)}</td> </td>
<td data-testid="ship-classes-cell-shields">{formatNumber(cls.shields)}</td> <td class="numeric" data-testid="ship-classes-cell-armament">
<td data-testid="ship-classes-cell-cargo">{formatNumber(cls.cargo)}</td> {cls.armament}
</td>
<td class="numeric" data-testid="ship-classes-cell-weapons">
{formatFloat(cls.weapons)}
</td>
<td class="numeric" data-testid="ship-classes-cell-shields">
{formatFloat(cls.shields)}
</td>
<td class="numeric" data-testid="ship-classes-cell-cargo">
{formatFloat(cls.cargo)}
</td>
<td> <td>
<button <button
type="button" type="button"
@@ -277,7 +284,7 @@ data fetching is performed here — the layout is responsible.
padding: 0.4rem 0.6rem; padding: 0.4rem 0.6rem;
text-align: left; text-align: left;
border-bottom: 1px solid var(--color-border-subtle); border-bottom: 1px solid var(--color-border-subtle);
font-size: 0.9rem; font-size: 0.85rem;
} }
.grid th { .grid th {
color: var(--color-text-muted); color: var(--color-text-muted);
@@ -285,6 +292,14 @@ data fetching is performed here — the layout is responsible.
letter-spacing: 0.04em; letter-spacing: 0.04em;
font-size: 0.75rem; font-size: 0.75rem;
} }
.grid th.numeric,
.grid td.numeric {
font-family: var(--font-mono);
text-align: right;
}
.grid th.numeric .sort {
justify-content: flex-end;
}
.grid tbody tr { .grid tbody tr {
cursor: pointer; cursor: pointer;
} }
+12 -12
View File
@@ -282,8 +282,8 @@ const en = {
"game.inspector.planet.field.population": "population", "game.inspector.planet.field.population": "population",
"game.inspector.planet.field.colonists": "colonists", "game.inspector.planet.field.colonists": "colonists",
"game.inspector.planet.field.industry": "industry", "game.inspector.planet.field.industry": "industry",
"game.inspector.planet.field.industry_stockpile": "industry stockpile ($)", "game.inspector.planet.field.industry_stockpile": "industry ($)",
"game.inspector.planet.field.materials_stockpile": "materials stockpile (M)", "game.inspector.planet.field.materials_stockpile": "materials (M)",
"game.inspector.planet.field.natural_resources": "natural resources", "game.inspector.planet.field.natural_resources": "natural resources",
"game.inspector.planet.field.production": "current production", "game.inspector.planet.field.production": "current production",
"game.inspector.planet.field.free_industry": "free production", "game.inspector.planet.field.free_industry": "free production",
@@ -503,10 +503,10 @@ const en = {
"game.table.races.empty": "no other races known yet", "game.table.races.empty": "no other races known yet",
"game.table.races.filter.placeholder": "filter by name", "game.table.races.filter.placeholder": "filter by name",
"game.table.races.column.name": "name", "game.table.races.column.name": "name",
"game.table.races.column.drive": "drive %", "game.table.races.column.drive": "drive",
"game.table.races.column.weapons": "weapons %", "game.table.races.column.weapons": "weapons",
"game.table.races.column.shields": "shields %", "game.table.races.column.shields": "shields",
"game.table.races.column.cargo": "cargo %", "game.table.races.column.cargo": "cargo",
"game.table.races.column.population": "population", "game.table.races.column.population": "population",
"game.table.races.column.industry": "production", "game.table.races.column.industry": "production",
"game.table.races.column.planets": "planets", "game.table.races.column.planets": "planets",
@@ -627,10 +627,10 @@ const en = {
"game.report.section.votes.empty": "no votes cast yet", "game.report.section.votes.empty": "no votes cast yet",
"game.report.section.player_status.title": "player status", "game.report.section.player_status.title": "player status",
"game.report.section.player_status.column.name": "name", "game.report.section.player_status.column.name": "name",
"game.report.section.player_status.column.drive": "drive %", "game.report.section.player_status.column.drive": "drive",
"game.report.section.player_status.column.weapons": "weapons %", "game.report.section.player_status.column.weapons": "weapons",
"game.report.section.player_status.column.shields": "shields %", "game.report.section.player_status.column.shields": "shields",
"game.report.section.player_status.column.cargo": "cargo %", "game.report.section.player_status.column.cargo": "cargo",
"game.report.section.player_status.column.population": "population", "game.report.section.player_status.column.population": "population",
"game.report.section.player_status.column.industry": "production", "game.report.section.player_status.column.industry": "production",
"game.report.section.player_status.column.planets": "planets", "game.report.section.player_status.column.planets": "planets",
@@ -693,8 +693,8 @@ const en = {
"game.report.section.bombings.column.industry": "industry", "game.report.section.bombings.column.industry": "industry",
"game.report.section.bombings.column.population": "population", "game.report.section.bombings.column.population": "population",
"game.report.section.bombings.column.colonists": "colonists", "game.report.section.bombings.column.colonists": "colonists",
"game.report.section.bombings.column.industry_stockpile": "industry stockpile ($)", "game.report.section.bombings.column.industry_stockpile": "industry ($)",
"game.report.section.bombings.column.materials_stockpile": "materials stockpile (M)", "game.report.section.bombings.column.materials_stockpile": "materials (M)",
"game.report.section.bombings.column.attack_power": "attack power", "game.report.section.bombings.column.attack_power": "attack power",
"game.report.section.bombings.wiped": "wiped", "game.report.section.bombings.wiped": "wiped",
"game.report.section.approaching_groups.title": "approaching groups", "game.report.section.approaching_groups.title": "approaching groups",
+12 -12
View File
@@ -283,8 +283,8 @@ const ru: Record<keyof typeof en, string> = {
"game.inspector.planet.field.population": "население", "game.inspector.planet.field.population": "население",
"game.inspector.planet.field.colonists": "колонисты", "game.inspector.planet.field.colonists": "колонисты",
"game.inspector.planet.field.industry": "промышленность", "game.inspector.planet.field.industry": "промышленность",
"game.inspector.planet.field.industry_stockpile": "запасы промышленности ($)", "game.inspector.planet.field.industry_stockpile": "промышленность ($)",
"game.inspector.planet.field.materials_stockpile": "запасы сырья (M)", "game.inspector.planet.field.materials_stockpile": "сырьё (M)",
"game.inspector.planet.field.natural_resources": "природные ресурсы", "game.inspector.planet.field.natural_resources": "природные ресурсы",
"game.inspector.planet.field.production": "текущее производство", "game.inspector.planet.field.production": "текущее производство",
"game.inspector.planet.field.free_industry": "свободные мощности", "game.inspector.planet.field.free_industry": "свободные мощности",
@@ -504,10 +504,10 @@ const ru: Record<keyof typeof en, string> = {
"game.table.races.empty": "других рас пока не видно", "game.table.races.empty": "других рас пока не видно",
"game.table.races.filter.placeholder": "фильтр по имени", "game.table.races.filter.placeholder": "фильтр по имени",
"game.table.races.column.name": "имя", "game.table.races.column.name": "имя",
"game.table.races.column.drive": "двигатель %", "game.table.races.column.drive": "двигатель",
"game.table.races.column.weapons": "оружие %", "game.table.races.column.weapons": "оружие",
"game.table.races.column.shields": "защита %", "game.table.races.column.shields": "защита",
"game.table.races.column.cargo": "трюм %", "game.table.races.column.cargo": "трюм",
"game.table.races.column.population": "население", "game.table.races.column.population": "население",
"game.table.races.column.industry": "производство", "game.table.races.column.industry": "производство",
"game.table.races.column.planets": "планет", "game.table.races.column.planets": "планет",
@@ -628,10 +628,10 @@ const ru: Record<keyof typeof en, string> = {
"game.report.section.votes.empty": "голосов ещё нет", "game.report.section.votes.empty": "голосов ещё нет",
"game.report.section.player_status.title": "статус игроков", "game.report.section.player_status.title": "статус игроков",
"game.report.section.player_status.column.name": "имя", "game.report.section.player_status.column.name": "имя",
"game.report.section.player_status.column.drive": "двигатель %", "game.report.section.player_status.column.drive": "двигатель",
"game.report.section.player_status.column.weapons": "оружие %", "game.report.section.player_status.column.weapons": "оружие",
"game.report.section.player_status.column.shields": "защита %", "game.report.section.player_status.column.shields": "защита",
"game.report.section.player_status.column.cargo": "трюм %", "game.report.section.player_status.column.cargo": "трюм",
"game.report.section.player_status.column.population": "население", "game.report.section.player_status.column.population": "население",
"game.report.section.player_status.column.industry": "производство", "game.report.section.player_status.column.industry": "производство",
"game.report.section.player_status.column.planets": "планет", "game.report.section.player_status.column.planets": "планет",
@@ -694,8 +694,8 @@ const ru: Record<keyof typeof en, string> = {
"game.report.section.bombings.column.industry": "промышленность", "game.report.section.bombings.column.industry": "промышленность",
"game.report.section.bombings.column.population": "население", "game.report.section.bombings.column.population": "население",
"game.report.section.bombings.column.colonists": "колонисты", "game.report.section.bombings.column.colonists": "колонисты",
"game.report.section.bombings.column.industry_stockpile": "запас промышленности ($)", "game.report.section.bombings.column.industry_stockpile": "промышленность ($)",
"game.report.section.bombings.column.materials_stockpile": "запас материалов (M)", "game.report.section.bombings.column.materials_stockpile": "сырьё (M)",
"game.report.section.bombings.column.attack_power": "сила удара", "game.report.section.bombings.column.attack_power": "сила удара",
"game.report.section.bombings.wiped": "уничтожена", "game.report.section.bombings.wiped": "уничтожена",
"game.report.section.approaching_groups.title": "приближающиеся группы", "game.report.section.approaching_groups.title": "приближающиеся группы",
+17 -17
View File
@@ -31,6 +31,7 @@ field with five buttons.
validateEntityName, validateEntityName,
type EntityNameInvalidReason, type EntityNameInvalidReason,
} from "$lib/util/entity-name"; } from "$lib/util/entity-name";
import { formatFloat } from "$lib/util/number-format";
import CargoRoutes from "./planet/cargo-routes.svelte"; import CargoRoutes from "./planet/cargo-routes.svelte";
import Production from "./planet/production.svelte"; import Production from "./planet/production.svelte";
import ShipGroups from "./planet/ship-groups.svelte"; import ShipGroups from "./planet/ship-groups.svelte";
@@ -99,14 +100,10 @@ field with five buttons.
const kindLabel = $derived(i18n.t(kindKeyMap[planet.kind])); const kindLabel = $derived(i18n.t(kindKeyMap[planet.kind]));
const coordinates = $derived( const coordinates = $derived(
`(${formatNumber(planet.x)}, ${formatNumber(planet.y)})`, `${formatFloat(planet.x)}, ${formatFloat(planet.y)}`,
); );
const productionLabel = $derived(productionDisplay(planet.production)); const productionLabel = $derived(productionDisplay(planet.production));
function formatNumber(value: number): string {
return value.toLocaleString(undefined, { maximumFractionDigits: 2 });
}
function productionDisplay(value: string | null): string { function productionDisplay(value: string | null): string {
if (value === null || value === "") { if (value === null || value === "") {
return i18n.t("game.inspector.planet.production_none"); return i18n.t("game.inspector.planet.production_none");
@@ -252,55 +249,55 @@ field with five buttons.
<div class="field" data-testid="inspector-planet-field-coordinates"> <div class="field" data-testid="inspector-planet-field-coordinates">
<dt>{i18n.t("game.inspector.planet.field.coordinates")}</dt> <dt>{i18n.t("game.inspector.planet.field.coordinates")}</dt>
<dd>{coordinates}</dd> <dd class="numeric">{coordinates}</dd>
</div> </div>
{#if planet.size !== null} {#if planet.size !== null}
<div class="field" data-testid="inspector-planet-field-size"> <div class="field" data-testid="inspector-planet-field-size">
<dt>{i18n.t("game.inspector.planet.field.size")}</dt> <dt>{i18n.t("game.inspector.planet.field.size")}</dt>
<dd>{formatNumber(planet.size)}</dd> <dd class="numeric">{formatFloat(planet.size)}</dd>
</div> </div>
{/if} {/if}
{#if planet.resources !== null} {#if planet.resources !== null}
<div class="field" data-testid="inspector-planet-field-natural_resources"> <div class="field" data-testid="inspector-planet-field-natural_resources">
<dt>{i18n.t("game.inspector.planet.field.natural_resources")}</dt> <dt>{i18n.t("game.inspector.planet.field.natural_resources")}</dt>
<dd>{formatNumber(planet.resources)}</dd> <dd class="numeric">{formatFloat(planet.resources)}</dd>
</div> </div>
{/if} {/if}
{#if planet.population !== null} {#if planet.population !== null}
<div class="field" data-testid="inspector-planet-field-population"> <div class="field" data-testid="inspector-planet-field-population">
<dt>{i18n.t("game.inspector.planet.field.population")}</dt> <dt>{i18n.t("game.inspector.planet.field.population")}</dt>
<dd>{formatNumber(planet.population)}</dd> <dd class="numeric">{formatFloat(planet.population)}</dd>
</div> </div>
{/if} {/if}
{#if planet.colonists !== null} {#if planet.colonists !== null}
<div class="field" data-testid="inspector-planet-field-colonists"> <div class="field" data-testid="inspector-planet-field-colonists">
<dt>{i18n.t("game.inspector.planet.field.colonists")}</dt> <dt>{i18n.t("game.inspector.planet.field.colonists")}</dt>
<dd>{formatNumber(planet.colonists)}</dd> <dd class="numeric">{formatFloat(planet.colonists)}</dd>
</div> </div>
{/if} {/if}
{#if planet.industry !== null} {#if planet.industry !== null}
<div class="field" data-testid="inspector-planet-field-industry"> <div class="field" data-testid="inspector-planet-field-industry">
<dt>{i18n.t("game.inspector.planet.field.industry")}</dt> <dt>{i18n.t("game.inspector.planet.field.industry")}</dt>
<dd>{formatNumber(planet.industry)}</dd> <dd class="numeric">{formatFloat(planet.industry)}</dd>
</div> </div>
{/if} {/if}
{#if planet.industryStockpile !== null} {#if planet.industryStockpile !== null}
<div class="field" data-testid="inspector-planet-field-industry_stockpile"> <div class="field" data-testid="inspector-planet-field-industry_stockpile">
<dt>{i18n.t("game.inspector.planet.field.industry_stockpile")}</dt> <dt>{i18n.t("game.inspector.planet.field.industry_stockpile")}</dt>
<dd>{formatNumber(planet.industryStockpile)}</dd> <dd class="numeric">{formatFloat(planet.industryStockpile)}</dd>
</div> </div>
{/if} {/if}
{#if planet.materialsStockpile !== null} {#if planet.materialsStockpile !== null}
<div class="field" data-testid="inspector-planet-field-materials_stockpile"> <div class="field" data-testid="inspector-planet-field-materials_stockpile">
<dt>{i18n.t("game.inspector.planet.field.materials_stockpile")}</dt> <dt>{i18n.t("game.inspector.planet.field.materials_stockpile")}</dt>
<dd>{formatNumber(planet.materialsStockpile)}</dd> <dd class="numeric">{formatFloat(planet.materialsStockpile)}</dd>
</div> </div>
{/if} {/if}
@@ -314,7 +311,7 @@ field with five buttons.
{#if planet.freeIndustry !== null} {#if planet.freeIndustry !== null}
<div class="field" data-testid="inspector-planet-field-free_industry"> <div class="field" data-testid="inspector-planet-field-free_industry">
<dt>{i18n.t("game.inspector.planet.field.free_industry")}</dt> <dt>{i18n.t("game.inspector.planet.field.free_industry")}</dt>
<dd>{formatNumber(planet.freeIndustry)}</dd> <dd class="numeric">{formatFloat(planet.freeIndustry)}</dd>
</div> </div>
{/if} {/if}
</dl> </dl>
@@ -362,17 +359,20 @@ field with five buttons.
} }
.field dt { .field dt {
color: var(--color-text-muted); color: var(--color-text-muted);
font-size: 0.85rem; font-size: 0.8rem;
} }
.field dd { .field dd {
margin: 0; margin: 0;
font-variant-numeric: tabular-nums; font-variant-numeric: tabular-nums;
font-size: 0.9rem; font-size: 0.85rem;
}
.field dd.numeric {
font-family: var(--font-mono);
} }
.hint { .hint {
margin: 0; margin: 0;
color: var(--color-text-muted); color: var(--color-text-muted);
font-size: 0.85rem; font-size: 0.8rem;
} }
.action { .action {
align-self: flex-start; align-self: flex-start;
@@ -32,6 +32,7 @@ ship-groups table view with an additional `(planet, race)` filter.
SELECTION_CONTEXT_KEY, SELECTION_CONTEXT_KEY,
type SelectionStore, type SelectionStore,
} from "$lib/selection.svelte"; } from "$lib/selection.svelte";
import { formatFloat } from "$lib/util/number-format";
type Props = { type Props = {
planet: ReportPlanet; planet: ReportPlanet;
@@ -90,10 +91,6 @@ ship-groups table view with an additional `(planet, race)` filter.
return rows; return rows;
}); });
function formatNumber(value: number): string {
return value.toLocaleString(undefined, { maximumFractionDigits: 2 });
}
function selectLocalGroup(groupId: string): void { function selectLocalGroup(groupId: string): void {
if (selection === undefined) return; if (selection === undefined) return;
selection.selectShipGroup({ variant: "local", id: groupId }); selection.selectShipGroup({ variant: "local", id: groupId });
@@ -125,7 +122,7 @@ ship-groups table view with an additional `(planet, race)` filter.
</span> </span>
<span class="mass"> <span class="mass">
{i18n.t("game.inspector.planet.ship_groups.row.mass", { {i18n.t("game.inspector.planet.ship_groups.row.mass", {
mass: formatNumber(row.mass), mass: formatFloat(row.mass),
})} })}
</span> </span>
</button> </button>
@@ -141,7 +138,7 @@ ship-groups table view with an additional `(planet, race)` filter.
</span> </span>
<span class="mass"> <span class="mass">
{i18n.t("game.inspector.planet.ship_groups.row.mass", { {i18n.t("game.inspector.planet.ship_groups.row.mass", {
mass: formatNumber(row.mass), mass: formatFloat(row.mass),
})} })}
</span> </span>
{/if} {/if}
@@ -20,6 +20,7 @@ variant — for Phase 19 the inspector is intentionally read-only.
ShipClassSummary, ShipClassSummary,
} from "../../api/game-state"; } from "../../api/game-state";
import { i18n, type TranslationKey } from "$lib/i18n/index.svelte"; import { i18n, type TranslationKey } from "$lib/i18n/index.svelte";
import { formatFloat } from "$lib/util/number-format";
import Actions from "./ship-group/actions.svelte"; import Actions from "./ship-group/actions.svelte";
export type ShipGroupSelection = export type ShipGroupSelection =
@@ -82,10 +83,6 @@ variant — for Phase 19 the inspector is intentionally read-only.
return planet.name; return planet.name;
} }
function formatNumber(value: number): string {
return value.toLocaleString(undefined, { maximumFractionDigits: 2 });
}
function cargoLabel(cargo: "NONE" | "COL" | "CAP" | "MAT" | "EMP"): string { function cargoLabel(cargo: "NONE" | "COL" | "CAP" | "MAT" | "EMP"): string {
if (cargo === "NONE") { if (cargo === "NONE") {
return i18n.t("game.inspector.ship_group.cargo.none"); return i18n.t("game.inspector.ship_group.cargo.none");
@@ -132,27 +129,27 @@ variant — for Phase 19 the inspector is intentionally read-only.
<dl class="fields"> <dl class="fields">
<div class="field" data-testid="inspector-ship-group-field-count"> <div class="field" data-testid="inspector-ship-group-field-count">
<dt>{i18n.t("game.inspector.ship_group.field.count")}</dt> <dt>{i18n.t("game.inspector.ship_group.field.count")}</dt>
<dd>{g.count}</dd> <dd class="numeric">{g.count}</dd>
</div> </div>
<div class="field" data-testid="inspector-ship-group-field-drive"> <div class="field" data-testid="inspector-ship-group-field-drive">
<dt>{i18n.t("game.inspector.ship_group.field.drive")}</dt> <dt>{i18n.t("game.inspector.ship_group.field.drive")}</dt>
<dd>{formatNumber(g.tech.drive)}</dd> <dd class="numeric">{formatFloat(g.tech.drive)}</dd>
</div> </div>
<div class="field" data-testid="inspector-ship-group-field-weapons"> <div class="field" data-testid="inspector-ship-group-field-weapons">
<dt>{i18n.t("game.inspector.ship_group.field.weapons")}</dt> <dt>{i18n.t("game.inspector.ship_group.field.weapons")}</dt>
<dd>{formatNumber(g.tech.weapons)}</dd> <dd class="numeric">{formatFloat(g.tech.weapons)}</dd>
</div> </div>
<div class="field" data-testid="inspector-ship-group-field-shields"> <div class="field" data-testid="inspector-ship-group-field-shields">
<dt>{i18n.t("game.inspector.ship_group.field.shields")}</dt> <dt>{i18n.t("game.inspector.ship_group.field.shields")}</dt>
<dd>{formatNumber(g.tech.shields)}</dd> <dd class="numeric">{formatFloat(g.tech.shields)}</dd>
</div> </div>
<div class="field" data-testid="inspector-ship-group-field-cargo-tech"> <div class="field" data-testid="inspector-ship-group-field-cargo-tech">
<dt>{i18n.t("game.inspector.ship_group.field.cargo_tech")}</dt> <dt>{i18n.t("game.inspector.ship_group.field.cargo_tech")}</dt>
<dd>{formatNumber(g.tech.cargo)}</dd> <dd class="numeric">{formatFloat(g.tech.cargo)}</dd>
</div> </div>
<div class="field" data-testid="inspector-ship-group-field-mass"> <div class="field" data-testid="inspector-ship-group-field-mass">
<dt>{i18n.t("game.inspector.ship_group.field.mass")}</dt> <dt>{i18n.t("game.inspector.ship_group.field.mass")}</dt>
<dd>{formatNumber(g.mass)}</dd> <dd class="numeric">{formatFloat(g.mass)}</dd>
</div> </div>
<div class="field" data-testid="inspector-ship-group-field-cargo-load"> <div class="field" data-testid="inspector-ship-group-field-cargo-load">
<dt>{i18n.t("game.inspector.ship_group.field.cargo_load")}</dt> <dt>{i18n.t("game.inspector.ship_group.field.cargo_load")}</dt>
@@ -160,7 +157,7 @@ variant — for Phase 19 the inspector is intentionally read-only.
{#if g.cargo === "NONE"} {#if g.cargo === "NONE"}
{cargoLabel(g.cargo)} {cargoLabel(g.cargo)}
{:else} {:else}
{cargoLabel(g.cargo)} × {formatNumber(g.load)} {cargoLabel(g.cargo)} × <span class="numeric">{formatFloat(g.load)}</span>
{/if} {/if}
</dd> </dd>
</div> </div>
@@ -181,7 +178,7 @@ variant — for Phase 19 the inspector is intentionally read-only.
</div> </div>
<div class="field" data-testid="inspector-ship-group-field-distance"> <div class="field" data-testid="inspector-ship-group-field-distance">
<dt>{i18n.t("game.inspector.ship_group.field.distance")}</dt> <dt>{i18n.t("game.inspector.ship_group.field.distance")}</dt>
<dd>{formatNumber(g.range!)}</dd> <dd class="numeric">{formatFloat(g.range!)}</dd>
</div> </div>
{/if} {/if}
@@ -212,23 +209,25 @@ variant — for Phase 19 the inspector is intentionally read-only.
</div> </div>
<div class="field" data-testid="inspector-ship-group-field-distance"> <div class="field" data-testid="inspector-ship-group-field-distance">
<dt>{i18n.t("game.inspector.ship_group.field.distance")}</dt> <dt>{i18n.t("game.inspector.ship_group.field.distance")}</dt>
<dd>{formatNumber(g.distance)}</dd> <dd class="numeric">{formatFloat(g.distance)}</dd>
</div> </div>
<div class="field" data-testid="inspector-ship-group-field-speed"> <div class="field" data-testid="inspector-ship-group-field-speed">
<dt>{i18n.t("game.inspector.ship_group.field.speed")}</dt> <dt>{i18n.t("game.inspector.ship_group.field.speed")}</dt>
<dd>{formatNumber(g.speed)}</dd> <dd class="numeric">{formatFloat(g.speed)}</dd>
</div> </div>
<div class="field" data-testid="inspector-ship-group-field-eta"> <div class="field" data-testid="inspector-ship-group-field-eta">
<dt>{i18n.t("game.inspector.ship_group.field.eta")}</dt> <dt>{i18n.t("game.inspector.ship_group.field.eta")}</dt>
<dd> <dd>
{eta === null {#if eta === null}
? i18n.t("game.designer.ship_class.preview.unavailable") {i18n.t("game.designer.ship_class.preview.unavailable")}
: eta} {:else}
<span class="numeric">{eta}</span>
{/if}
</dd> </dd>
</div> </div>
<div class="field" data-testid="inspector-ship-group-field-mass"> <div class="field" data-testid="inspector-ship-group-field-mass">
<dt>{i18n.t("game.inspector.ship_group.field.mass")}</dt> <dt>{i18n.t("game.inspector.ship_group.field.mass")}</dt>
<dd>{formatNumber(g.mass)}</dd> <dd class="numeric">{formatFloat(g.mass)}</dd>
</div> </div>
</dl> </dl>
{:else} {:else}
@@ -238,8 +237,8 @@ variant — for Phase 19 the inspector is intentionally read-only.
data-testid="inspector-ship-group-field-coordinates" data-testid="inspector-ship-group-field-coordinates"
> >
<dt>{i18n.t("game.inspector.ship_group.field.coordinates")}</dt> <dt>{i18n.t("game.inspector.ship_group.field.coordinates")}</dt>
<dd> <dd class="numeric">
({formatNumber(selection.group.x)}, {formatNumber(selection.group.y)}) {formatFloat(selection.group.x)}, {formatFloat(selection.group.y)}
</dd> </dd>
</div> </div>
</dl> </dl>
@@ -285,16 +284,20 @@ variant — for Phase 19 the inspector is intentionally read-only.
} }
.field dt { .field dt {
color: var(--color-text-muted); color: var(--color-text-muted);
font-size: 0.85rem; font-size: 0.8rem;
} }
.field dd { .field dd {
margin: 0; margin: 0;
font-variant-numeric: tabular-nums; font-variant-numeric: tabular-nums;
font-size: 0.9rem; font-size: 0.85rem;
}
.field dd.numeric,
.field dd .numeric {
font-family: var(--font-mono);
} }
.hint { .hint {
margin: 0; margin: 0;
color: var(--color-text-muted); color: var(--color-text-muted);
font-size: 0.85rem; font-size: 0.8rem;
} }
</style> </style>
@@ -42,6 +42,7 @@ modernize cost preview backed by `core.blockUpgradeCost`.
type ShipGroupUpgradeTech, type ShipGroupUpgradeTech,
} from "../../../sync/order-types"; } from "../../../sync/order-types";
import { validateEntityName } from "$lib/util/entity-name"; import { validateEntityName } from "$lib/util/entity-name";
import { formatFloat } from "$lib/util/number-format";
type Props = { type Props = {
group: ReportLocalShipGroup; group: ReportLocalShipGroup;
@@ -675,9 +676,6 @@ modernize cost preview backed by `core.blockUpgradeCost`.
CARGO: "game.inspector.ship_group.action.tech.cargo", CARGO: "game.inspector.ship_group.action.tech.cargo",
}; };
function formatNumber(value: number): string {
return value.toLocaleString(undefined, { maximumFractionDigits: 2 });
}
</script> </script>
<section class="actions" data-testid="inspector-ship-group-actions"> <section class="actions" data-testid="inspector-ship-group-actions">
@@ -973,7 +971,7 @@ modernize cost preview backed by `core.blockUpgradeCost`.
{i18n.t("game.inspector.ship_group.action.modernize.cost_unavailable")} {i18n.t("game.inspector.ship_group.action.modernize.cost_unavailable")}
{:else} {:else}
{i18n.t("game.inspector.ship_group.action.modernize.cost", { {i18n.t("game.inspector.ship_group.action.modernize.cost", {
cost: formatNumber(modernizeCostPreview), cost: formatFloat(modernizeCostPreview),
})} })}
{/if} {/if}
</p> </p>
@@ -1148,7 +1146,7 @@ modernize cost preview backed by `core.blockUpgradeCost`.
} }
.action { .action {
font: inherit; font: inherit;
font-size: 0.85rem; font-size: 0.8rem;
padding: 0.2rem 0.55rem; padding: 0.2rem 0.55rem;
background: transparent; background: transparent;
color: var(--color-text-muted); color: var(--color-text-muted);
@@ -1177,7 +1175,7 @@ modernize cost preview backed by `core.blockUpgradeCost`.
display: flex; display: flex;
flex-direction: column; flex-direction: column;
gap: 0.2rem; gap: 0.2rem;
font-size: 0.85rem; font-size: 0.8rem;
color: var(--color-text-muted); color: var(--color-text-muted);
} }
.form input[type="number"], .form input[type="number"],
@@ -1196,7 +1194,7 @@ modernize cost preview backed by `core.blockUpgradeCost`.
flex-wrap: wrap; flex-wrap: wrap;
align-items: baseline; align-items: baseline;
gap: 0.4rem; gap: 0.4rem;
font-size: 0.85rem; font-size: 0.8rem;
} }
.form .destination-readonly .label { .form .destination-readonly .label {
color: var(--color-text-muted); color: var(--color-text-muted);
@@ -1208,7 +1206,7 @@ modernize cost preview backed by `core.blockUpgradeCost`.
} }
.form-actions button { .form-actions button {
font: inherit; font: inherit;
font-size: 0.85rem; font-size: 0.8rem;
padding: 0.25rem 0.65rem; padding: 0.25rem 0.65rem;
background: transparent; background: transparent;
color: var(--color-text-muted); color: var(--color-text-muted);
@@ -1226,18 +1224,18 @@ modernize cost preview backed by `core.blockUpgradeCost`.
} }
.preview { .preview {
margin: 0; margin: 0;
font-size: 0.85rem; font-size: 0.8rem;
color: var(--color-text-muted); color: var(--color-text-muted);
} }
.warning { .warning {
margin: 0; margin: 0;
font-size: 0.85rem; font-size: 0.8rem;
color: var(--color-warning); color: var(--color-warning);
} }
.locked { .locked {
margin: 0; margin: 0;
padding: 0.4rem 0.55rem; padding: 0.4rem 0.55rem;
font-size: 0.85rem; font-size: 0.8rem;
color: var(--color-text-muted); color: var(--color-text-muted);
background: var(--color-surface-overlay); background: var(--color-surface-overlay);
border: 1px solid var(--color-border); border: 1px solid var(--color-border);
+40
View File
@@ -0,0 +1,40 @@
// Shared numeric formatters for inspector and report views.
//
// Engine wire-format for floats is `Fixed3` quantised
// (pkg/model/report/report.go). The UI renders them as a fixed 3-decimal
// string with neither thousand separators nor locale-aware grouping —
// values stay column-aligned across rows and never reflow when the locale
// switches mid-session. Integer counts (planet count, ship `count`) are
// rendered with zero fractional digits to match their wire shape
// (uint16 / uint64). The convention mirrors the calculator-tab view
// (`useGrouping: false`, 3 decimals) so inspector and report tables read
// the same way as the calculator panel.
/**
* formatFloat renders an engine-emitted `Float` with three fractional
* digits and no thousand separators. Used for tech levels, stockpiles,
* coordinates, range, mass, votes — every report payload that is not
* an integer count or a `[0, 1]` fraction.
*/
export function formatFloat(value: number): string {
return value.toFixed(3);
}
/**
* formatInt renders an integer-ish count (planet count, ship count,
* etc.) with zero fractional digits and no thousand separators.
*/
export function formatInt(value: number): string {
return value.toFixed(0);
}
/**
* formatPercent renders a `[0, 1]` fraction as a one-decimal percent
* without a `%` suffix — the column header carries the unit. 0.1 %
* precision (the third decimal of the underlying float) matches the
* project-wide "three decimal digits" convention used elsewhere by
* `formatFloat`.
*/
export function formatPercent(fraction: number): string {
return (fraction * 100).toFixed(1);
}
+2 -2
View File
@@ -87,7 +87,7 @@ describe("planet inspector", () => {
); );
expect( expect(
ui.getByTestId("inspector-planet-field-coordinates"), ui.getByTestId("inspector-planet-field-coordinates"),
).toHaveTextContent("(100.25, 200)"); ).toHaveTextContent("100.250, 200.000");
expect(ui.getByTestId("inspector-planet-field-size")).toHaveTextContent( expect(ui.getByTestId("inspector-planet-field-size")).toHaveTextContent(
"size", "size",
); );
@@ -240,7 +240,7 @@ describe("planet inspector", () => {
); );
expect( expect(
ui.getByTestId("inspector-planet-field-coordinates"), ui.getByTestId("inspector-planet-field-coordinates"),
).toHaveTextContent("(1,234, -5)"); ).toHaveTextContent("1234.000, -5.000");
expect(ui.queryByTestId("inspector-planet-field-size")).toBeNull(); expect(ui.queryByTestId("inspector-planet-field-size")).toBeNull();
expect(ui.queryByTestId("inspector-planet-field-natural_resources")).toBeNull(); expect(ui.queryByTestId("inspector-planet-field-natural_resources")).toBeNull();
}); });
+9 -11
View File
@@ -175,18 +175,16 @@ describe("races table", () => {
expect(rows).toHaveLength(1); expect(rows).toHaveLength(1);
expect(rows[0]).toHaveAttribute("data-name", "Andori"); expect(rows[0]).toHaveAttribute("data-name", "Andori");
expect(ui.getByTestId("races-cell-name")).toHaveTextContent("Andori"); expect(ui.getByTestId("races-cell-name")).toHaveTextContent("Andori");
expect(ui.getByTestId("races-cell-drive")).toHaveTextContent("25"); // drive/weapons/shields/cargo are tech LEVELS from the engine
expect(ui.getByTestId("races-cell-weapons")).toHaveTextContent("50"); // (see F8-08 bugfix); rendered as 3-decimal Floats, not percents.
expect(ui.getByTestId("races-cell-shields")).toHaveTextContent("75"); expect(ui.getByTestId("races-cell-drive")).toHaveTextContent("0.250");
expect(ui.getByTestId("races-cell-cargo")).toHaveTextContent("100"); expect(ui.getByTestId("races-cell-weapons")).toHaveTextContent("0.500");
expect(ui.getByTestId("races-cell-population")).toHaveTextContent( expect(ui.getByTestId("races-cell-shields")).toHaveTextContent("0.750");
/12[,\s]345/, expect(ui.getByTestId("races-cell-cargo")).toHaveTextContent("1.000");
); expect(ui.getByTestId("races-cell-population")).toHaveTextContent("12345");
expect(ui.getByTestId("races-cell-industry")).toHaveTextContent( expect(ui.getByTestId("races-cell-industry")).toHaveTextContent("6789");
/6[,\s]?789/,
);
expect(ui.getByTestId("races-cell-planets")).toHaveTextContent("4"); expect(ui.getByTestId("races-cell-planets")).toHaveTextContent("4");
expect(ui.getByTestId("races-cell-votes")).toHaveTextContent("3.5"); expect(ui.getByTestId("races-cell-votes")).toHaveTextContent("3.500");
}); });
test("filters rows by case-insensitive name match", async () => { test("filters rows by case-insensitive name match", async () => {