Give the user menu one door into settings instead of three
ci / build and test (push) Successful in 2m39s
ci / desktop nightly (push) Failing after 34s
ci / api image (push) Failing after 10s
ci / android head (push) Successful in 4m5s

Settings, Vaults and Preferences all opened the same full-window settings mode
and differed only in which of its seven pages they landed on. SettingsNav, which
lists every one of those pages including Vaults and Preferences, is on screen the
whole time you are in there — so two of the three rows were deep links into a
place the third one opens. They saved one click and cost a menu that read as
though it offered three destinations.

It was three rows for a reason that had stopped applying. Until v5c, "Settings"
and "Preferences" were two doors onto the same bare Preferences screen, and
splitting them was the fix for something real; Vaults arrived beside them when
VaultsScreen became a settings page too. Folding those two back in is not a
return to that state. They are pages now, with a rail of their own, and what a
menu at the foot of the nav rail is for is getting into settings at all.

Nothing became unreachable — both are one click deeper, from the rail that was
always going to be the way to them.

◆ WHAT STAYS, AND WHY IT IS NOT THE SAME ARGUMENT.

Logout stays and still lands on the Account page rather than on General, because
its confirmation card is drawn inline on that one page while the vault is
unlocked; arming it from anywhere else would raise a card nobody could see. The
vault switches and New vault above the divider stay too — neither is a page of
the settings mode, and the switches have no other home.

◆ THE ROWS ARE REMOVED FROM THE MARKUP, NOT UNWIRED.

NavRailTests asserts their absence rather than only asserting that Settings still
works, because the failure that guards against is silent: a row left in the
markup with its handler deleted is a menu entry that does nothing when pressed,
which reads as a broken application rather than a tidied menu. The helper it
asserts through excludes the vault switches by their data context and not by
their label — a vault is free to be called "Vaults", and a test that a menu entry
is absent must not be defeatable by what somebody named a vault.

Two facts go, one comes back plus the absence check; the layout suite is 166.

Manual check 1.8 walked a reader through all three rows in turn and now walks
them through the one, then through SettingsNav's seven — which is where the two
went. design-import-gaps.md gains a v5c-5 section rather than an edit to the v5c
table: what that table records is what shipped in that pass, and it did.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
jaap-janandClaude Opus 5 committed 2026-08-26 11:01:54 +02:00
1 parent c270f3fe39
commit ed936380b3
5 files changed
+94 -78

No files matched your search

@@ -210,32 +210,38 @@ public sealed class NavRailTests : IAsyncLifetime
}
/// <summary>
/// Settings, Vaults and Preferences each enter settings mode on the page they promise, and shut the
/// popover behind them.
/// Settings enters settings mode on its landing page, and shuts the popover behind it.
/// </summary>
/// <remarks>
/// <para>
/// v5c: these three used to land on a bare <c>ShellScreen</c> — Settings and Preferences on the very
/// same one, since the mock's own Settings area did not exist yet. Now that it does, each opens the
/// settings mode on its own page — see <see cref="MainWindowViewModel.EnterSettings"/> — and "Settings"
/// and "Preferences" are no longer the same click.
/// </para>
/// <para>
/// Three <see cref="Fact"/>s over one private body rather than a <see cref="Theory"/>: <c>SettingsPage</c>
/// is <c>internal</c>, and a public theory method may not carry an internal type in its signature.
/// </para>
/// v5c gave this menu three navigation rows — Settings, Vaults and Preferences — because settings mode
/// had just been built out of what were two bare <c>ShellScreen</c>s, and "Settings" and "Preferences"
/// had been the same click until then. All three opened the same mode and differed only in which page
/// they landed on, which made two of them deep links into a place the third one opens, past a
/// <c>SettingsNav</c> that lists every page anyway. One row now; see <c>NavRail.axaml</c>'s own remark.
/// </remarks>
[Fact]
public Task ThePopoversSettingsRow_EntersSettingsOnGeneralAndClosesThePopover() =>
APopoverRowLandsOnAsync("Settings", SettingsPage.General);
/// <summary>
/// The two rows that were folded into it are gone from the menu rather than merely unwired.
/// </summary>
/// <remarks>
/// Worth a test of its own because the failure it guards is silent: a row left in the markup and
/// stripped of its handler is a menu entry that does nothing when pressed, which reads as a broken
/// application rather than as a tidied menu. Both destinations are still reachable — one click deeper,
/// from <c>SettingsNav</c> — which is the whole argument for removing them from here.
/// </remarks>
[Fact]
public Task ThePopoversVaultsRow_EntersSettingsOnVaultsAndClosesThePopover() =>
APopoverRowLandsOnAsync("Vaults", SettingsPage.Vaults);
public Task ThePopoverNoLongerOffersVaultsOrPreferencesOfItsOwn() =>
OnTheRailAsync((rail, window) =>
{
Click(UserChip(rail), window);
[Fact]
public Task ThePopoversPreferencesRow_EntersSettingsOnPreferencesAndClosesThePopover() =>
APopoverRowLandsOnAsync("Preferences", SettingsPage.Preferences);
PopoverRows(window).ShouldNotContain("Vaults");
PopoverRows(window).ShouldNotContain("Preferences");
PopoverRows(window).ShouldContain("Settings");
});
private Task APopoverRowLandsOnAsync(string label, SettingsPage target) =>
OnTheRailAsync((rail, window) =>
@@ -369,6 +375,24 @@ public sealed class NavRailTests : IAsyncLifetime
.OfType<TextBlock>()
.Any(text => string.Equals(text.Text, label, StringComparison.Ordinal)));
/// <summary>
/// Every word the open popover offers as a place to go.
/// </summary>
/// <remarks>
/// The vault switches are excluded by their data context rather than by their label, because a vault is
/// free to be called anything — including "Vaults" — and a test asserting that a menu entry is absent
/// must not be satisfiable or defeatable by what somebody named a vault.
/// </remarks>
private static IReadOnlyList<string> PopoverRows(Visual window) =>
[.. window.GetVisualDescendants()
.OfType<Button>()
.Where(button => button.Classes.Contains("poprow")
&& button.DataContext is not VaultToggleViewModel)
.SelectMany(button => button.GetVisualDescendants().OfType<TextBlock>())
.Select(text => text.Text)
.Where(text => !string.IsNullOrEmpty(text))
.Select(text => text!)];
/// <summary>Whether a vault row's magenta check square is drawn, for whether it is currently shown.</summary>
private static bool HasVisibleCheck(Button row) =>
row.GetVisualDescendants().OfType<Border>().Any(