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,
type ScienceInvalidReason,
} from "$lib/util/science-validation";
import { formatPercent } from "$lib/util/number-format";
const rendered = getContext<RenderedReportSource | undefined>(
RENDERED_REPORT_CONTEXT_KEY,
@@ -106,12 +107,7 @@ fractions is a Phase 21 decision documented in
const canSave = $derived(validation.ok && draft !== undefined);
const sumPercent = $derived(drive + weapons + shields + cargo);
const sumDisplay = $derived(
sumPercent.toLocaleString(undefined, {
minimumFractionDigits: 0,
maximumFractionDigits: 1,
}),
);
const sumDisplay = $derived(sumPercent.toFixed(1));
$effect(() => {
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 {
activeView.select("table", { tableEntity: "sciences" });
}
@@ -1,61 +1,44 @@
// Shared number / planet formatters for the Phase 23 Report View
// sections. Inlined in 10+ components, so factoring keeps each
// section component focused on its data shape. The formatters
// match the conventions of the per-entity tables (tabular numerals,
// 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.
// Number formatters and lookup helpers reused across the report-section
// components. The numeric formatters delegate to the project-wide
// `lib/util/number-format` so inspector, report tables, and the
// calculator panel all render numbers identically.
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
* percent (without a `%` suffix — the column header carries the
* unit). Matches the convention used by `table-races.svelte` and
* `table-sciences.svelte`.
* formatPercent renders a `[0, 1]` fraction as a one-decimal percent
* (without a `%` suffix — the column header carries the unit).
* Re-exported from the shared util for backwards-compatible imports
* across the report sections.
*/
export function formatPercent(fraction: number): string {
return (fraction * 100).toLocaleString(undefined, {
minimumFractionDigits: 0,
maximumFractionDigits: 1,
});
}
export const formatPercent = formatPercentBase;
/**
* formatCount renders an integer-ish value (population, industry,
* planet count, …) without fractional digits and with locale-aware
* thousand separators.
* planet count, …) with zero fractional digits and no thousand
* separators. Alias of the shared `formatInt`.
*/
export function formatCount(value: number): string {
return value.toLocaleString(undefined, {
minimumFractionDigits: 0,
maximumFractionDigits: 0,
});
}
export const formatCount = formatInt;
/**
* formatFloat renders a floating-point value with up to two
* fractional digits. Used for stockpiles, distances, cost, mass —
* everything the engine emits as a `Float` that is not a fraction.
* formatFloat renders an engine `Float` (Fixed3-quantised) with three
* fractional digits and no thousand separators. Used for stockpiles,
* 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 {
return value.toLocaleString(undefined, {
minimumFractionDigits: 0,
maximumFractionDigits: 2,
});
}
export const formatFloat = formatFloatBase;
/**
* formatVotes renders a vote weight with up to two decimal digits —
* mirrors the races table's column convention so the cumulative
* vote totals line up across views.
* formatVotes renders a vote weight. Votes travel as the same `Float`
* shape as every other float field, so this is a semantic alias of
* `formatFloat` kept for readability at the call site.
*/
export function formatVotes(value: number): string {
return value.toLocaleString(undefined, {
minimumFractionDigits: 0,
maximumFractionDigits: 2,
});
}
export const formatVotes = formatFloatBase;
/**
* planetLabel renders a planet reference as `#<number> (<name>)` if
@@ -42,11 +42,11 @@ class when the group lands and a battle roster forms.
<tr>
<th>{i18n.t("game.report.section.approaching_groups.column.from")}</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")}
</th>
<th>{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.speed")}</th>
<th class="numeric">{i18n.t("game.report.section.approaching_groups.column.mass")}</th>
</tr>
</thead>
<tbody>
@@ -54,9 +54,9 @@ class when the group lands and a battle roster forms.
<tr data-testid="approaching-groups-row">
<td>{planetLabel(r.origin, planets)}</td>
<td>{planetLabel(r.destination, planets)}</td>
<td>{formatFloat(r.distance)}</td>
<td>{formatFloat(r.speed)}</td>
<td>{formatFloat(r.mass)}</td>
<td class="numeric">{formatFloat(r.distance)}</td>
<td class="numeric">{formatFloat(r.speed)}</td>
<td class="numeric">{formatFloat(r.mass)}</td>
</tr>
{/each}
</tbody>
@@ -79,7 +79,7 @@ class when the group lands and a battle roster forms.
border-collapse: collapse;
width: 100%;
font-variant-numeric: tabular-nums;
font-size: 0.9rem;
font-size: 0.85rem;
}
.grid th,
.grid td {
@@ -93,6 +93,11 @@ class when the group lands and a battle roster forms.
letter-spacing: 0.04em;
font-size: 0.75rem;
}
.grid th.numeric,
.grid td.numeric {
font-family: var(--font-mono);
text-align: right;
}
.grid tbody tr:hover {
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.attacker")}</th>
<th>{i18n.t("game.report.section.bombings.column.production")}</th>
<th>{i18n.t("game.report.section.bombings.column.industry")}</th>
<th>{i18n.t("game.report.section.bombings.column.population")}</th>
<th>{i18n.t("game.report.section.bombings.column.colonists")}</th>
<th>
<th class="numeric">{i18n.t("game.report.section.bombings.column.industry")}</th>
<th class="numeric">{i18n.t("game.report.section.bombings.column.population")}</th>
<th class="numeric">{i18n.t("game.report.section.bombings.column.colonists")}</th>
<th class="numeric">
{i18n.t("game.report.section.bombings.column.industry_stockpile")}
</th>
<th>
<th class="numeric">
{i18n.t("game.report.section.bombings.column.materials_stockpile")}
</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>
</tr>
</thead>
@@ -67,12 +67,12 @@ Decoder sorts by `planetNumber` already.
<td>{b.owner}</td>
<td>{b.attacker}</td>
<td>{b.production}</td>
<td>{formatFloat(b.industry)}</td>
<td>{formatFloat(b.population)}</td>
<td>{formatFloat(b.colonists)}</td>
<td>{formatFloat(b.industryStockpile)}</td>
<td>{formatFloat(b.materialsStockpile)}</td>
<td>{formatCount(b.attackPower)}</td>
<td class="numeric">{formatFloat(b.industry)}</td>
<td class="numeric">{formatFloat(b.population)}</td>
<td class="numeric">{formatFloat(b.colonists)}</td>
<td class="numeric">{formatFloat(b.industryStockpile)}</td>
<td class="numeric">{formatFloat(b.materialsStockpile)}</td>
<td class="numeric">{formatCount(b.attackPower)}</td>
<td>
{#if b.wiped}
<span
@@ -105,7 +105,7 @@ Decoder sorts by `planetNumber` already.
border-collapse: collapse;
width: 100%;
font-variant-numeric: tabular-nums;
font-size: 0.9rem;
font-size: 0.85rem;
}
.grid th,
.grid td {
@@ -119,6 +119,11 @@ Decoder sorts by `planetNumber` already.
letter-spacing: 0.04em;
font-size: 0.75rem;
}
.grid th.numeric,
.grid td.numeric {
font-family: var(--font-mono);
text-align: right;
}
.grid tbody tr:hover {
background: var(--color-surface);
}
@@ -94,7 +94,7 @@ has many routes.
border-collapse: collapse;
width: 100%;
font-variant-numeric: tabular-nums;
font-size: 0.9rem;
font-size: 0.85rem;
}
.grid th,
.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.name")}</th>
<th>{i18n.t("game.report.section.foreign_planets.column.owner")}</th>
<th>{i18n.t("game.report.section.my_planets.column.coordinates")}</th>
<th>{i18n.t("game.report.section.my_planets.column.size")}</th>
<th>{i18n.t("game.report.section.my_planets.column.resources")}</th>
<th>{i18n.t("game.report.section.my_planets.column.population")}</th>
<th>{i18n.t("game.report.section.my_planets.column.industry")}</th>
<th>
<th class="numeric">{i18n.t("game.report.section.my_planets.column.coordinates")}</th>
<th class="numeric">{i18n.t("game.report.section.my_planets.column.size")}</th>
<th class="numeric">{i18n.t("game.report.section.my_planets.column.resources")}</th>
<th class="numeric">{i18n.t("game.report.section.my_planets.column.population")}</th>
<th class="numeric">{i18n.t("game.report.section.my_planets.column.industry")}</th>
<th class="numeric">
{i18n.t("game.report.section.my_planets.column.industry_stockpile")}
</th>
<th>
<th class="numeric">
{i18n.t("game.report.section.my_planets.column.materials_stockpile")}
</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.free_industry")}</th>
<th class="numeric">{i18n.t("game.report.section.my_planets.column.free_industry")}</th>
</tr>
</thead>
<tbody>
@@ -64,16 +64,16 @@ as the local planets table plus an `owner` column.
<td>{p.number}</td>
<td>{p.name}</td>
<td>{p.owner ?? ""}</td>
<td>{formatFloat(p.x)}, {formatFloat(p.y)}</td>
<td>{formatFloat(p.size ?? 0)}</td>
<td>{formatFloat(p.resources ?? 0)}</td>
<td>{formatFloat(p.population ?? 0)}</td>
<td>{formatFloat(p.industry ?? 0)}</td>
<td>{formatFloat(p.industryStockpile ?? 0)}</td>
<td>{formatFloat(p.materialsStockpile ?? 0)}</td>
<td>{formatFloat(p.colonists ?? 0)}</td>
<td class="numeric">{formatFloat(p.x)}, {formatFloat(p.y)}</td>
<td class="numeric">{formatFloat(p.size ?? 0)}</td>
<td class="numeric">{formatFloat(p.resources ?? 0)}</td>
<td class="numeric">{formatFloat(p.population ?? 0)}</td>
<td class="numeric">{formatFloat(p.industry ?? 0)}</td>
<td class="numeric">{formatFloat(p.industryStockpile ?? 0)}</td>
<td class="numeric">{formatFloat(p.materialsStockpile ?? 0)}</td>
<td class="numeric">{formatFloat(p.colonists ?? 0)}</td>
<td>{p.production ?? "—"}</td>
<td>{formatFloat(p.freeIndustry ?? 0)}</td>
<td class="numeric">{formatFloat(p.freeIndustry ?? 0)}</td>
</tr>
{/each}
</tbody>
@@ -96,7 +96,7 @@ as the local planets table plus an `owner` column.
border-collapse: collapse;
width: 100%;
font-variant-numeric: tabular-nums;
font-size: 0.9rem;
font-size: 0.85rem;
}
.grid th,
.grid td {
@@ -110,6 +110,11 @@ as the local planets table plus an `owner` column.
letter-spacing: 0.04em;
font-size: 0.75rem;
}
.grid th.numeric,
.grid td.numeric {
font-family: var(--font-mono);
text-align: right;
}
.grid tbody tr:hover {
background: var(--color-surface);
}
@@ -67,10 +67,10 @@ unit even when the section spans many races.
<thead>
<tr>
<th>{i18n.t("game.report.section.my_sciences.column.name")}</th>
<th>{i18n.t("game.report.section.my_sciences.column.drive")}</th>
<th>{i18n.t("game.report.section.my_sciences.column.weapons")}</th>
<th>{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.drive")}</th>
<th class="numeric">{i18n.t("game.report.section.my_sciences.column.weapons")}</th>
<th class="numeric">{i18n.t("game.report.section.my_sciences.column.shields")}</th>
<th class="numeric">{i18n.t("game.report.section.my_sciences.column.cargo")}</th>
</tr>
</thead>
<tbody>
@@ -81,10 +81,10 @@ unit even when the section spans many races.
data-name={r.name}
>
<td>{r.name}</td>
<td>{formatPercent(r.drive)}</td>
<td>{formatPercent(r.weapons)}</td>
<td>{formatPercent(r.shields)}</td>
<td>{formatPercent(r.cargo)}</td>
<td class="numeric">{formatPercent(r.drive)}</td>
<td class="numeric">{formatPercent(r.weapons)}</td>
<td class="numeric">{formatPercent(r.shields)}</td>
<td class="numeric">{formatPercent(r.cargo)}</td>
</tr>
{/each}
</tbody>
@@ -115,7 +115,7 @@ unit even when the section spans many races.
border-collapse: collapse;
width: 100%;
font-variant-numeric: tabular-nums;
font-size: 0.9rem;
font-size: 0.85rem;
}
.grid th,
.grid td {
@@ -129,6 +129,11 @@ unit even when the section spans many races.
letter-spacing: 0.04em;
font-size: 0.75rem;
}
.grid th.numeric,
.grid td.numeric {
font-family: var(--font-mono);
text-align: right;
}
.grid tbody tr:hover {
background: var(--color-surface);
}
@@ -65,12 +65,12 @@ incoming groups.
<thead>
<tr>
<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.weapons")}</th>
<th>{i18n.t("game.report.section.my_ship_classes.column.shields")}</th>
<th>{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.my_ship_classes.column.weapons")}</th>
<th class="numeric">{i18n.t("game.report.section.my_ship_classes.column.shields")}</th>
<th class="numeric">{i18n.t("game.report.section.my_ship_classes.column.cargo")}</th>
<th class="numeric">{i18n.t("game.report.section.foreign_ship_classes.column.mass")}</th>
</tr>
</thead>
<tbody>
@@ -81,12 +81,12 @@ incoming groups.
data-name={r.name}
>
<td>{r.name}</td>
<td>{formatFloat(r.drive)}</td>
<td class="numeric">{formatFloat(r.drive)}</td>
<td>{r.armament}</td>
<td>{formatFloat(r.weapons)}</td>
<td>{formatFloat(r.shields)}</td>
<td>{formatFloat(r.cargo)}</td>
<td>{formatFloat(r.mass)}</td>
<td class="numeric">{formatFloat(r.weapons)}</td>
<td class="numeric">{formatFloat(r.shields)}</td>
<td class="numeric">{formatFloat(r.cargo)}</td>
<td class="numeric">{formatFloat(r.mass)}</td>
</tr>
{/each}
</tbody>
@@ -117,7 +117,7 @@ incoming groups.
border-collapse: collapse;
width: 100%;
font-variant-numeric: tabular-nums;
font-size: 0.9rem;
font-size: 0.85rem;
}
.grid th,
.grid td {
@@ -131,6 +131,11 @@ incoming groups.
letter-spacing: 0.04em;
font-size: 0.75rem;
}
.grid th.numeric,
.grid td.numeric {
font-family: var(--font-mono);
text-align: right;
}
.grid tbody tr:hover {
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.origin")}</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>{i18n.t("game.report.section.my_ship_groups.column.mass")}</th>
<th class="numeric">{i18n.t("game.report.section.my_ship_groups.column.speed")}</th>
<th class="numeric">{i18n.t("game.report.section.my_ship_groups.column.mass")}</th>
</tr>
</thead>
<tbody>
@@ -64,8 +64,8 @@ to groups the player doesn't own.
{g.origin === null ? "—" : planetLabel(g.origin, planets)}
</td>
<td>{g.range === null ? "—" : formatFloat(g.range)}</td>
<td>{formatFloat(g.speed)}</td>
<td>{formatFloat(g.mass)}</td>
<td class="numeric">{formatFloat(g.speed)}</td>
<td class="numeric">{formatFloat(g.mass)}</td>
</tr>
{/each}
</tbody>
@@ -88,7 +88,7 @@ to groups the player doesn't own.
border-collapse: collapse;
width: 100%;
font-variant-numeric: tabular-nums;
font-size: 0.9rem;
font-size: 0.85rem;
}
.grid th,
.grid td {
@@ -102,6 +102,11 @@ to groups the player doesn't own.
letter-spacing: 0.04em;
font-size: 0.75rem;
}
.grid th.numeric,
.grid td.numeric {
font-family: var(--font-mono);
text-align: right;
}
.grid tbody tr:hover {
background: var(--color-surface);
}
@@ -31,13 +31,13 @@ section is never empty as long as the report has loaded.
{:else}
<dl class="kv">
<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>
<dd data-testid="galaxy-summary-field-size">
<dd class="numeric" data-testid="galaxy-summary-field-size">
{report.mapWidth} × {report.mapHeight}
</dd>
<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>
<dd data-testid="galaxy-summary-field-race">{report.race}</dd>
</dl>
@@ -60,7 +60,7 @@ section is never empty as long as the report has loaded.
grid-template-columns: max-content 1fr;
gap: 0.3rem 1rem;
margin: 0;
font-size: 0.9rem;
font-size: 0.85rem;
}
.kv dt {
color: var(--color-text-muted);
@@ -73,4 +73,7 @@ section is never empty as long as the report has loaded.
color: var(--color-text);
font-variant-numeric: tabular-nums;
}
.kv dd.numeric {
font-family: var(--font-mono);
}
</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.origin")}</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>
</thead>
<tbody>
@@ -58,7 +58,7 @@ in orbit has neither); empty cells in those columns are normal.
{f.origin === null ? "—" : planetLabel(f.origin, planets)}
</td>
<td>{f.range === null ? "—" : formatFloat(f.range)}</td>
<td>{formatFloat(f.speed)}</td>
<td class="numeric">{formatFloat(f.speed)}</td>
</tr>
{/each}
</tbody>
@@ -81,7 +81,7 @@ in orbit has neither); empty cells in those columns are normal.
border-collapse: collapse;
width: 100%;
font-variant-numeric: tabular-nums;
font-size: 0.9rem;
font-size: 0.85rem;
}
.grid th,
.grid td {
@@ -95,6 +95,11 @@ in orbit has neither); empty cells in those columns are normal.
letter-spacing: 0.04em;
font-size: 0.75rem;
}
.grid th.numeric,
.grid td.numeric {
font-family: var(--font-mono);
text-align: right;
}
.grid tbody tr:hover {
background: var(--color-surface);
}
@@ -41,20 +41,20 @@ column set (matches `ReportPlanet` shape).
<tr>
<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.coordinates")}</th>
<th>{i18n.t("game.report.section.my_planets.column.size")}</th>
<th>{i18n.t("game.report.section.my_planets.column.resources")}</th>
<th>{i18n.t("game.report.section.my_planets.column.population")}</th>
<th>{i18n.t("game.report.section.my_planets.column.industry")}</th>
<th>
<th class="numeric">{i18n.t("game.report.section.my_planets.column.coordinates")}</th>
<th class="numeric">{i18n.t("game.report.section.my_planets.column.size")}</th>
<th class="numeric">{i18n.t("game.report.section.my_planets.column.resources")}</th>
<th class="numeric">{i18n.t("game.report.section.my_planets.column.population")}</th>
<th class="numeric">{i18n.t("game.report.section.my_planets.column.industry")}</th>
<th class="numeric">
{i18n.t("game.report.section.my_planets.column.industry_stockpile")}
</th>
<th>
<th class="numeric">
{i18n.t("game.report.section.my_planets.column.materials_stockpile")}
</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.free_industry")}</th>
<th class="numeric">{i18n.t("game.report.section.my_planets.column.free_industry")}</th>
</tr>
</thead>
<tbody>
@@ -62,16 +62,16 @@ column set (matches `ReportPlanet` shape).
<tr data-testid="my-planets-row" data-number={p.number}>
<td>{p.number}</td>
<td>{p.name}</td>
<td>{formatFloat(p.x)}, {formatFloat(p.y)}</td>
<td>{formatFloat(p.size ?? 0)}</td>
<td>{formatFloat(p.resources ?? 0)}</td>
<td>{formatFloat(p.population ?? 0)}</td>
<td>{formatFloat(p.industry ?? 0)}</td>
<td>{formatFloat(p.industryStockpile ?? 0)}</td>
<td>{formatFloat(p.materialsStockpile ?? 0)}</td>
<td>{formatFloat(p.colonists ?? 0)}</td>
<td class="numeric">{formatFloat(p.x)}, {formatFloat(p.y)}</td>
<td class="numeric">{formatFloat(p.size ?? 0)}</td>
<td class="numeric">{formatFloat(p.resources ?? 0)}</td>
<td class="numeric">{formatFloat(p.population ?? 0)}</td>
<td class="numeric">{formatFloat(p.industry ?? 0)}</td>
<td class="numeric">{formatFloat(p.industryStockpile ?? 0)}</td>
<td class="numeric">{formatFloat(p.materialsStockpile ?? 0)}</td>
<td class="numeric">{formatFloat(p.colonists ?? 0)}</td>
<td>{p.production ?? "—"}</td>
<td>{formatFloat(p.freeIndustry ?? 0)}</td>
<td class="numeric">{formatFloat(p.freeIndustry ?? 0)}</td>
</tr>
{/each}
</tbody>
@@ -94,7 +94,7 @@ column set (matches `ReportPlanet` shape).
border-collapse: collapse;
width: 100%;
font-variant-numeric: tabular-nums;
font-size: 0.9rem;
font-size: 0.85rem;
}
.grid th,
.grid td {
@@ -108,6 +108,11 @@ column set (matches `ReportPlanet` shape).
letter-spacing: 0.04em;
font-size: 0.75rem;
}
.grid th.numeric,
.grid td.numeric {
font-family: var(--font-mono);
text-align: right;
}
.grid tbody tr:hover {
background: var(--color-surface);
}
@@ -39,20 +39,20 @@ table).
<thead>
<tr>
<th>{i18n.t("game.report.section.my_sciences.column.name")}</th>
<th>{i18n.t("game.report.section.my_sciences.column.drive")}</th>
<th>{i18n.t("game.report.section.my_sciences.column.weapons")}</th>
<th>{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.drive")}</th>
<th class="numeric">{i18n.t("game.report.section.my_sciences.column.weapons")}</th>
<th class="numeric">{i18n.t("game.report.section.my_sciences.column.shields")}</th>
<th class="numeric">{i18n.t("game.report.section.my_sciences.column.cargo")}</th>
</tr>
</thead>
<tbody>
{#each rows as r (r.name)}
<tr data-testid="my-sciences-row" data-name={r.name}>
<td>{r.name}</td>
<td>{formatPercent(r.drive)}</td>
<td>{formatPercent(r.weapons)}</td>
<td>{formatPercent(r.shields)}</td>
<td>{formatPercent(r.cargo)}</td>
<td class="numeric">{formatPercent(r.drive)}</td>
<td class="numeric">{formatPercent(r.weapons)}</td>
<td class="numeric">{formatPercent(r.shields)}</td>
<td class="numeric">{formatPercent(r.cargo)}</td>
</tr>
{/each}
</tbody>
@@ -75,7 +75,7 @@ table).
border-collapse: collapse;
width: 100%;
font-variant-numeric: tabular-nums;
font-size: 0.9rem;
font-size: 0.85rem;
}
.grid th,
.grid td {
@@ -89,6 +89,11 @@ table).
letter-spacing: 0.04em;
font-size: 0.75rem;
}
.grid th.numeric,
.grid td.numeric {
font-family: var(--font-mono);
text-align: right;
}
.grid tbody tr:hover {
background: var(--color-surface);
}
@@ -40,22 +40,22 @@ drafts immediately, matching the ship-class designer's behaviour.
<thead>
<tr>
<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.weapons")}</th>
<th>{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.weapons")}</th>
<th class="numeric">{i18n.t("game.report.section.my_ship_classes.column.shields")}</th>
<th class="numeric">{i18n.t("game.report.section.my_ship_classes.column.cargo")}</th>
</tr>
</thead>
<tbody>
{#each rows as r (r.name)}
<tr data-testid="my-ship-classes-row" data-name={r.name}>
<td>{r.name}</td>
<td>{formatFloat(r.drive)}</td>
<td class="numeric">{formatFloat(r.drive)}</td>
<td>{r.armament}</td>
<td>{formatFloat(r.weapons)}</td>
<td>{formatFloat(r.shields)}</td>
<td>{formatFloat(r.cargo)}</td>
<td class="numeric">{formatFloat(r.weapons)}</td>
<td class="numeric">{formatFloat(r.shields)}</td>
<td class="numeric">{formatFloat(r.cargo)}</td>
</tr>
{/each}
</tbody>
@@ -78,7 +78,7 @@ drafts immediately, matching the ship-class designer's behaviour.
border-collapse: collapse;
width: 100%;
font-variant-numeric: tabular-nums;
font-size: 0.9rem;
font-size: 0.85rem;
}
.grid th,
.grid td {
@@ -92,6 +92,11 @@ drafts immediately, matching the ship-class designer's behaviour.
letter-spacing: 0.04em;
font-size: 0.75rem;
}
.grid th.numeric,
.grid td.numeric {
font-family: var(--font-mono);
text-align: right;
}
.grid tbody tr:hover {
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.origin")}</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>{i18n.t("game.report.section.my_ship_groups.column.mass")}</th>
<th class="numeric">{i18n.t("game.report.section.my_ship_groups.column.speed")}</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>
</tr>
</thead>
@@ -77,8 +77,8 @@ shown together with `load` when carrying.
{g.origin === null ? "—" : planetLabel(g.origin, planets)}
</td>
<td>{g.range === null ? "—" : formatFloat(g.range)}</td>
<td>{formatFloat(g.speed)}</td>
<td>{formatFloat(g.mass)}</td>
<td class="numeric">{formatFloat(g.speed)}</td>
<td class="numeric">{formatFloat(g.mass)}</td>
<td>{g.fleet ?? "—"}</td>
</tr>
{/each}
@@ -102,7 +102,7 @@ shown together with `load` when carrying.
border-collapse: collapse;
width: 100%;
font-variant-numeric: tabular-nums;
font-size: 0.9rem;
font-size: 0.85rem;
}
.grid th,
.grid td {
@@ -116,6 +116,11 @@ shown together with `load` when carrying.
letter-spacing: 0.04em;
font-size: 0.75rem;
}
.grid th.numeric,
.grid td.numeric {
font-family: var(--font-mono);
text-align: right;
}
.grid tbody tr:hover {
background: var(--color-surface);
}
@@ -14,7 +14,7 @@ highlight so the user can locate themselves quickly.
RENDERED_REPORT_CONTEXT_KEY,
type RenderedReportSource,
} from "$lib/rendered-report.svelte";
import { formatCount, formatPercent, formatVotes } from "./format";
import { formatCount, formatFloat, formatVotes } from "./format";
const rendered = getContext<RenderedReportSource | undefined>(
RENDERED_REPORT_CONTEXT_KEY,
@@ -37,14 +37,14 @@ highlight so the user can locate themselves quickly.
<thead>
<tr>
<th>{i18n.t("game.report.section.player_status.column.name")}</th>
<th>{i18n.t("game.report.section.player_status.column.drive")}</th>
<th>{i18n.t("game.report.section.player_status.column.weapons")}</th>
<th>{i18n.t("game.report.section.player_status.column.shields")}</th>
<th>{i18n.t("game.report.section.player_status.column.cargo")}</th>
<th>{i18n.t("game.report.section.player_status.column.population")}</th>
<th>{i18n.t("game.report.section.player_status.column.industry")}</th>
<th>{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.drive")}</th>
<th class="numeric">{i18n.t("game.report.section.player_status.column.weapons")}</th>
<th class="numeric">{i18n.t("game.report.section.player_status.column.shields")}</th>
<th class="numeric">{i18n.t("game.report.section.player_status.column.cargo")}</th>
<th class="numeric">{i18n.t("game.report.section.player_status.column.population")}</th>
<th class="numeric">{i18n.t("game.report.section.player_status.column.industry")}</th>
<th class="numeric">{i18n.t("game.report.section.player_status.column.planets")}</th>
<th class="numeric">{i18n.t("game.report.section.player_status.column.votes")}</th>
</tr>
</thead>
<tbody>
@@ -73,14 +73,14 @@ highlight so the user can locate themselves quickly.
</span>
{/if}
</td>
<td>{formatPercent(p.drive)}</td>
<td>{formatPercent(p.weapons)}</td>
<td>{formatPercent(p.shields)}</td>
<td>{formatPercent(p.cargo)}</td>
<td>{formatCount(p.population)}</td>
<td>{formatCount(p.industry)}</td>
<td>{formatCount(p.planets)}</td>
<td>{formatVotes(p.votesReceived)}</td>
<td class="numeric">{formatFloat(p.drive)}</td>
<td class="numeric">{formatFloat(p.weapons)}</td>
<td class="numeric">{formatFloat(p.shields)}</td>
<td class="numeric">{formatFloat(p.cargo)}</td>
<td class="numeric">{formatCount(p.population)}</td>
<td class="numeric">{formatCount(p.industry)}</td>
<td class="numeric">{formatCount(p.planets)}</td>
<td class="numeric">{formatVotes(p.votesReceived)}</td>
</tr>
{/each}
</tbody>
@@ -103,7 +103,7 @@ highlight so the user can locate themselves quickly.
border-collapse: collapse;
width: 100%;
font-variant-numeric: tabular-nums;
font-size: 0.9rem;
font-size: 0.85rem;
}
.grid th,
.grid td {
@@ -117,6 +117,11 @@ highlight so the user can locate themselves quickly.
letter-spacing: 0.04em;
font-size: 0.75rem;
}
.grid th.numeric,
.grid td.numeric {
font-family: var(--font-mono);
text-align: right;
}
.grid tbody tr:hover {
background: var(--color-surface);
}
@@ -41,12 +41,12 @@ reads `#17 (Castle)` rather than just `#17`.
<tr>
<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.cost")}</th>
<th>
<th class="numeric">{i18n.t("game.report.section.ships_in_production.column.cost")}</th>
<th class="numeric">
{i18n.t("game.report.section.ships_in_production.column.prod_used")}
</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>
</thead>
<tbody>
@@ -58,10 +58,10 @@ reads `#17 (Castle)` rather than just `#17`.
>
<td>{planetLabel(r.planetNumber, planets)}</td>
<td>{r.class}</td>
<td>{formatFloat(r.cost)}</td>
<td>{formatFloat(r.prodUsed)}</td>
<td class="numeric">{formatFloat(r.cost)}</td>
<td class="numeric">{formatFloat(r.prodUsed)}</td>
<td>{(r.percent * 100).toFixed(1)}</td>
<td>{formatFloat(r.freeIndustry)}</td>
<td class="numeric">{formatFloat(r.freeIndustry)}</td>
</tr>
{/each}
</tbody>
@@ -84,7 +84,7 @@ reads `#17 (Castle)` rather than just `#17`.
border-collapse: collapse;
width: 100%;
font-variant-numeric: tabular-nums;
font-size: 0.9rem;
font-size: 0.85rem;
}
.grid th,
.grid td {
@@ -98,6 +98,11 @@ reads `#17 (Castle)` rather than just `#17`.
letter-spacing: 0.04em;
font-size: 0.75rem;
}
.grid th.numeric,
.grid td.numeric {
font-family: var(--font-mono);
text-align: right;
}
.grid tbody tr:hover {
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">
<thead>
<tr>
<th>{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.x")}</th>
<th class="numeric">{i18n.t("game.report.section.unidentified_groups.column.y")}</th>
</tr>
</thead>
<tbody>
{#each rows as g, i (i)}
<tr data-testid="unidentified-groups-row">
<td>{formatFloat(g.x)}</td>
<td>{formatFloat(g.y)}</td>
<td class="numeric">{formatFloat(g.x)}</td>
<td class="numeric">{formatFloat(g.y)}</td>
</tr>
{/each}
</tbody>
@@ -68,7 +68,7 @@ radar that doesn't even resolve to a planet.
border-collapse: collapse;
width: 100%;
font-variant-numeric: tabular-nums;
font-size: 0.9rem;
font-size: 0.85rem;
}
.grid th,
.grid td {
@@ -82,6 +82,11 @@ radar that doesn't even resolve to a planet.
letter-spacing: 0.04em;
font-size: 0.75rem;
}
.grid th.numeric,
.grid td.numeric {
font-family: var(--font-mono);
text-align: right;
}
.grid tbody tr:hover {
background: var(--color-surface);
}
@@ -42,13 +42,13 @@ are intentionally omitted.
<tr>
<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.coordinates")}</th>
<th>{i18n.t("game.report.section.my_planets.column.size")}</th>
<th>{i18n.t("game.report.section.my_planets.column.resources")}</th>
<th>
<th class="numeric">{i18n.t("game.report.section.my_planets.column.coordinates")}</th>
<th class="numeric">{i18n.t("game.report.section.my_planets.column.size")}</th>
<th class="numeric">{i18n.t("game.report.section.my_planets.column.resources")}</th>
<th class="numeric">
{i18n.t("game.report.section.my_planets.column.industry_stockpile")}
</th>
<th>
<th class="numeric">
{i18n.t("game.report.section.my_planets.column.materials_stockpile")}
</th>
</tr>
@@ -58,11 +58,11 @@ are intentionally omitted.
<tr data-testid="uninhabited-planets-row" data-number={p.number}>
<td>{p.number}</td>
<td>{p.name}</td>
<td>{formatFloat(p.x)}, {formatFloat(p.y)}</td>
<td>{formatFloat(p.size ?? 0)}</td>
<td>{formatFloat(p.resources ?? 0)}</td>
<td>{formatFloat(p.industryStockpile ?? 0)}</td>
<td>{formatFloat(p.materialsStockpile ?? 0)}</td>
<td class="numeric">{formatFloat(p.x)}, {formatFloat(p.y)}</td>
<td class="numeric">{formatFloat(p.size ?? 0)}</td>
<td class="numeric">{formatFloat(p.resources ?? 0)}</td>
<td class="numeric">{formatFloat(p.industryStockpile ?? 0)}</td>
<td class="numeric">{formatFloat(p.materialsStockpile ?? 0)}</td>
</tr>
{/each}
</tbody>
@@ -85,7 +85,7 @@ are intentionally omitted.
border-collapse: collapse;
width: 100%;
font-variant-numeric: tabular-nums;
font-size: 0.9rem;
font-size: 0.85rem;
}
.grid th,
.grid td {
@@ -99,6 +99,11 @@ are intentionally omitted.
letter-spacing: 0.04em;
font-size: 0.75rem;
}
.grid th.numeric,
.grid td.numeric {
font-family: var(--font-mono);
text-align: right;
}
.grid tbody tr:hover {
background: var(--color-surface);
}
@@ -40,14 +40,14 @@ else is known.
<thead>
<tr>
<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>
</thead>
<tbody>
{#each rows as p (p.number)}
<tr data-testid="unknown-planets-row" data-number={p.number}>
<td>{p.number}</td>
<td>{formatFloat(p.x)}, {formatFloat(p.y)}</td>
<td class="numeric">{formatFloat(p.x)}, {formatFloat(p.y)}</td>
</tr>
{/each}
</tbody>
@@ -70,7 +70,7 @@ else is known.
border-collapse: collapse;
width: 100%;
font-variant-numeric: tabular-nums;
font-size: 0.9rem;
font-size: 0.85rem;
}
.grid th,
.grid td {
@@ -84,6 +84,11 @@ else is known.
letter-spacing: 0.04em;
font-size: 0.75rem;
}
.grid th.numeric,
.grid td.numeric {
font-family: var(--font-mono);
text-align: right;
}
.grid tbody tr:hover {
background: var(--color-surface);
}
@@ -58,14 +58,14 @@ explanatory text on the races table.
<thead>
<tr>
<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>
</thead>
<tbody>
{#each races as r (r.name)}
<tr data-testid="votes-received-row" data-race={r.name}>
<td>{r.name}</td>
<td>{formatVotes(r.votesReceived)}</td>
<td class="numeric">{formatVotes(r.votesReceived)}</td>
</tr>
{/each}
</tbody>
@@ -113,7 +113,7 @@ explanatory text on the races table.
.grid {
border-collapse: collapse;
font-variant-numeric: tabular-nums;
font-size: 0.9rem;
font-size: 0.85rem;
}
.grid th,
.grid td {
@@ -35,6 +35,10 @@ data fetching is performed here — the layout is responsible.
} from "../../sync/order-draft.svelte";
import type { Relation } from "../../sync/order-types";
import ViewState from "$lib/ui/view-state.svelte";
import {
formatFloat,
formatInt,
} from "$lib/util/number-format";
type SortColumn =
| "name"
@@ -122,31 +126,6 @@ data fetching is performed here — the layout is responsible.
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> {
if (draft === undefined) return;
// 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">
{i18n.t("game.table.races.votes.mine")}:
</span>
<span class="summary-value">{formatVotes(myVotes)}</span>
<span class="summary-value numeric">{formatFloat(myVotes)}</span>
</span>
<label class="summary-cell vote-picker">
<span class="summary-label">
@@ -244,7 +223,7 @@ data fetching is performed here — the layout is responsible.
<thead>
<tr>
{#each COLUMNS as column (column)}
<th aria-sort={ariaSort(column)}>
<th aria-sort={ariaSort(column)} class:numeric={column !== "name"}>
<button
type="button"
class="sort"
@@ -267,23 +246,29 @@ data fetching is performed here — the layout is responsible.
{#each sorted as r (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-drive">{formatPercent(r.drive)}</td>
<td data-testid="races-cell-weapons">
{formatPercent(r.weapons)}
<td class="numeric" data-testid="races-cell-drive">
{formatFloat(r.drive)}
</td>
<td data-testid="races-cell-shields">
{formatPercent(r.shields)}
<td class="numeric" data-testid="races-cell-weapons">
{formatFloat(r.weapons)}
</td>
<td data-testid="races-cell-cargo">{formatPercent(r.cargo)}</td>
<td data-testid="races-cell-population">
{formatCount(r.population)}
<td class="numeric" data-testid="races-cell-shields">
{formatFloat(r.shields)}
</td>
<td data-testid="races-cell-industry">
{formatCount(r.industry)}
<td class="numeric" data-testid="races-cell-cargo">
{formatFloat(r.cargo)}
</td>
<td data-testid="races-cell-planets">{formatCount(r.planets)}</td>
<td data-testid="races-cell-votes">
{formatVotes(r.votesReceived)}
<td class="numeric" data-testid="races-cell-population">
{formatInt(r.population)}
</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>
<div
@@ -359,6 +344,9 @@ data fetching is performed here — the layout is responsible.
color: var(--color-text);
font-variant-numeric: tabular-nums;
}
.summary-value.numeric {
font-family: var(--font-mono);
}
.vote-picker select {
font: inherit;
padding: 0.2rem 0.4rem;
@@ -403,7 +391,7 @@ data fetching is performed here — the layout is responsible.
padding: 0.4rem 0.6rem;
text-align: left;
border-bottom: 1px solid var(--color-border-subtle);
font-size: 0.9rem;
font-size: 0.85rem;
}
.grid th {
color: var(--color-text-muted);
@@ -411,6 +399,14 @@ data fetching is performed here — the layout is responsible.
letter-spacing: 0.04em;
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 {
background: var(--color-surface);
}
@@ -31,6 +31,7 @@ data fetching is performed here — the shell is responsible.
ORDER_DRAFT_CONTEXT_KEY,
OrderDraftStore,
} from "../../sync/order-draft.svelte";
import { formatPercent } from "$lib/util/number-format";
type SortColumn = "name" | "drive" | "weapons" | "shields" | "cargo";
type SortDirection = "asc" | "desc";
@@ -103,16 +104,6 @@ data fetching is performed here — the shell is responsible.
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 {
activeView.select("designer-science", { scienceId: name });
}
@@ -174,7 +165,7 @@ data fetching is performed here — the shell is responsible.
<thead>
<tr>
{#each COLUMNS as column (column)}
<th aria-sort={ariaSort(column)}>
<th aria-sort={ariaSort(column)} class:numeric={column !== "name"}>
<button
type="button"
class="sort"
@@ -201,14 +192,18 @@ data fetching is performed here — the shell is responsible.
ondblclick={() => openDesigner(sci.name)}
>
<td data-testid="sciences-cell-name">{sci.name}</td>
<td data-testid="sciences-cell-drive">{formatPercent(sci.drive)}</td>
<td data-testid="sciences-cell-weapons">
<td class="numeric" data-testid="sciences-cell-drive">
{formatPercent(sci.drive)}
</td>
<td class="numeric" data-testid="sciences-cell-weapons">
{formatPercent(sci.weapons)}
</td>
<td data-testid="sciences-cell-shields">
<td class="numeric" data-testid="sciences-cell-shields">
{formatPercent(sci.shields)}
</td>
<td data-testid="sciences-cell-cargo">{formatPercent(sci.cargo)}</td>
<td class="numeric" data-testid="sciences-cell-cargo">
{formatPercent(sci.cargo)}
</td>
<td>
<button
type="button"
@@ -283,7 +278,7 @@ data fetching is performed here — the shell is responsible.
padding: 0.4rem 0.6rem;
text-align: left;
border-bottom: 1px solid var(--color-border-subtle);
font-size: 0.9rem;
font-size: 0.85rem;
}
.grid th {
color: var(--color-text-muted);
@@ -291,6 +286,14 @@ data fetching is performed here — the shell is responsible.
letter-spacing: 0.04em;
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 {
cursor: pointer;
}
@@ -27,6 +27,7 @@ data fetching is performed here — the layout is responsible.
} from "../../sync/order-draft.svelte";
import { calculatorLoadRequest } from "$lib/calculator/load-request.svelte";
import ViewState from "$lib/ui/view-state.svelte";
import { formatFloat } from "$lib/util/number-format";
type SortColumn =
| "name"
@@ -106,10 +107,6 @@ data fetching is performed here — the layout is responsible.
return sortDirection === "asc" ? "ascending" : "descending";
}
function formatNumber(value: number): string {
return value.toLocaleString(undefined, { maximumFractionDigits: 2 });
}
function openInCalculator(name: string): void {
calculatorLoadRequest.request(name);
}
@@ -171,7 +168,7 @@ data fetching is performed here — the layout is responsible.
<thead>
<tr>
{#each COLUMNS as column (column)}
<th aria-sort={ariaSort(column)}>
<th aria-sort={ariaSort(column)} class:numeric={column !== "name"}>
<button
type="button"
class="sort"
@@ -198,11 +195,21 @@ data fetching is performed here — the layout is responsible.
ondblclick={() => openInCalculator(cls.name)}
>
<td data-testid="ship-classes-cell-name">{cls.name}</td>
<td data-testid="ship-classes-cell-drive">{formatNumber(cls.drive)}</td>
<td data-testid="ship-classes-cell-armament">{cls.armament}</td>
<td data-testid="ship-classes-cell-weapons">{formatNumber(cls.weapons)}</td>
<td data-testid="ship-classes-cell-shields">{formatNumber(cls.shields)}</td>
<td data-testid="ship-classes-cell-cargo">{formatNumber(cls.cargo)}</td>
<td class="numeric" data-testid="ship-classes-cell-drive">
{formatFloat(cls.drive)}
</td>
<td class="numeric" data-testid="ship-classes-cell-armament">
{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>
<button
type="button"
@@ -277,7 +284,7 @@ data fetching is performed here — the layout is responsible.
padding: 0.4rem 0.6rem;
text-align: left;
border-bottom: 1px solid var(--color-border-subtle);
font-size: 0.9rem;
font-size: 0.85rem;
}
.grid th {
color: var(--color-text-muted);
@@ -285,6 +292,14 @@ data fetching is performed here — the layout is responsible.
letter-spacing: 0.04em;
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 {
cursor: pointer;
}