fix(ui): F8-06 calculator polish — drop delete-class button, reserve lock slot
- Remove the `delete <ship_class_name>` button (and `deleteClass`, `canDelete`, `.delete` CSS, `game.calculator.action.delete` i18n key) from the calculator. Delete-class lives in the ship-classes table — the broader rework will land under #53. - Bombing and cargo-capacity rows now reserve a hidden lock-slot placeholder so their value column lines up vertically with the mass/speed/attack/defence rows (which carry a lock button).
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -365,7 +365,6 @@ const ru: Record<keyof typeof en, string> = {
|
||||
"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": "двигатель",
|
||||
|
||||
@@ -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<void> {
|
||||
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<LockableOutputId, string> = $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.
|
||||
</button>
|
||||
{/if}
|
||||
</div>
|
||||
{#if cs.mode === "ship" && canDelete}
|
||||
<button
|
||||
type="button"
|
||||
class="delete"
|
||||
data-testid="calculator-delete"
|
||||
onclick={() => void deleteClass()}
|
||||
>
|
||||
{i18n.t("game.calculator.action.delete")} {cs.loadedExisting}
|
||||
</button>
|
||||
{/if}
|
||||
|
||||
<ShipDesignArea
|
||||
bind:blocks={cs.blocks}
|
||||
@@ -704,6 +679,7 @@ long-lived planning tool. `ensureGame` resets it when the game changes.
|
||||
<span class="val" data-testid="calculator-out-bombing">
|
||||
{fmt(result.outputs?.bombing)}
|
||||
</span>
|
||||
<span class="lock-slot" aria-hidden="true">🔓</span>
|
||||
</span>
|
||||
<span></span>
|
||||
</div>
|
||||
@@ -713,6 +689,7 @@ long-lived planning tool. `ensureGame` resets it when the game changes.
|
||||
<span class="val" data-testid="calculator-out-cargo-capacity">
|
||||
{fmt(result.outputs === null ? null : result.cargoCapacity)}
|
||||
</span>
|
||||
<span class="lock-slot" aria-hidden="true">🔓</span>
|
||||
</span>
|
||||
<span></span>
|
||||
</div>
|
||||
@@ -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;
|
||||
|
||||
@@ -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 <name>` 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();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user