Give the phone the + it was promised, and the editors behind it

Steps 5 and 6 of docs/adding-hosts-on-the-phone.md, which finishes it. The
phone can now put things in a keychain rather than only read one.

The floating + arrives three designs after it was first asked for. It was
refused twice on the honest grounds that it would open an editor this head had
not got — and that stopped being true when steps 1 to 4 landed. It raises a
sheet rather than a form, because "add" on this screen has been two operations
since groups existed: a host, or a group to file hosts under.

Both editors are cards in the list's own row, swapped for the list rather than
stacked over it, following SnippetsScreen and for the reason written there: a
form on top of the list hides what it is about. There is still no dialog and no
editor screen anywhere on this head. The port and username boxes may be left
empty, and the greyed text inside each is what the host will actually use —
following the group picker as it moves, so the form says what leaving a box
blank will do rather than making it a guess.

The back gesture gains a guard above the switch rather than a case inside it.
An editor is not a screen and has no entry there, and it is strictly nearer:
the sheet sits over the editor's own screen, so back lowers whatever is topmost
before it considers moving between screens.

A group editor needs a way in, and the phone draws no groups panel — so the
heading in the host list gains a pencil. A + that adds groups with no way to
correct one is the same strange thing to ship as one that adds hosts with no
way to correct one, and worse: a group's defaults are what every host beneath
it falls back to.

Scouting step 6 turned up a bug worth more than the prose it was looking for.
ReloadGroupsAsync read the active vault alone, which was a cosmetic limitation
while a group carried only a name — a teammate's host showed under UNGROUPED
and nothing else was lost. Since a group began lending a port, a username and a
binding, the same omission silently dropped all three: that host would dial 22
as nobody while the machine is on 2222 as deploy, with nothing on screen saying
why. The resolution map now spans every readable vault. The editable list stays
narrow, because a row shown across vaults needs a vault id for rename and
delete and two vaults may hold groups with one name — but the map needs none of
that, since it is only ever asked what an id says.

An adversarial review of this change found something I had written a comment
denying. The + was a child of the outer Panel rather than of the list's row, so
it anchored to the bottom of the screen and sat squarely over the new EDIT
button and the lower edge of CONNECT — and because a Panel hit-tests its
last-declared child first, tapping the right end of EDIT would have raised the
add sheet. The comment beside it claimed it was in the list's row precisely so
that could not happen, and manual-checks 8.1 names that exact failure. It is in
Grid.Row 2 now. The same pass caught a connect bar that was disabled rather
than hidden while its own comment said hidden, a scrim that would have flashed
its default pressed chrome across the screen, a fab comment claiming an accent
fill no other button had when Button.primary has it, and a back-gesture remark
naming two view-model properties that do not exist anywhere in the repo.

That last one came from this plan, which invented IsVaultsSurface and
IsConnectionsSurface, and pointed step 6 at a ConnectionsScreen.axaml that has
never existed. Both are corrected in the plan rather than quietly worked
around, along with a fourth "groups are flat" site in README.md that step 1
missed.

The phone's rectangles are not measurable and structurally never will be: the
layout suite is net10.0, the Android head is net10.0-android, and Avalonia's
application is a one-shot process global — so a second head cannot share the
process even if the reference were possible. Everything the sheet and both
editors bind to is shared and is tested headlessly here; the pixels go to
docs/manual-checks.md phase 8, nine checks, which is where this project already
sends what it cannot assert.

Tags remain the one thing asked for that did not ship. Tag is a full item kind
and TagIds merges, encodes and resolves, but no screen draws a chip or offers
to add one, so the tags a client can store are ones nothing here can see. Both
editors carry the set through a save untouched so a client that can set them
does not lose them. Recorded as half-shipped in docs/design-import-gaps.md
rather than quietly left out.

Verified by the whole suite: 1402 tests over nineteen projects, none failing.
Both heads build. Nothing seen on a display — phase 8 is what that costs.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
2026-08-03 11:30:00 +02:00
co-authored by Claude Opus 5
parent b7335743d9
commit 4925dcf179
11 changed files with 1050 additions and 50 deletions
+58 -3
View File
@@ -115,10 +115,65 @@
</Style>
<!--
No floating action button. The design puts one on HOSTS and one on S3, and both would open an editor
this head has not got — hosts and keychain items are created on the desktop and sync down. The style is
not here waiting for them either: an unused style is a claim that the control exists somewhere.
The floating action button the design has asked for since v2, and which this theme refused twice — on
the grounds that it would open an editor this head had not got, and that an unused style is a claim the
control exists somewhere. Both were true. The second is why the style arrives in the same commit as the
button rather than ahead of it.
56 rather than the 44 the header icons use. It is the only control on the screen that floats over
content rather than sitting in a bar, so it has no neighbouring chrome to borrow an edge from, and a
thumb reaching the bottom-right corner of a 360dp screen is doing it without looking.
Accent-filled, which it shares with Button.primary and with nothing else — and it means the same thing
in both places: the one action on the surface that is not a choice between peers. Circular by radius
rather than by a Path, so the pressed state the template draws is the same shape as the button.
Still only on HOSTS. The design puts a second one on S3 and that editor genuinely does not exist yet, so
the style being here is not permission to draw one there.
-->
<Style Selector="Button.fab">
<Setter Property="Width" Value="56" />
<Setter Property="Height" Value="56" />
<Setter Property="Padding" Value="0" />
<Setter Property="Background" Value="{StaticResource Accent}" />
<Setter Property="BorderThickness" Value="0" />
<Setter Property="CornerRadius" Value="28" />
<Setter Property="HorizontalContentAlignment" Value="Center" />
<Setter Property="VerticalContentAlignment" Value="Center" />
<Setter Property="Foreground" Value="{StaticResource AccentInk}" />
<Setter Property="FontFamily" Value="{StaticResource MonoFont}" />
<Setter Property="FontSize" Value="24" />
<Setter Property="FontWeight" Value="SemiBold" />
</Style>
<Style Selector="Button.fab:pressed /template/ ContentPresenter">
<Setter Property="Background" Value="{StaticResource Accent}" />
<Setter Property="Opacity" Value="0.82" />
</Style>
<!--
The dimmed area behind a sheet, and a Button rather than a Border because tapping it has to mean
something — see the add sheet in HostsScreen, which is dismissible for the reason the host-key sheet is
not.
62% of Canvas, written out because the palette holds no alpha variant of a surface. The pressed rule is
not decoration: a Button's default template lights its ContentPresenter when touched, so without it the
scrim would flash a pale rectangle across the whole screen on the way to closing — which reads as the
interface glitching rather than as a tap being received.
-->
<Style Selector="Button.scrim">
<Setter Property="Background" Value="#9E0E1220" />
<Setter Property="BorderThickness" Value="0" />
<Setter Property="CornerRadius" Value="0" />
<Setter Property="Padding" Value="0" />
<Setter Property="HorizontalAlignment" Value="Stretch" />
<Setter Property="VerticalAlignment" Value="Stretch" />
</Style>
<Style Selector="Button.scrim:pointerover /template/ ContentPresenter">
<Setter Property="Background" Value="#9E0E1220" />
</Style>
<Style Selector="Button.scrim:pressed /template/ ContentPresenter">
<Setter Property="Background" Value="#9E0E1220" />
</Style>
<!--
A card: one item, one rule, one thing you can act on. The design uses it for a key, a snippet, a