From b31d9f4c45095f445bdaa2693733cecd8db207f0 Mon Sep 17 00:00:00 2001 From: Ilia Denisov Date: Wed, 27 May 2026 11:08:22 +0200 Subject: [PATCH 1/2] =?UTF-8?q?fix(ui):=20F8-08=20unified=20number=20forma?= =?UTF-8?q?t=20=E2=80=94=20mono,=20fixed=203-decimal,=20no=20separators?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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
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 / - 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) --- .../lib/active-view/designer-science.svelte | 15 +--- .../src/lib/active-view/report/format.ts | 69 +++++++--------- .../report/section-approaching-groups.svelte | 19 +++-- .../report/section-bombings.svelte | 31 ++++---- .../report/section-cargo-routes.svelte | 2 +- .../report/section-foreign-planets.svelte | 43 +++++----- .../report/section-foreign-sciences.svelte | 23 +++--- .../section-foreign-ship-classes.svelte | 27 ++++--- .../report/section-foreign-ship-groups.svelte | 15 ++-- .../report/section-galaxy-summary.svelte | 11 ++- .../report/section-my-fleets.svelte | 11 ++- .../report/section-my-planets.svelte | 43 +++++----- .../report/section-my-sciences.svelte | 23 +++--- .../report/section-my-ship-classes.svelte | 23 +++--- .../report/section-my-ship-groups.svelte | 15 ++-- .../report/section-player-status.svelte | 41 +++++----- .../report/section-ships-in-production.svelte | 19 +++-- .../report/section-unidentified-groups.svelte | 15 ++-- .../report/section-uninhabited-planets.svelte | 27 ++++--- .../report/section-unknown-planets.svelte | 11 ++- .../active-view/report/section-votes.svelte | 6 +- .../src/lib/active-view/table-races.svelte | 78 +++++++++---------- .../src/lib/active-view/table-sciences.svelte | 35 +++++---- .../lib/active-view/table-ship-classes.svelte | 37 ++++++--- ui/frontend/src/lib/i18n/locales/en.ts | 24 +++--- ui/frontend/src/lib/i18n/locales/ru.ts | 24 +++--- ui/frontend/src/lib/inspectors/planet.svelte | 34 ++++---- .../lib/inspectors/planet/ship-groups.svelte | 9 +-- .../src/lib/inspectors/ship-group.svelte | 49 ++++++------ .../lib/inspectors/ship-group/actions.svelte | 20 +++-- ui/frontend/src/lib/util/number-format.ts | 40 ++++++++++ ui/frontend/tests/inspector-planet.test.ts | 4 +- ui/frontend/tests/table-races.test.ts | 20 +++-- 33 files changed, 484 insertions(+), 379 deletions(-) create mode 100644 ui/frontend/src/lib/util/number-format.ts diff --git a/ui/frontend/src/lib/active-view/designer-science.svelte b/ui/frontend/src/lib/active-view/designer-science.svelte index e89e9cb..01aeaa6 100644 --- a/ui/frontend/src/lib/active-view/designer-science.svelte +++ b/ui/frontend/src/lib/active-view/designer-science.svelte @@ -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( 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" }); } diff --git a/ui/frontend/src/lib/active-view/report/format.ts b/ui/frontend/src/lib/active-view/report/format.ts index 7824c14..3ba3608 100644 --- a/ui/frontend/src/lib/active-view/report/format.ts +++ b/ui/frontend/src/lib/active-view/report/format.ts @@ -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 `# ()` if diff --git a/ui/frontend/src/lib/active-view/report/section-approaching-groups.svelte b/ui/frontend/src/lib/active-view/report/section-approaching-groups.svelte index 6d46daf..020f33c 100644 --- a/ui/frontend/src/lib/active-view/report/section-approaching-groups.svelte +++ b/ui/frontend/src/lib/active-view/report/section-approaching-groups.svelte @@ -42,11 +42,11 @@ class when the group lands and a battle roster forms. {i18n.t("game.report.section.approaching_groups.column.from")} {i18n.t("game.report.section.approaching_groups.column.to")} - + {i18n.t("game.report.section.approaching_groups.column.distance")} - {i18n.t("game.report.section.approaching_groups.column.speed")} - {i18n.t("game.report.section.approaching_groups.column.mass")} + {i18n.t("game.report.section.approaching_groups.column.speed")} + {i18n.t("game.report.section.approaching_groups.column.mass")} @@ -54,9 +54,9 @@ class when the group lands and a battle roster forms. {planetLabel(r.origin, planets)} {planetLabel(r.destination, planets)} - {formatFloat(r.distance)} - {formatFloat(r.speed)} - {formatFloat(r.mass)} + {formatFloat(r.distance)} + {formatFloat(r.speed)} + {formatFloat(r.mass)} {/each} @@ -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); } diff --git a/ui/frontend/src/lib/active-view/report/section-bombings.svelte b/ui/frontend/src/lib/active-view/report/section-bombings.svelte index 2f8789f..8c50285 100644 --- a/ui/frontend/src/lib/active-view/report/section-bombings.svelte +++ b/ui/frontend/src/lib/active-view/report/section-bombings.svelte @@ -42,16 +42,16 @@ Decoder sorts by `planetNumber` already. {i18n.t("game.report.section.bombings.column.owner")} {i18n.t("game.report.section.bombings.column.attacker")} {i18n.t("game.report.section.bombings.column.production")} - {i18n.t("game.report.section.bombings.column.industry")} - {i18n.t("game.report.section.bombings.column.population")} - {i18n.t("game.report.section.bombings.column.colonists")} - + {i18n.t("game.report.section.bombings.column.industry")} + {i18n.t("game.report.section.bombings.column.population")} + {i18n.t("game.report.section.bombings.column.colonists")} + {i18n.t("game.report.section.bombings.column.industry_stockpile")} - + {i18n.t("game.report.section.bombings.column.materials_stockpile")} - {i18n.t("game.report.section.bombings.column.attack_power")} + {i18n.t("game.report.section.bombings.column.attack_power")} @@ -67,12 +67,12 @@ Decoder sorts by `planetNumber` already. {b.owner} {b.attacker} {b.production} - {formatFloat(b.industry)} - {formatFloat(b.population)} - {formatFloat(b.colonists)} - {formatFloat(b.industryStockpile)} - {formatFloat(b.materialsStockpile)} - {formatCount(b.attackPower)} + {formatFloat(b.industry)} + {formatFloat(b.population)} + {formatFloat(b.colonists)} + {formatFloat(b.industryStockpile)} + {formatFloat(b.materialsStockpile)} + {formatCount(b.attackPower)} {#if b.wiped} {i18n.t("game.report.section.my_planets.column.number")} {i18n.t("game.report.section.my_planets.column.name")} {i18n.t("game.report.section.foreign_planets.column.owner")} - {i18n.t("game.report.section.my_planets.column.coordinates")} - {i18n.t("game.report.section.my_planets.column.size")} - {i18n.t("game.report.section.my_planets.column.resources")} - {i18n.t("game.report.section.my_planets.column.population")} - {i18n.t("game.report.section.my_planets.column.industry")} - + {i18n.t("game.report.section.my_planets.column.coordinates")} + {i18n.t("game.report.section.my_planets.column.size")} + {i18n.t("game.report.section.my_planets.column.resources")} + {i18n.t("game.report.section.my_planets.column.population")} + {i18n.t("game.report.section.my_planets.column.industry")} + {i18n.t("game.report.section.my_planets.column.industry_stockpile")} - + {i18n.t("game.report.section.my_planets.column.materials_stockpile")} - {i18n.t("game.report.section.my_planets.column.colonists")} + {i18n.t("game.report.section.my_planets.column.colonists")} {i18n.t("game.report.section.my_planets.column.production")} - {i18n.t("game.report.section.my_planets.column.free_industry")} + {i18n.t("game.report.section.my_planets.column.free_industry")} @@ -64,16 +64,16 @@ as the local planets table plus an `owner` column. {p.number} {p.name} {p.owner ?? ""} - {formatFloat(p.x)}, {formatFloat(p.y)} - {formatFloat(p.size ?? 0)} - {formatFloat(p.resources ?? 0)} - {formatFloat(p.population ?? 0)} - {formatFloat(p.industry ?? 0)} - {formatFloat(p.industryStockpile ?? 0)} - {formatFloat(p.materialsStockpile ?? 0)} - {formatFloat(p.colonists ?? 0)} + {formatFloat(p.x)}, {formatFloat(p.y)} + {formatFloat(p.size ?? 0)} + {formatFloat(p.resources ?? 0)} + {formatFloat(p.population ?? 0)} + {formatFloat(p.industry ?? 0)} + {formatFloat(p.industryStockpile ?? 0)} + {formatFloat(p.materialsStockpile ?? 0)} + {formatFloat(p.colonists ?? 0)} {p.production ?? "—"} - {formatFloat(p.freeIndustry ?? 0)} + {formatFloat(p.freeIndustry ?? 0)} {/each} @@ -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); } diff --git a/ui/frontend/src/lib/active-view/report/section-foreign-sciences.svelte b/ui/frontend/src/lib/active-view/report/section-foreign-sciences.svelte index 856ad4c..fdba66f 100644 --- a/ui/frontend/src/lib/active-view/report/section-foreign-sciences.svelte +++ b/ui/frontend/src/lib/active-view/report/section-foreign-sciences.svelte @@ -67,10 +67,10 @@ unit even when the section spans many races. {i18n.t("game.report.section.my_sciences.column.name")} - {i18n.t("game.report.section.my_sciences.column.drive")} - {i18n.t("game.report.section.my_sciences.column.weapons")} - {i18n.t("game.report.section.my_sciences.column.shields")} - {i18n.t("game.report.section.my_sciences.column.cargo")} + {i18n.t("game.report.section.my_sciences.column.drive")} + {i18n.t("game.report.section.my_sciences.column.weapons")} + {i18n.t("game.report.section.my_sciences.column.shields")} + {i18n.t("game.report.section.my_sciences.column.cargo")} @@ -81,10 +81,10 @@ unit even when the section spans many races. data-name={r.name} > {r.name} - {formatPercent(r.drive)} - {formatPercent(r.weapons)} - {formatPercent(r.shields)} - {formatPercent(r.cargo)} + {formatPercent(r.drive)} + {formatPercent(r.weapons)} + {formatPercent(r.shields)} + {formatPercent(r.cargo)} {/each} @@ -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); } diff --git a/ui/frontend/src/lib/active-view/report/section-foreign-ship-classes.svelte b/ui/frontend/src/lib/active-view/report/section-foreign-ship-classes.svelte index 1e2a323..e85a63f 100644 --- a/ui/frontend/src/lib/active-view/report/section-foreign-ship-classes.svelte +++ b/ui/frontend/src/lib/active-view/report/section-foreign-ship-classes.svelte @@ -65,12 +65,12 @@ incoming groups. {i18n.t("game.report.section.my_ship_classes.column.name")} - {i18n.t("game.report.section.my_ship_classes.column.drive")} + {i18n.t("game.report.section.my_ship_classes.column.drive")} {i18n.t("game.report.section.my_ship_classes.column.armament")} - {i18n.t("game.report.section.my_ship_classes.column.weapons")} - {i18n.t("game.report.section.my_ship_classes.column.shields")} - {i18n.t("game.report.section.my_ship_classes.column.cargo")} - {i18n.t("game.report.section.foreign_ship_classes.column.mass")} + {i18n.t("game.report.section.my_ship_classes.column.weapons")} + {i18n.t("game.report.section.my_ship_classes.column.shields")} + {i18n.t("game.report.section.my_ship_classes.column.cargo")} + {i18n.t("game.report.section.foreign_ship_classes.column.mass")} @@ -81,12 +81,12 @@ incoming groups. data-name={r.name} > {r.name} - {formatFloat(r.drive)} + {formatFloat(r.drive)} {r.armament} - {formatFloat(r.weapons)} - {formatFloat(r.shields)} - {formatFloat(r.cargo)} - {formatFloat(r.mass)} + {formatFloat(r.weapons)} + {formatFloat(r.shields)} + {formatFloat(r.cargo)} + {formatFloat(r.mass)} {/each} @@ -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); } diff --git a/ui/frontend/src/lib/active-view/report/section-foreign-ship-groups.svelte b/ui/frontend/src/lib/active-view/report/section-foreign-ship-groups.svelte index 01b8076..4957f34 100644 --- a/ui/frontend/src/lib/active-view/report/section-foreign-ship-groups.svelte +++ b/ui/frontend/src/lib/active-view/report/section-foreign-ship-groups.svelte @@ -49,8 +49,8 @@ to groups the player doesn't own. {i18n.t("game.report.section.my_ship_groups.column.destination")} {i18n.t("game.report.section.my_ship_groups.column.origin")} {i18n.t("game.report.section.my_ship_groups.column.range")} - {i18n.t("game.report.section.my_ship_groups.column.speed")} - {i18n.t("game.report.section.my_ship_groups.column.mass")} + {i18n.t("game.report.section.my_ship_groups.column.speed")} + {i18n.t("game.report.section.my_ship_groups.column.mass")} @@ -64,8 +64,8 @@ to groups the player doesn't own. {g.origin === null ? "—" : planetLabel(g.origin, planets)} {g.range === null ? "—" : formatFloat(g.range)} - {formatFloat(g.speed)} - {formatFloat(g.mass)} + {formatFloat(g.speed)} + {formatFloat(g.mass)} {/each} @@ -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); } diff --git a/ui/frontend/src/lib/active-view/report/section-galaxy-summary.svelte b/ui/frontend/src/lib/active-view/report/section-galaxy-summary.svelte index e3f231b..c86150d 100644 --- a/ui/frontend/src/lib/active-view/report/section-galaxy-summary.svelte +++ b/ui/frontend/src/lib/active-view/report/section-galaxy-summary.svelte @@ -31,13 +31,13 @@ section is never empty as long as the report has loaded. {:else}
{i18n.t("game.report.section.galaxy_summary.field.turn")}
-
{report.turn}
+
{report.turn}
{i18n.t("game.report.section.galaxy_summary.field.size")}
-
+
{report.mapWidth} × {report.mapHeight}
{i18n.t("game.report.section.galaxy_summary.field.planets")}
-
{report.planetCount}
+
{report.planetCount}
{i18n.t("game.report.section.galaxy_summary.field.race")}
{report.race}
@@ -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); + } diff --git a/ui/frontend/src/lib/active-view/report/section-my-fleets.svelte b/ui/frontend/src/lib/active-view/report/section-my-fleets.svelte index 6b250ac..66dfe50 100644 --- a/ui/frontend/src/lib/active-view/report/section-my-fleets.svelte +++ b/ui/frontend/src/lib/active-view/report/section-my-fleets.svelte @@ -44,7 +44,7 @@ in orbit has neither); empty cells in those columns are normal. {i18n.t("game.report.section.my_fleets.column.destination")} {i18n.t("game.report.section.my_fleets.column.origin")} {i18n.t("game.report.section.my_fleets.column.range")} - {i18n.t("game.report.section.my_fleets.column.speed")} + {i18n.t("game.report.section.my_fleets.column.speed")} @@ -58,7 +58,7 @@ in orbit has neither); empty cells in those columns are normal. {f.origin === null ? "—" : planetLabel(f.origin, planets)} {f.range === null ? "—" : formatFloat(f.range)} - {formatFloat(f.speed)} + {formatFloat(f.speed)} {/each} @@ -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); } diff --git a/ui/frontend/src/lib/active-view/report/section-my-planets.svelte b/ui/frontend/src/lib/active-view/report/section-my-planets.svelte index cff1181..8809dde 100644 --- a/ui/frontend/src/lib/active-view/report/section-my-planets.svelte +++ b/ui/frontend/src/lib/active-view/report/section-my-planets.svelte @@ -41,20 +41,20 @@ column set (matches `ReportPlanet` shape). {i18n.t("game.report.section.my_planets.column.number")} {i18n.t("game.report.section.my_planets.column.name")} - {i18n.t("game.report.section.my_planets.column.coordinates")} - {i18n.t("game.report.section.my_planets.column.size")} - {i18n.t("game.report.section.my_planets.column.resources")} - {i18n.t("game.report.section.my_planets.column.population")} - {i18n.t("game.report.section.my_planets.column.industry")} - + {i18n.t("game.report.section.my_planets.column.coordinates")} + {i18n.t("game.report.section.my_planets.column.size")} + {i18n.t("game.report.section.my_planets.column.resources")} + {i18n.t("game.report.section.my_planets.column.population")} + {i18n.t("game.report.section.my_planets.column.industry")} + {i18n.t("game.report.section.my_planets.column.industry_stockpile")} - + {i18n.t("game.report.section.my_planets.column.materials_stockpile")} - {i18n.t("game.report.section.my_planets.column.colonists")} + {i18n.t("game.report.section.my_planets.column.colonists")} {i18n.t("game.report.section.my_planets.column.production")} - {i18n.t("game.report.section.my_planets.column.free_industry")} + {i18n.t("game.report.section.my_planets.column.free_industry")} @@ -62,16 +62,16 @@ column set (matches `ReportPlanet` shape). {p.number} {p.name} - {formatFloat(p.x)}, {formatFloat(p.y)} - {formatFloat(p.size ?? 0)} - {formatFloat(p.resources ?? 0)} - {formatFloat(p.population ?? 0)} - {formatFloat(p.industry ?? 0)} - {formatFloat(p.industryStockpile ?? 0)} - {formatFloat(p.materialsStockpile ?? 0)} - {formatFloat(p.colonists ?? 0)} + {formatFloat(p.x)}, {formatFloat(p.y)} + {formatFloat(p.size ?? 0)} + {formatFloat(p.resources ?? 0)} + {formatFloat(p.population ?? 0)} + {formatFloat(p.industry ?? 0)} + {formatFloat(p.industryStockpile ?? 0)} + {formatFloat(p.materialsStockpile ?? 0)} + {formatFloat(p.colonists ?? 0)} {p.production ?? "—"} - {formatFloat(p.freeIndustry ?? 0)} + {formatFloat(p.freeIndustry ?? 0)} {/each} @@ -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); } diff --git a/ui/frontend/src/lib/active-view/report/section-my-sciences.svelte b/ui/frontend/src/lib/active-view/report/section-my-sciences.svelte index 23b15ce..6e0b03c 100644 --- a/ui/frontend/src/lib/active-view/report/section-my-sciences.svelte +++ b/ui/frontend/src/lib/active-view/report/section-my-sciences.svelte @@ -39,20 +39,20 @@ table). {i18n.t("game.report.section.my_sciences.column.name")} - {i18n.t("game.report.section.my_sciences.column.drive")} - {i18n.t("game.report.section.my_sciences.column.weapons")} - {i18n.t("game.report.section.my_sciences.column.shields")} - {i18n.t("game.report.section.my_sciences.column.cargo")} + {i18n.t("game.report.section.my_sciences.column.drive")} + {i18n.t("game.report.section.my_sciences.column.weapons")} + {i18n.t("game.report.section.my_sciences.column.shields")} + {i18n.t("game.report.section.my_sciences.column.cargo")} {#each rows as r (r.name)} {r.name} - {formatPercent(r.drive)} - {formatPercent(r.weapons)} - {formatPercent(r.shields)} - {formatPercent(r.cargo)} + {formatPercent(r.drive)} + {formatPercent(r.weapons)} + {formatPercent(r.shields)} + {formatPercent(r.cargo)} {/each} @@ -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); } diff --git a/ui/frontend/src/lib/active-view/report/section-my-ship-classes.svelte b/ui/frontend/src/lib/active-view/report/section-my-ship-classes.svelte index 9e23a3b..731e189 100644 --- a/ui/frontend/src/lib/active-view/report/section-my-ship-classes.svelte +++ b/ui/frontend/src/lib/active-view/report/section-my-ship-classes.svelte @@ -40,22 +40,22 @@ drafts immediately, matching the ship-class designer's behaviour. {i18n.t("game.report.section.my_ship_classes.column.name")} - {i18n.t("game.report.section.my_ship_classes.column.drive")} + {i18n.t("game.report.section.my_ship_classes.column.drive")} {i18n.t("game.report.section.my_ship_classes.column.armament")} - {i18n.t("game.report.section.my_ship_classes.column.weapons")} - {i18n.t("game.report.section.my_ship_classes.column.shields")} - {i18n.t("game.report.section.my_ship_classes.column.cargo")} + {i18n.t("game.report.section.my_ship_classes.column.weapons")} + {i18n.t("game.report.section.my_ship_classes.column.shields")} + {i18n.t("game.report.section.my_ship_classes.column.cargo")} {#each rows as r (r.name)} {r.name} - {formatFloat(r.drive)} + {formatFloat(r.drive)} {r.armament} - {formatFloat(r.weapons)} - {formatFloat(r.shields)} - {formatFloat(r.cargo)} + {formatFloat(r.weapons)} + {formatFloat(r.shields)} + {formatFloat(r.cargo)} {/each} @@ -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); } diff --git a/ui/frontend/src/lib/active-view/report/section-my-ship-groups.svelte b/ui/frontend/src/lib/active-view/report/section-my-ship-groups.svelte index a501a92..016c53a 100644 --- a/ui/frontend/src/lib/active-view/report/section-my-ship-groups.svelte +++ b/ui/frontend/src/lib/active-view/report/section-my-ship-groups.svelte @@ -59,8 +59,8 @@ shown together with `load` when carrying. {i18n.t("game.report.section.my_ship_groups.column.destination")} {i18n.t("game.report.section.my_ship_groups.column.origin")} {i18n.t("game.report.section.my_ship_groups.column.range")} - {i18n.t("game.report.section.my_ship_groups.column.speed")} - {i18n.t("game.report.section.my_ship_groups.column.mass")} + {i18n.t("game.report.section.my_ship_groups.column.speed")} + {i18n.t("game.report.section.my_ship_groups.column.mass")} {i18n.t("game.report.section.my_ship_groups.column.fleet")} @@ -77,8 +77,8 @@ shown together with `load` when carrying. {g.origin === null ? "—" : planetLabel(g.origin, planets)} {g.range === null ? "—" : formatFloat(g.range)} - {formatFloat(g.speed)} - {formatFloat(g.mass)} + {formatFloat(g.speed)} + {formatFloat(g.mass)} {g.fleet ?? "—"} {/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); } diff --git a/ui/frontend/src/lib/active-view/report/section-player-status.svelte b/ui/frontend/src/lib/active-view/report/section-player-status.svelte index 6377865..99776d1 100644 --- a/ui/frontend/src/lib/active-view/report/section-player-status.svelte +++ b/ui/frontend/src/lib/active-view/report/section-player-status.svelte @@ -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( RENDERED_REPORT_CONTEXT_KEY, @@ -37,14 +37,14 @@ highlight so the user can locate themselves quickly. {i18n.t("game.report.section.player_status.column.name")} - {i18n.t("game.report.section.player_status.column.drive")} - {i18n.t("game.report.section.player_status.column.weapons")} - {i18n.t("game.report.section.player_status.column.shields")} - {i18n.t("game.report.section.player_status.column.cargo")} - {i18n.t("game.report.section.player_status.column.population")} - {i18n.t("game.report.section.player_status.column.industry")} - {i18n.t("game.report.section.player_status.column.planets")} - {i18n.t("game.report.section.player_status.column.votes")} + {i18n.t("game.report.section.player_status.column.drive")} + {i18n.t("game.report.section.player_status.column.weapons")} + {i18n.t("game.report.section.player_status.column.shields")} + {i18n.t("game.report.section.player_status.column.cargo")} + {i18n.t("game.report.section.player_status.column.population")} + {i18n.t("game.report.section.player_status.column.industry")} + {i18n.t("game.report.section.player_status.column.planets")} + {i18n.t("game.report.section.player_status.column.votes")} @@ -73,14 +73,14 @@ highlight so the user can locate themselves quickly.
{/if} - {formatPercent(p.drive)} - {formatPercent(p.weapons)} - {formatPercent(p.shields)} - {formatPercent(p.cargo)} - {formatCount(p.population)} - {formatCount(p.industry)} - {formatCount(p.planets)} - {formatVotes(p.votesReceived)} + {formatFloat(p.drive)} + {formatFloat(p.weapons)} + {formatFloat(p.shields)} + {formatFloat(p.cargo)} + {formatCount(p.population)} + {formatCount(p.industry)} + {formatCount(p.planets)} + {formatVotes(p.votesReceived)} {/each} @@ -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); } diff --git a/ui/frontend/src/lib/active-view/report/section-ships-in-production.svelte b/ui/frontend/src/lib/active-view/report/section-ships-in-production.svelte index e39ceee..db6ccab 100644 --- a/ui/frontend/src/lib/active-view/report/section-ships-in-production.svelte +++ b/ui/frontend/src/lib/active-view/report/section-ships-in-production.svelte @@ -41,12 +41,12 @@ reads `#17 (Castle)` rather than just `#17`. {i18n.t("game.report.section.ships_in_production.column.planet")} {i18n.t("game.report.section.ships_in_production.column.class")} - {i18n.t("game.report.section.ships_in_production.column.cost")} - + {i18n.t("game.report.section.ships_in_production.column.cost")} + {i18n.t("game.report.section.ships_in_production.column.prod_used")} {i18n.t("game.report.section.ships_in_production.column.percent")} - {i18n.t("game.report.section.ships_in_production.column.free")} + {i18n.t("game.report.section.ships_in_production.column.free")} @@ -58,10 +58,10 @@ reads `#17 (Castle)` rather than just `#17`. > {planetLabel(r.planetNumber, planets)} {r.class} - {formatFloat(r.cost)} - {formatFloat(r.prodUsed)} + {formatFloat(r.cost)} + {formatFloat(r.prodUsed)} {(r.percent * 100).toFixed(1)} - {formatFloat(r.freeIndustry)} + {formatFloat(r.freeIndustry)} {/each} @@ -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); } diff --git a/ui/frontend/src/lib/active-view/report/section-unidentified-groups.svelte b/ui/frontend/src/lib/active-view/report/section-unidentified-groups.svelte index 2407efe..bb1f5e3 100644 --- a/ui/frontend/src/lib/active-view/report/section-unidentified-groups.svelte +++ b/ui/frontend/src/lib/active-view/report/section-unidentified-groups.svelte @@ -37,15 +37,15 @@ radar that doesn't even resolve to a planet. - - + + {#each rows as g, i (i)} - - + + {/each} @@ -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); } diff --git a/ui/frontend/src/lib/active-view/report/section-uninhabited-planets.svelte b/ui/frontend/src/lib/active-view/report/section-uninhabited-planets.svelte index ea13203..0d8fb01 100644 --- a/ui/frontend/src/lib/active-view/report/section-uninhabited-planets.svelte +++ b/ui/frontend/src/lib/active-view/report/section-uninhabited-planets.svelte @@ -42,13 +42,13 @@ are intentionally omitted. - - - - + + + - @@ -58,11 +58,11 @@ are intentionally omitted. - - - - - + + + + + {/each} @@ -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); } diff --git a/ui/frontend/src/lib/active-view/report/section-unknown-planets.svelte b/ui/frontend/src/lib/active-view/report/section-unknown-planets.svelte index e5805b9..2075b7f 100644 --- a/ui/frontend/src/lib/active-view/report/section-unknown-planets.svelte +++ b/ui/frontend/src/lib/active-view/report/section-unknown-planets.svelte @@ -40,14 +40,14 @@ else is known. - + {#each rows as p (p.number)} - + {/each} @@ -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); } diff --git a/ui/frontend/src/lib/active-view/report/section-votes.svelte b/ui/frontend/src/lib/active-view/report/section-votes.svelte index a36f054..3ee4ace 100644 --- a/ui/frontend/src/lib/active-view/report/section-votes.svelte +++ b/ui/frontend/src/lib/active-view/report/section-votes.svelte @@ -58,14 +58,14 @@ explanatory text on the races table. - + {#each races as r (r.name)} - + {/each} @@ -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 { diff --git a/ui/frontend/src/lib/active-view/table-races.svelte b/ui/frontend/src/lib/active-view/table-races.svelte index 61f1e91..c1bb261 100644 --- a/ui/frontend/src/lib/active-view/table-races.svelte +++ b/ui/frontend/src/lib/active-view/table-races.svelte @@ -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 { 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. {i18n.t("game.table.races.votes.mine")}: - {formatVotes(myVotes)} + {formatFloat(myVotes)} {#each COLUMNS as column (column)} - - - - - - - - - + + + {#each COLUMNS as column (column)} - - - + - - + {#each COLUMNS as column (column)} - - - - - - + + + + +
{i18n.t("game.report.section.unidentified_groups.column.x")}{i18n.t("game.report.section.unidentified_groups.column.y")}{i18n.t("game.report.section.unidentified_groups.column.x")}{i18n.t("game.report.section.unidentified_groups.column.y")}
{formatFloat(g.x)}{formatFloat(g.y)}{formatFloat(g.x)}{formatFloat(g.y)}
{i18n.t("game.report.section.my_planets.column.number")} {i18n.t("game.report.section.my_planets.column.name")}{i18n.t("game.report.section.my_planets.column.coordinates")}{i18n.t("game.report.section.my_planets.column.size")}{i18n.t("game.report.section.my_planets.column.resources")} + {i18n.t("game.report.section.my_planets.column.coordinates")}{i18n.t("game.report.section.my_planets.column.size")}{i18n.t("game.report.section.my_planets.column.resources")} {i18n.t("game.report.section.my_planets.column.industry_stockpile")} + {i18n.t("game.report.section.my_planets.column.materials_stockpile")}
{p.number} {p.name}{formatFloat(p.x)}, {formatFloat(p.y)}{formatFloat(p.size ?? 0)}{formatFloat(p.resources ?? 0)}{formatFloat(p.industryStockpile ?? 0)}{formatFloat(p.materialsStockpile ?? 0)}{formatFloat(p.x)}, {formatFloat(p.y)}{formatFloat(p.size ?? 0)}{formatFloat(p.resources ?? 0)}{formatFloat(p.industryStockpile ?? 0)}{formatFloat(p.materialsStockpile ?? 0)}
{i18n.t("game.report.section.my_planets.column.number")}{i18n.t("game.report.section.my_planets.column.coordinates")}{i18n.t("game.report.section.my_planets.column.coordinates")}
{p.number}{formatFloat(p.x)}, {formatFloat(p.y)}{formatFloat(p.x)}, {formatFloat(p.y)}
{i18n.t("game.report.section.votes.column.race")}{i18n.t("game.report.section.votes.column.votes")}{i18n.t("game.report.section.votes.column.votes")}
{r.name}{formatVotes(r.votesReceived)}{formatVotes(r.votesReceived)}
+ {r.name}{formatPercent(r.drive)} - {formatPercent(r.weapons)} + + {formatFloat(r.drive)} - {formatPercent(r.shields)} + + {formatFloat(r.weapons)} {formatPercent(r.cargo)} - {formatCount(r.population)} + + {formatFloat(r.shields)} - {formatCount(r.industry)} + + {formatFloat(r.cargo)} {formatCount(r.planets)} - {formatVotes(r.votesReceived)} + + {formatInt(r.population)} + + {formatInt(r.industry)} + + {formatInt(r.planets)} + + {formatFloat(r.votesReceived)}
+ {sci.name}{formatPercent(sci.drive)} + + {formatPercent(sci.drive)} + {formatPercent(sci.weapons)} + {formatPercent(sci.shields)} {formatPercent(sci.cargo)} + {formatPercent(sci.cargo)} +
+ {cls.name}{formatNumber(cls.drive)}{cls.armament}{formatNumber(cls.weapons)}{formatNumber(cls.shields)}{formatNumber(cls.cargo)} + {formatFloat(cls.drive)} + + {cls.armament} + + {formatFloat(cls.weapons)} + + {formatFloat(cls.shields)} + + {formatFloat(cls.cargo)} + @@ -141,7 +138,7 @@ ship-groups table view with an additional `(planet, race)` filter. {i18n.t("game.inspector.planet.ship_groups.row.mass", { - mass: formatNumber(row.mass), + mass: formatFloat(row.mass), })} {/if} diff --git a/ui/frontend/src/lib/inspectors/ship-group.svelte b/ui/frontend/src/lib/inspectors/ship-group.svelte index bc5e588..be8e242 100644 --- a/ui/frontend/src/lib/inspectors/ship-group.svelte +++ b/ui/frontend/src/lib/inspectors/ship-group.svelte @@ -20,6 +20,7 @@ variant — for Phase 19 the inspector is intentionally read-only. ShipClassSummary, } from "../../api/game-state"; import { i18n, type TranslationKey } from "$lib/i18n/index.svelte"; + import { formatFloat } from "$lib/util/number-format"; import Actions from "./ship-group/actions.svelte"; export type ShipGroupSelection = @@ -82,10 +83,6 @@ variant — for Phase 19 the inspector is intentionally read-only. return planet.name; } - function formatNumber(value: number): string { - return value.toLocaleString(undefined, { maximumFractionDigits: 2 }); - } - function cargoLabel(cargo: "NONE" | "COL" | "CAP" | "MAT" | "EMP"): string { if (cargo === "NONE") { return i18n.t("game.inspector.ship_group.cargo.none"); @@ -132,27 +129,27 @@ variant — for Phase 19 the inspector is intentionally read-only.
{i18n.t("game.inspector.ship_group.field.count")}
-
{g.count}
+
{g.count}
{i18n.t("game.inspector.ship_group.field.drive")}
-
{formatNumber(g.tech.drive)}
+
{formatFloat(g.tech.drive)}
{i18n.t("game.inspector.ship_group.field.weapons")}
-
{formatNumber(g.tech.weapons)}
+
{formatFloat(g.tech.weapons)}
{i18n.t("game.inspector.ship_group.field.shields")}
-
{formatNumber(g.tech.shields)}
+
{formatFloat(g.tech.shields)}
{i18n.t("game.inspector.ship_group.field.cargo_tech")}
-
{formatNumber(g.tech.cargo)}
+
{formatFloat(g.tech.cargo)}
{i18n.t("game.inspector.ship_group.field.mass")}
-
{formatNumber(g.mass)}
+
{formatFloat(g.mass)}
{i18n.t("game.inspector.ship_group.field.cargo_load")}
@@ -160,7 +157,7 @@ variant — for Phase 19 the inspector is intentionally read-only. {#if g.cargo === "NONE"} {cargoLabel(g.cargo)} {:else} - {cargoLabel(g.cargo)} × {formatNumber(g.load)} + {cargoLabel(g.cargo)} × {formatFloat(g.load)} {/if}
@@ -181,7 +178,7 @@ variant — for Phase 19 the inspector is intentionally read-only.
{i18n.t("game.inspector.ship_group.field.distance")}
-
{formatNumber(g.range!)}
+
{formatFloat(g.range!)}
{/if} @@ -212,23 +209,25 @@ variant — for Phase 19 the inspector is intentionally read-only.
{i18n.t("game.inspector.ship_group.field.distance")}
-
{formatNumber(g.distance)}
+
{formatFloat(g.distance)}
{i18n.t("game.inspector.ship_group.field.speed")}
-
{formatNumber(g.speed)}
+
{formatFloat(g.speed)}
{i18n.t("game.inspector.ship_group.field.eta")}
- {eta === null - ? i18n.t("game.designer.ship_class.preview.unavailable") - : eta} + {#if eta === null} + {i18n.t("game.designer.ship_class.preview.unavailable")} + {:else} + {eta} + {/if}
{i18n.t("game.inspector.ship_group.field.mass")}
-
{formatNumber(g.mass)}
+
{formatFloat(g.mass)}
{:else} @@ -238,8 +237,8 @@ variant — for Phase 19 the inspector is intentionally read-only. data-testid="inspector-ship-group-field-coordinates" >
{i18n.t("game.inspector.ship_group.field.coordinates")}
-
- ({formatNumber(selection.group.x)}, {formatNumber(selection.group.y)}) +
+ {formatFloat(selection.group.x)}, {formatFloat(selection.group.y)}
@@ -285,16 +284,20 @@ variant — for Phase 19 the inspector is intentionally read-only. } .field dt { color: var(--color-text-muted); - font-size: 0.85rem; + font-size: 0.8rem; } .field dd { margin: 0; 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 { margin: 0; color: var(--color-text-muted); - font-size: 0.85rem; + font-size: 0.8rem; } diff --git a/ui/frontend/src/lib/inspectors/ship-group/actions.svelte b/ui/frontend/src/lib/inspectors/ship-group/actions.svelte index a9e7a6a..2dcd37b 100644 --- a/ui/frontend/src/lib/inspectors/ship-group/actions.svelte +++ b/ui/frontend/src/lib/inspectors/ship-group/actions.svelte @@ -42,6 +42,7 @@ modernize cost preview backed by `core.blockUpgradeCost`. type ShipGroupUpgradeTech, } from "../../../sync/order-types"; import { validateEntityName } from "$lib/util/entity-name"; + import { formatFloat } from "$lib/util/number-format"; type Props = { group: ReportLocalShipGroup; @@ -675,9 +676,6 @@ modernize cost preview backed by `core.blockUpgradeCost`. CARGO: "game.inspector.ship_group.action.tech.cargo", }; - function formatNumber(value: number): string { - return value.toLocaleString(undefined, { maximumFractionDigits: 2 }); - }
@@ -973,7 +971,7 @@ modernize cost preview backed by `core.blockUpgradeCost`. {i18n.t("game.inspector.ship_group.action.modernize.cost_unavailable")} {:else} {i18n.t("game.inspector.ship_group.action.modernize.cost", { - cost: formatNumber(modernizeCostPreview), + cost: formatFloat(modernizeCostPreview), })} {/if}

@@ -1148,7 +1146,7 @@ modernize cost preview backed by `core.blockUpgradeCost`. } .action { font: inherit; - font-size: 0.85rem; + font-size: 0.8rem; padding: 0.2rem 0.55rem; background: transparent; color: var(--color-text-muted); @@ -1177,7 +1175,7 @@ modernize cost preview backed by `core.blockUpgradeCost`. display: flex; flex-direction: column; gap: 0.2rem; - font-size: 0.85rem; + font-size: 0.8rem; color: var(--color-text-muted); } .form input[type="number"], @@ -1196,7 +1194,7 @@ modernize cost preview backed by `core.blockUpgradeCost`. flex-wrap: wrap; align-items: baseline; gap: 0.4rem; - font-size: 0.85rem; + font-size: 0.8rem; } .form .destination-readonly .label { color: var(--color-text-muted); @@ -1208,7 +1206,7 @@ modernize cost preview backed by `core.blockUpgradeCost`. } .form-actions button { font: inherit; - font-size: 0.85rem; + font-size: 0.8rem; padding: 0.25rem 0.65rem; background: transparent; color: var(--color-text-muted); @@ -1226,18 +1224,18 @@ modernize cost preview backed by `core.blockUpgradeCost`. } .preview { margin: 0; - font-size: 0.85rem; + font-size: 0.8rem; color: var(--color-text-muted); } .warning { margin: 0; - font-size: 0.85rem; + font-size: 0.8rem; color: var(--color-warning); } .locked { margin: 0; padding: 0.4rem 0.55rem; - font-size: 0.85rem; + font-size: 0.8rem; color: var(--color-text-muted); background: var(--color-surface-overlay); border: 1px solid var(--color-border); diff --git a/ui/frontend/src/lib/util/number-format.ts b/ui/frontend/src/lib/util/number-format.ts new file mode 100644 index 0000000..a774ecd --- /dev/null +++ b/ui/frontend/src/lib/util/number-format.ts @@ -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); +} diff --git a/ui/frontend/tests/inspector-planet.test.ts b/ui/frontend/tests/inspector-planet.test.ts index 439c31e..84b41ce 100644 --- a/ui/frontend/tests/inspector-planet.test.ts +++ b/ui/frontend/tests/inspector-planet.test.ts @@ -87,7 +87,7 @@ describe("planet inspector", () => { ); expect( ui.getByTestId("inspector-planet-field-coordinates"), - ).toHaveTextContent("(100.25, 200)"); + ).toHaveTextContent("100.250, 200.000"); expect(ui.getByTestId("inspector-planet-field-size")).toHaveTextContent( "size", ); @@ -240,7 +240,7 @@ describe("planet inspector", () => { ); expect( 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-natural_resources")).toBeNull(); }); diff --git a/ui/frontend/tests/table-races.test.ts b/ui/frontend/tests/table-races.test.ts index c07de22..fc37079 100644 --- a/ui/frontend/tests/table-races.test.ts +++ b/ui/frontend/tests/table-races.test.ts @@ -175,18 +175,16 @@ describe("races table", () => { expect(rows).toHaveLength(1); expect(rows[0]).toHaveAttribute("data-name", "Andori"); expect(ui.getByTestId("races-cell-name")).toHaveTextContent("Andori"); - expect(ui.getByTestId("races-cell-drive")).toHaveTextContent("25"); - expect(ui.getByTestId("races-cell-weapons")).toHaveTextContent("50"); - expect(ui.getByTestId("races-cell-shields")).toHaveTextContent("75"); - expect(ui.getByTestId("races-cell-cargo")).toHaveTextContent("100"); - expect(ui.getByTestId("races-cell-population")).toHaveTextContent( - /12[,\s]345/, - ); - expect(ui.getByTestId("races-cell-industry")).toHaveTextContent( - /6[,\s]?789/, - ); + // drive/weapons/shields/cargo are tech LEVELS from the engine + // (see F8-08 bugfix); rendered as 3-decimal Floats, not percents. + expect(ui.getByTestId("races-cell-drive")).toHaveTextContent("0.250"); + expect(ui.getByTestId("races-cell-weapons")).toHaveTextContent("0.500"); + expect(ui.getByTestId("races-cell-shields")).toHaveTextContent("0.750"); + expect(ui.getByTestId("races-cell-cargo")).toHaveTextContent("1.000"); + expect(ui.getByTestId("races-cell-population")).toHaveTextContent("12345"); + expect(ui.getByTestId("races-cell-industry")).toHaveTextContent("6789"); 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 () => { -- 2.52.0 From ed4e2f58a16a9180183185339ff974079b073e27 Mon Sep 17 00:00:00 2001 From: Ilia Denisov Date: Wed, 27 May 2026 11:14:28 +0200 Subject: [PATCH 2/2] =?UTF-8?q?test(ui):=20F8-08=20e2e=20=E2=80=94=20match?= =?UTF-8?q?=20new=201-dec=20percent=20+=203-dec=20float=20formatting?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit sciences.spec.ts: `sciences-cell-drive` now reads "25.0" (was "25") because formatPercent always emits one fractional digit. ship-classes.spec.ts: `ship-classes-cell-drive` now reads "1.000" (was "1") because formatFloat always emits three fractional digits. Co-Authored-By: Claude Opus 4.7 (1M context) --- ui/frontend/tests/e2e/sciences.spec.ts | 2 +- ui/frontend/tests/e2e/ship-classes.spec.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/ui/frontend/tests/e2e/sciences.spec.ts b/ui/frontend/tests/e2e/sciences.spec.ts index 6e004ae..ffdec61 100644 --- a/ui/frontend/tests/e2e/sciences.spec.ts +++ b/ui/frontend/tests/e2e/sciences.spec.ts @@ -319,7 +319,7 @@ test("create / list / delete science via the table + designer", async ({ await expect(page.getByTestId("sciences-table")).toBeVisible(); const row = page.getByTestId("sciences-row"); await expect(row).toHaveAttribute("data-name", "FirstStep"); - await expect(page.getByTestId("sciences-cell-drive")).toHaveText("25"); + await expect(page.getByTestId("sciences-cell-drive")).toHaveText("25.0"); // The auto-sync round-trip lands as applied. await page.getByTestId("sidebar-tab-order").click(); diff --git a/ui/frontend/tests/e2e/ship-classes.spec.ts b/ui/frontend/tests/e2e/ship-classes.spec.ts index cf42edc..5742f65 100644 --- a/ui/frontend/tests/e2e/ship-classes.spec.ts +++ b/ui/frontend/tests/e2e/ship-classes.spec.ts @@ -293,7 +293,7 @@ test("create / list / delete ship class via the table + calculator", async ({ await expect(page.getByTestId("ship-classes-table")).toBeVisible(); const row = page.getByTestId("ship-classes-row"); await expect(row).toHaveAttribute("data-name", "Drone"); - await expect(page.getByTestId("ship-classes-cell-drive")).toHaveText("1"); + await expect(page.getByTestId("ship-classes-cell-drive")).toHaveText("1.000"); // The auto-sync round-trip lands as applied. await page.getByTestId("sidebar-tab-order").click(); -- 2.52.0