Merge branch 'main' into the group's move and its deletion question

Main took the group's EDIT and DELETE off the GROUPS heading while this branch
was adding a MOVE beside them, so the conflict was about the same six pixels
from both directions. Main's answer wins outright, and it is the better one for
the reason its own message gives: a button beside a heading has no card under a
pointer to mean, and had to work its subject out from the selection or from the
trail. Moving a group had that problem worst of all — the thing it takes with it
is everything on the shelf, and "which shelf" is not a question a button there
could answer plainly.

So the MOVE button is gone and the menu entry it was drawn beside is the whole
of it. That entry was already in this branch, above the separator DELETE sits
below, and it needed no change: the card menu selects whatever was right-clicked
before it runs anything, which is exactly the aiming a group move wants.

Three things went with the button. ShowsGroupActions, which main deleted because
hiding buttons was all it did, and which this branch had extended to hide them
for the move panel as well. CanMoveGroupTarget, which existed to answer whether
that button was worth drawing — CanMoveSelectedHost stays, because the phone
really does leave the host's MOVE out rather than offer a refusal, and a menu
whose entries came and went would be a menu whose items move. And the two test
assertions that read them, which were describing the button rather than the
behaviour; what they were guarding is that the two panels never share the
moment, and IsConfirmingGroupDeletion says that directly.

The move panel and the deletion question both keep their place under the
heading, which is where the buttons were and is now simply where that section
puts things. They still exclude each other, by disarming rather than by a
visibility flag: MoveGroup clears a pending deletion and DeleteGroup folds the
move panel away.

Manual checks 3.3 was rewritten by main for the menu and by this branch for the
tick, and now says both; 3.3a is new and walks a two-level shelf across a vault
boundary, which is the half of this feature no headless test can watch land.
This commit is contained in:
2026-08-04 17:11:06 +02:00
38 changed files with 3416 additions and 165 deletions
@@ -557,11 +557,10 @@ public sealed class ScreenLayoutTests : IAsyncLifetime
}
/// <remarks>
/// The question replaces the group's row of buttons rather than stacking under it the same rule every
/// other row in this application follows — and it is the tallest thing this section draws: a heading, a
/// consequence, a boxed count, and now a tick with a sentence beside it asking whether the machines go
/// too. The tick is the part worth measuring, because it is a wrapping paragraph inside a control whose
/// own height the layout does not obviously account for.
/// The question opens under the GROUPS heading and pushes the cards down, and it is the tallest thing
/// this section draws: a heading, a consequence, a boxed count, and now a tick with a sentence beside it
/// asking whether the machines go too. The tick is the part worth measuring, because it is a wrapping
/// paragraph inside a control whose own height the layout does not obviously account for.
/// </remarks>
[Fact]
public async Task TheHostsScreenFitsWhileAGroupDeletionIsBeingConfirmed()
@@ -580,10 +579,10 @@ public sealed class ScreenLayoutTests : IAsyncLifetime
/// <remarks>
/// <para>
/// The group's move panel, which takes that same row of buttons and is the wordier of the two: a
/// heading, a combo box, a wrapping paragraph naming everything that travels and everything that does
/// not, and two buttons — above a wrap of group cards and the host grid, all of which still have to fit
/// under it.
/// The group's move panel, under the GROUPS heading beside the deletion question and the wordier of the
/// two: a heading, a combo box, a wrapping paragraph naming everything that travels and everything that
/// does not, and two buttons — above a wrap of group cards and the host grid, all of which still have to
/// fit under it.
/// </para>
/// <para>
/// The state is set here rather than through <c>MoveGroupCommand</c>, which would refuse: this fixture's
@@ -604,7 +603,7 @@ public sealed class ScreenLayoutTests : IAsyncLifetime
vault.SelectedMoveGroupVault = vault.MoveGroupVaultChoices[0];
vault.IsMovingGroup = true;
vault.ShowsGroupActions.ShouldBeFalse("the panel takes the row rather than sharing it");
vault.IsConfirmingGroupDeletion.ShouldBeFalse("the two panels share the space and never the moment");
await MeasureHostsAsync(faults => faults.ShouldBeEmpty("with the group move panel up"));
}