fix(ui): F8-06 calculator polish — unified spinner UX, lock-infeasible on (0, 1), dropdown reset-changes
Tests · UI / test (push) Has been cancelled
Tests · Integration / integration (pull_request) Successful in 1m45s
Tests · Go / test (pull_request) Successful in 2m3s
Tests · UI / test (pull_request) Successful in 2m28s

Owner review on PR #61:

- п.9 (option B). Hide the native spinner on EVERY numeric input in
  the calculator (DWSC blocks, armament, tech, planet MAT, custom
  load, lock value, modernization target tech) and drive every step
  through ArrowUp / ArrowDown. The column widths stay stable and the
  inputs read consistently across the whole row. The ship blocks
  keep the smart (0 ↔ 1) jump on ArrowUp/ArrowDown; armament steps
  ±1 with a JS handler instead of relying on the native spinner.
  Other inputs step by their natural grain (±0.001 for tech / lock,
  ±0.01 for MAT / load).
- п.10. Tech-level labels (`tech-val`) and the planet MAT label
  (`mat-val`) now read through the same `Ceil3` formatter as the
  derived results, so plain-text numeric values share the report's
  3-decimal tabular formatting. The design-area component receives
  `formatNumber` as a prop; the resolved (goal-seek) cell uses the
  same formatter, so the read-only computed value matches the rest
  of the row.
- п.12. `computeCalculator` now validates the back-solved block
  against the same DWSC rule the live validator enforces (`0` or
  `≥ 1`). When the solver lands in the `(0, 1)` gap (e.g. attack
  0.5 / weaponsTech 1.5 → weapons 0.333…) the lock is flagged
  infeasible — the lock input flips red and the claimed block is
  NOT back-solved into the invalid range, so the design preview
  keeps reading the user's own typed values instead of silently
  showing a sub-1 block.
- new. Selecting an existing ship class from the name datalist now
  loads it immediately. `change` fires only on blur in Firefox,
  which is why the previous behaviour looked delayed; switching the
  load to `oninput` with an `InputEvent.inputType` check makes the
  load synchronous everywhere (datalist replacement carries
  `"insertReplacementText"` in Chromium / WebKit, `undefined` in
  Firefox; keyboard typing always carries a typing `inputType`).
  Before loading we compare the live blocks to the previously
  loaded class (or to the empty defaults) and, if they differ, ask
  through a `window.confirm`. On decline we revert the name field
  and leave the design untouched.

Tests: calculator-tab and calc-model gain six cases (armament
step, tech/MAT formatter labels, lock infeasible on (0, 1) for
both attack→weapons and emptyMass→cargo, lock-value Arrow step,
dropdown immediate load + confirm-blocks-load + confirm-allows-load),
all 779 vitest tests green. docs/calculator-ux.md follows the new
behaviour.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
Ilia Denisov
2026-05-26 18:02:56 +02:00
parent e9b904332e
commit cbf7f65916
8 changed files with 466 additions and 61 deletions
+42 -18
View File
@@ -30,12 +30,19 @@ in as a per-ship result rather than a separate mode.
override at or above their current tech. Clicking the closed
padlock resets to the default. The padlock slot is always reserved,
so the column width does not shift as the lock state toggles. The
four ship-class blocks (drive, weapons, shields, cargo) use a smart
keyboard step that respects the engine value rule (`0` or `≥ 1`):
ArrowUp from 0 jumps straight to 1, otherwise +0.1; ArrowDown from
1 collapses to 0, otherwise 0.1, never producing an invalid value
in `(0, 1)`. The native spinner is hidden on these inputs (it would
produce invalid intermediates); armament keeps its native step 1.
inherited tech value reads through the same 3-decimal `Ceil3`
formatter the report uses, so the column lines up with derived
values. **Every numeric input in the calculator hides the native
spinner and drives stepping through ArrowUp / ArrowDown.** This keeps
the column widths stable, makes the inputs read consistently, and
gives each row a step that matches its purpose. The four ship-class
blocks (drive, weapons, shields, cargo) use a smart step that
respects the engine value rule (`0` or `≥ 1`): ArrowUp from 0 jumps
straight to 1, otherwise +0.1; ArrowDown from 1 collapses to 0,
otherwise 0.1, never producing an invalid value in `(0, 1)`.
Armament steps ±1 (clamped at 0). Tech, planet MAT, custom load,
lock value, and modernization target tech each step by their natural
grain (±0.001 for tech and lock values, ±0.01 for MAT and load).
2. **Calculator area** — derived results: empty/loaded mass, empty/
loaded speed, attack, defence, bombing (per ship), cargo capacity.
A load toggle (empty / full / custom) sets the cargo load (in cargo
@@ -48,8 +55,10 @@ in as a per-ship result rather than a separate mode.
turns per ship). The MAT follows the same lock idiom as the tech
cells: the planet number renders with an open padlock, clicking
opens an input with a closed padlock, and the closed padlock resets
to the planet value. The realistic multi-turn forecast with CAP/COL
supply is planned (see ../ROADMAP.md).
to the planet value. The MAT label reads through the same 3-decimal
`Ceil3` formatter, matching the rest of the calculator's label
values. The realistic multi-turn forecast with CAP/COL supply is
planned (see ../ROADMAP.md).
## Locks and goal-seek
@@ -80,16 +89,19 @@ pinned by the player, click to reset*:
Only **one** result may be locked at a time (the others' lock
affordances disable with a tooltip). An unreachable target — e.g. a
speed above the stripped-hull ceiling `20 × driveTech`, or a
solved block that fails the value rules — leaves the locked cell in a
red error state and does not apply. Inverse solving lives in
`pkg/calc/solve.go`; the bisection for defence → shields is the only
non-analytic case. Locking a speed is disabled when the drive block is
zero (a deliberately immobile ship has no speed to back-solve). With
the drive block as the only non-zero mass the displayed speed equals
the ceiling exactly (every positive drive gives the same speed), so
the solver accepts that ceiling target as a feasible lock and any
positive drive solves it.
speed above the stripped-hull ceiling `20 × driveTech`, or a solved
block that fails the value rules (a DWSC value in the `(0, 1)` gap)
— leaves the locked cell in a red error state and does not apply.
When that happens the claimed block is **not** back-solved into the
invalid range; the design preview keeps reading the user's typed
values, so the row never silently shows a sub-1 block. Inverse
solving lives in `pkg/calc/solve.go`; the bisection for defence →
shields is the only non-analytic case. Locking a speed is disabled
when the drive block is zero (a deliberately immobile ship has no
speed to back-solve). With the drive block as the only non-zero mass
the displayed speed equals the ceiling exactly (every positive drive
gives the same speed), so the solver accepts that ceiling target as
a feasible lock and any positive drive solves it.
## Validation and display
@@ -119,6 +131,18 @@ overlay reflects the change immediately. Ship classes are immutable after
creation (per `game/rules.txt`), so there is no edit — only Create-new
and Delete.
Selecting a class from the dropdown loads it **immediately**, the
moment the option is clicked. (Native `change` only fires on blur in
Firefox; switching the load trigger to `input` makes the load
synchronous everywhere, since the `InputEvent.inputType` flags a
datalist replacement as `"insertReplacementText"` in Chromium / WebKit
or `undefined` in Firefox — keyboard typing always carries a typing
`inputType`.) If the live blocks differ from the previously loaded
class (or, when nothing is loaded, from the empty defaults), the
calculator first asks `Discard unsaved changes and load class «…»?`
through a `window.confirm`; declining reverts the name field and
leaves the current blocks untouched.
## Reach circles
When an own planet is selected in calculator mode, the calculator