fix(ui): F8-08 unified number format — mono, fixed 3-decimal, no separators
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:
@@ -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);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user