diff --git a/ui/docs/calculator-ux.md b/ui/docs/calculator-ux.md index 5797eec..11fd44b 100644 --- a/ui/docs/calculator-ux.md +++ b/ui/docs/calculator-ux.md @@ -49,7 +49,10 @@ in as a per-ship result rather than a separate mode. units) that the loaded-column results use. At **full** the toggle shows the ship's cargo capacity; a **custom** load over that capacity is flagged as an error. With a zero cargo block there is no hold, so - the load is pinned to empty and the toggle is disabled. + the load is pinned to empty and the toggle is disabled. The bombing + and cargo-capacity rows have no goal-seek lock, but they still + reserve a hidden lock-slot placeholder so the value column stays + vertically aligned with the lockable rows above. 3. **Planet area** — when an own planet is selected on the map, shows its MAT (overridable) and the single-turn build rate (ships per turn, turns per ship). The MAT follows the same lock idiom as the tech @@ -125,17 +128,17 @@ fourth decimal as the user types: typing `1.2345` clamps the input to `util.Fixed*`; `Ceil3` is a display-only helper that lives in `pkg/calc` so the UI and Go share one implementation. -## Create / load / delete +## Create / load The name field is a combobox over the player's existing classes. Picking an existing class loads it as a template (so you can tweak and Create a new one); Create is disabled while the name is invalid or duplicate -(reusing `lib/util/ship-class-validation.ts`). When a saved class is -loaded, a Delete affordance appears. Create / Delete reuse the existing -`createShipClass` / `removeShipClass` order-draft flow, so the optimistic -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. +(reusing `lib/util/ship-class-validation.ts`). Create reuses the existing +`createShipClass` order-draft flow, so the optimistic overlay reflects +the change immediately. Ship classes are immutable after creation (per +`game/rules.txt`), so there is no edit — only Create-new. Delete-class +lives in the ship-classes table (`lib/active-view/table-ship-classes.svelte`), +not the calculator. Selecting a class from the dropdown loads it **immediately**, the moment the option is clicked. (Native `change` only fires on blur in diff --git a/ui/frontend/src/lib/i18n/locales/en.ts b/ui/frontend/src/lib/i18n/locales/en.ts index 353a17c..57031a0 100644 --- a/ui/frontend/src/lib/i18n/locales/en.ts +++ b/ui/frontend/src/lib/i18n/locales/en.ts @@ -364,7 +364,6 @@ const en = { "game.calculator.name.placeholder": "new class name", "game.calculator.name.existing": "your ship classes", "game.calculator.action.create": "create", - "game.calculator.action.delete": "delete", "game.calculator.col.ship": "ship", "game.calculator.col.tech": "tech", "game.calculator.field.drive": "drive", diff --git a/ui/frontend/src/lib/i18n/locales/ru.ts b/ui/frontend/src/lib/i18n/locales/ru.ts index c0ccda1..b6ac94b 100644 --- a/ui/frontend/src/lib/i18n/locales/ru.ts +++ b/ui/frontend/src/lib/i18n/locales/ru.ts @@ -365,7 +365,6 @@ const ru: Record = { "game.calculator.name.placeholder": "имя нового класса", "game.calculator.name.existing": "ваши классы кораблей", "game.calculator.action.create": "создать", - "game.calculator.action.delete": "удалить", "game.calculator.col.ship": "корабль", "game.calculator.col.tech": "технологии", "game.calculator.field.drive": "двигатель", diff --git a/ui/frontend/src/lib/sidebar/calculator-tab.svelte b/ui/frontend/src/lib/sidebar/calculator-tab.svelte index 41edfb2..5cb7dc0 100644 --- a/ui/frontend/src/lib/sidebar/calculator-tab.svelte +++ b/ui/frontend/src/lib/sidebar/calculator-tab.svelte @@ -204,11 +204,6 @@ long-lived planning tool. `ensureGame` resets it when the game changes. nameValidation.ok ? "" : i18n.t(nameInvalidKeyMap[nameValidation.reason]), ); const canCreate = $derived(nameValidation.ok && draft !== undefined); - const canDelete = $derived( - cs.loadedExisting !== null && - existingNames.includes(cs.loadedExisting) && - draft !== undefined, - ); // Per-block modernization upgrade cost (current tech → target tech). const modernCosts = $derived.by(() => { @@ -489,16 +484,6 @@ long-lived planning tool. `ensureGame` resets it when the game changes. cs.loadedExisting = created.name; } - async function deleteClass(): Promise { - if (cs.loadedExisting === null || draft === undefined) return; - await draft.add({ - kind: "removeShipClass", - id: crypto.randomUUID(), - name: cs.loadedExisting, - }); - cs.loadedExisting = null; - } - const LOCK_LABELS: Record = $derived({ emptyMass: i18n.t("game.calculator.out.mass"), loadedMass: i18n.t("game.calculator.out.mass"), @@ -608,16 +593,6 @@ long-lived planning tool. `ensureGame` resets it when the game changes. {/if} - {#if cs.mode === "ship" && canDelete} - - {/if} {fmt(result.outputs?.bombing)} + @@ -713,6 +689,7 @@ long-lived planning tool. `ensureGame` resets it when the game changes. {fmt(result.outputs === null ? null : result.cargoCapacity)} + @@ -893,8 +870,7 @@ long-lived planning tool. `ensureGame` resets it when the game changes. .name[aria-invalid="true"] { border-color: var(--color-danger); } - .create, - .delete { + .create { font: inherit; font-size: 0.8rem; padding: 0.25rem 0.55rem; @@ -912,10 +888,6 @@ long-lived planning tool. `ensureGame` resets it when the game changes. opacity: 0.5; cursor: not-allowed; } - .delete { - color: var(--color-danger); - align-self: flex-start; - } .load { display: flex; align-items: center; @@ -1036,6 +1008,12 @@ long-lived planning tool. `ensureGame` resets it when the game changes. cursor: not-allowed; opacity: 0.2; } + .lock-slot { + flex: none; + font-size: 0.7rem; + line-height: 1; + visibility: hidden; + } .planet { border-top: 1px solid var(--color-border-subtle); padding-top: 0.5rem; diff --git a/ui/frontend/tests/calculator-tab.test.ts b/ui/frontend/tests/calculator-tab.test.ts index e45c3c3..52c942d 100644 --- a/ui/frontend/tests/calculator-tab.test.ts +++ b/ui/frontend/tests/calculator-tab.test.ts @@ -628,4 +628,40 @@ describe("calculator-tab", () => { expect(ui.getByTestId("calculator-block-drive")).toHaveValue(3); confirm.mockRestore(); }); + + test("does not render a delete-class button after loading a class", async () => { + const ui = mount({ + report: makeReport({ + localShipClass: [ + { + name: "Scout", + drive: 3, + armament: 0, + weapons: 0, + shields: 2, + cargo: 1, + }, + ], + } as unknown as GameReport), + }); + await fireEvent.input(ui.getByTestId("calculator-name"), { + target: { value: "Scout" }, + inputType: "insertReplacementText", + }); + // The loaded class state used to render a `delete ` button; + // the calculator no longer owns delete-class — issue #53 will. + expect(ui.queryByTestId("calculator-delete")).toBeNull(); + }); + + test("bombing and cargo-capacity rows reserve the lock slot for column alignment", () => { + const ui = mount(); + for (const id of ["calculator-out-bombing", "calculator-out-cargo-capacity"]) { + const cell = ui.getByTestId(id).parentElement; + expect(cell).not.toBeNull(); + // A hidden placeholder occupies the same width as the lock button + // on the mass/speed/attack/defence rows, so the value column does + // not drift right on the rows without a lock. + expect(cell!.querySelector(".lock-slot")).not.toBeNull(); + } + }); });