Lay the phone out like the desktop when the surface is not a phone
ci / build and test (push) Successful in 2m54s
ci / android head (push) Failing after 3m15s
ci / api image (push) Successful in 46s

Three destinations in a bar and everything else behind SETTINGS is the right
shape at 360dp, where a fourth entry costs the width of the three that are there.
On a tablet, an unfolded foldable or a landscape phone it is the wrong one: there
is room for every destination at once, and the hub becomes an extra tap between
somebody and a screen they can already see space for.

So at 600dp — Android's own boundary between a compact window and a medium one,
in the density-independent units Avalonia lays out in — the bar stands down and
PhoneRail takes the left edge with all nine on it. It is the desktop's NavRail
arrangement rather than its file: the two heads cannot share a view, and this one
draws the phone's destination set with the phone's palette and touch targets.

The flags are computed in code rather than assembled in the markup because none
of them is a single question any more, and Avalonia's bindings have no "and" —
and the header's condition is an "or", which not even a wrapper can express. That
header is the one worth reading twice: narrow it stands down behind SETTINGS, so
the hub's screens can draw their own; wide there is no hub to be behind, so it
stays up everywhere. Losing it on the keychain would be losing the only LOCK
button on the surface.

Removing the hub means removing the routes into it, and there were four kinds.
The rail has no SETTINGS entry, because that screen is a menu of the rail. The
five back arrows in the screens under it are hidden, since an arrow to a screen
the layout removed is the one control on a header that leads nowhere. The system
back gesture goes to Hosts instead. And unfolding while sitting on the hub moves
to Hosts, rather than leaving somebody on a list of things now visible beside it.

One bug fixed on the way: OnBodyResized returned early unless the keyboard was
open, so a foldable would have opened to a phone layout until somebody typed
something. The chrome is refreshed first and unconditionally; the early return
belongs to the older job below it.

What this does not do is use the width *inside* a screen — the host list is one
column at any size. Two columns needs the row model to change, because that list
is headings and hosts in one sequence and a heading has to span, and that model
is shared with the desktop. Check 8.1 walks the rail; nothing here is covered by
a test, for the reason 8.0 exists.
This commit is contained in:
2026-08-05 20:50:18 +02:00
parent 0c61ea3a97
commit 5cb361ea13
11 changed files with 500 additions and 38 deletions
+46 -16
View File
@@ -917,7 +917,37 @@ repository; see `docs/platform-flags.md`. Get the stack with `adb logcat -d -b c
Every check below assumes this one passed, and none of them can be reached if it did not.
### 8.1 The + is reachable and is not covering anything
### 8.1 A wide surface draws the rail, and a narrow one draws the bar · **needs a tablet, a foldable or a resizable window**
Open the application on something at least 600dp wide — a tablet, an unfolded foldable, a phone in
landscape, or a freeform window dragged wide.
**Pass:** a rail down the left edge carrying Hosts, Connections, Keychain, Snippets, SFTP, S3, Logs, Vaults
and Preferences, with Preferences at the foot. **No bar across the bottom.** The header with the vault's
name, the sync light and LOCK is up, and stays up on every one of those screens rather than disappearing on
the keychain the way it does on a phone.
Now make it narrow again — fold it, rotate it, drag the window in.
**Pass:** the rail goes, the bottom bar comes back with its three entries, and the screens behind SETTINGS
have their back arrows again.
Go to SETTINGS on the narrow surface, then widen it without touching anything else.
**Pass:** it lands on Hosts rather than sitting on the hub. The hub is a list of the destinations the rail
now carries, so staying there would be a menu of things visible beside it.
On the wide surface, open the keychain and press the system back gesture.
**Pass:** Hosts. Not the hub — that screen is not drawn here, and backing into it would strand somebody on
a surface with no way off it but a second back.
**Failure means:** a rail *and* a bottom bar at once is `ShowsRail` and `ShowsBottomBar` no longer being
each other's opposite. A back arrow on a wide surface leads to a screen the layout removed. And a bar that
never becomes a rail is the resize handler having gone back to returning early when the keyboard is
closed — which is how a foldable would open to a phone layout until somebody typed something.
### 8.2 The + is reachable and is not covering anything
Look at the bottom right of the host list with no host selected.
@@ -930,7 +960,7 @@ and hold a host so the connect bar rises.
harness would catch it if it could run — its only fit rule is containment inside the window, never overlap
between two controls, so a button sitting squarely on top of CONNECT reports clean.
### 8.2 The sheet says what each choice does
### 8.3 The sheet says what each choice does
Tap the +.
@@ -941,7 +971,7 @@ with a sentence under it, and CANCEL. The scrim darkens the list.
not a decision, and navigating away is a fine way to answer it. Compare the changed-host-key screen, which
must offer no way out but BACK.
### 8.3 The sheet is dismissible three ways
### 8.4 The sheet is dismissible three ways
Tap the scrim. Reopen, tap CANCEL. Reopen, use the system back gesture.
@@ -950,7 +980,7 @@ Tap the scrim. Reopen, tap CANCEL. Reopen, use the system back gesture.
**Failure means:** back is being handled by the screen switch rather than by the guard above it, and the
gesture has left HOSTS instead of closing the sheet.
### 8.4 An empty port box says what it will dial
### 8.5 An empty port box says what it will dial
Add a group with a default port of 2222, then tap + → New host and choose that group in the GROUP picker.
@@ -960,7 +990,7 @@ chosen. The username box behaves the same way.
**Failure means:** the editor loaded a resolved value into the box instead of a placeholder behind it —
saving would then pin 2222 onto the host, and changing the group later would leave that host behind.
### 8.5 A host created that way inherits rather than copies
### 8.6 A host created that way inherits rather than copies
Save that host, then edit the group's default port to 2200 and look at the host's row.
@@ -968,7 +998,7 @@ Save that host, then edit the group's default port to 2200 and look at the host'
**Failure means:** the port was copied at creation and the group is a one-shot template.
### 8.6 Filing a host into a group does not silently pin its password
### 8.7 Filing a host into a group does not silently pin its password
Add a group with a default SSH key. Add a host with no group and leave AUTHENTICATION on **Password (ask
each time)**. Save. Now edit it and choose the group.
@@ -979,7 +1009,7 @@ each time)**. Save. Now edit it and choose the group.
**Failure means:** the picker's "Password (ask each time)" was written as a decision when it was only the
absence of one, and this host will never use the fleet's key.
### 8.7 A host can still refuse its group's key
### 8.8 A host can still refuse its group's key
On that same host, set AUTHENTICATION back to **Password (ask each time)** and save.
@@ -987,7 +1017,7 @@ On that same host, set AUTHENTICATION back to **Password (ask each time)** and s
**Failure means:** the fourth state has no storage and the host is inheriting whatever the group says.
### 8.8 Back closes the topmost thing, one at a time
### 8.9 Back closes the topmost thing, one at a time
Tap + → New host, then use the back gesture.
@@ -998,7 +1028,7 @@ Repeat with the group menu: tap the `⋯` on a group heading, then back.
**Pass:** the menu closes and nothing is armed behind it — no editor, no question, no move panel.
### 8.9 Every target is a thumb's size
### 8.10 Every target is a thumb's size
Walk both sheets' rows, the editor's fields, both pickers, the checkbox and the four buttons.
@@ -1007,7 +1037,7 @@ Walk both sheets' rows, the editor's fields, both pickers, the checkbox and the
**Failure means:** a control took its size from its glyph. The desktop suite's only tap-target assertion has
a floor of 20 pixels — set for a mouse — so copying it here would certify a target no thumb can hit.
### 8.10 The group menu names its group, and the heading still fits
### 8.11 The group menu names its group, and the heading still fits
Needs a vault with at least one group holding a host. On HOSTS, look at the group's heading row first.
@@ -1024,7 +1054,7 @@ scrim closes it.
**Failure means:** if the name is missing, the sheet is bound to the wrong thing; the whole point of naming
it is that the heading it was raised from may have scrolled away under the scrim.
### 8.11 Deleting a group asks the second question
### 8.12 Deleting a group asks the second question
`⋯` → Delete on a group with at least two hosts filed under it.
@@ -1041,7 +1071,7 @@ Now tick it and press DELETE.
**Pass:** the group and its hosts are gone. Untick it on a third group and the hosts survive under UNGROUPED.
### 8.12 Moving a group names the shelf · **needs a second writable vault**
### 8.13 Moving a group names the shelf · **needs a second writable vault**
`⋯` → Move to another vault.
@@ -1060,7 +1090,7 @@ selection, which this head never sets. That is the exact failure the header-aime
With only one writable vault, the entry answers on the status line instead and no panel opens. That is the
intended answer, not a failure.
### 8.13 DELETE under a host is not beside EDIT
### 8.14 DELETE under a host is not beside EDIT
Press and hold a host so the connect bar rises.
@@ -1076,7 +1106,7 @@ puts them back with the host still there.
the second press landing on whatever the list rebuilt underneath.
### 8.14 A tap connects, and a long press is what raises the bar · **the one worth the most care**
### 8.15 A tap connects, and a long press is what raises the bar · **the one worth the most care**
On a host bound to a key or a stored credential — one that needs nothing typed — tap the row once.
@@ -1100,7 +1130,7 @@ a password host with an empty box is worse: the connection fails somewhere the u
press that connects means `held` in `HostsScreen.axaml.cs` is not suppressing the `Tapped` that follows a
hold, so one touch fires both gestures.
### 8.15 The pencil in the header edits the chosen host
### 8.16 The pencil in the header edits the chosen host
With a host chosen — by a tap that connected, or by a long press — look at the header beside LOCK.
@@ -1337,7 +1367,7 @@ Tapping the hand-typed one puts its address back in the box on this screen and l
**Failure means:** if the hand-typed one navigates to HOSTS, the log entry is carrying a host id it should
not have. If the password came back, something is storing it — it never was. And if the keychain one arrives
with the host selected but **no bar**, `ConnectToRecent` is setting the selection rather than opening the
pane: since 8.14 a selection alone raises nothing, so that is a screen with nothing to press.
pane: since 8.15 a selection alone raises nothing, so that is a screen with nothing to press.
### 11.9 Settings holds the keychain, and back walks out of it