Move SFTP and S3 into the tab strip, and the host list into a card grid

Four asks in one pass over the desktop head, and two of them are furniture
moving rather than anything new.

THE STRIP IS THE WINDOW'S NOW, not the terminal's. Vaults, SFTP and S3 sit at
its head and cannot be closed, and open terminals follow them. SFTP and S3 left
the nav rail to get there, which is the one semantic change: they are the two
destinations you stay in while something runs, and a rail entry is for
somewhere you go and come back from. So the rail belongs to the Vaults tab and
collapses with it, which also means SFTP, S3 and a terminal each get the full
1016 rather than the 826 a page gets.

The tab is expressed as "a page, and not one of the two the strip took" rather
than as a fourth ShellSurface. Both are still ShellScreen members and have to
be — that is what they are on the phone, where they are two rows in a hub — so
a surface each would have been a second way to say a thing Screen already says.
IsTransfersShowing and IsBucketsShowing light the other two tabs unchanged.
What is new is one field: the page Vaults comes back to, because it is the one
tab with sub-navigation and therefore the one that can return to the wrong
place. That is not the hidden field ShellSurface argues against — that one
would be a second copy of "which page"; this is a tab remembering its own.

THE HOSTS SCREEN IS A GRID, and the 268-pixel sidebar is gone. That column was
choosing among forty machines and editing one of them at two-thirds width, and
it was narrow so the editor beneath it could be a column at all. Cards took the
first job at full width; a 304-pixel drawer took the second and collapses when
nothing is selected. Pressing a group card narrows the grid; SHOW ALL is the
way back. The group editor moved into the drawer as well, which finally makes
IsEditingGroup mean the same thing on both heads — it was the phone's alone,
because the desktop's editor was a bar that was always on screen.

AreHostsExpanded and ToggleHosts went with the control that used them. They
folded the whole list away under one heading, an affordance that existed
because the column was narrow. Folding a single group is a different thing and
is still here.

THE TYPE SCALE IS A POINT LARGER and the text ramp is white. The base size was
never stated anywhere — a bare TextBlock took TextElement's default of 12 — so
raising the scale meant naming it, on Window and on UserControl. The second
selector is not redundancy: the layout harness hosts a UserControl in a window
it builds itself, and without it the suite would measure every screen a point
smaller than it ships, silently. A selector on TextBlock would have been the
obvious way and is wrong, because a style setter beats an inherited value and
would collapse every deliberate step back to one number.

#E3E7F4 is a blue-tinted white on blue-black surfaces, which costs contrast
twice — once for being darker than white and once for sharing a hue with what
it is drawn on. Pure white is 18.3:1 against the canvas where that was 15.5:1.
Every step below moved with the top, so the intervals the design chose are
kept and TextDim clears 9:1 against 6.4:1. The palette is shared, so the phone
has both changes too.

TWO DEFECTS THE HARNESS STRUCTURALLY CANNOT SEE, found by rendering the screen
rather than by measuring it, and both now covered.

The tile was 232 and was first written as 248, from arithmetic that left out
the scrolling stack's own margins. Every layout test passed — the harness asks
whether a control is inside the window, never how many fit on a line — so the
grid quietly became one column wide at exactly the minimum this application
guarantees, which is the shape cards exist to avoid.
TheHostsGridKeepsTwoColumnsAtTheMinimumWithTheDrawerOpen counts columns
instead, and fails at 248.

And a card's text ran past its own border, because a horizontal StackPanel
measures children with infinite width: a TextBlock inside one never learns it
is short of room, so TextTrimming never fires. Both card rows are grids with a
star column that gives way and an Auto column that does not — a hostname with
its tail cut is still the machine you were looking for, where a badge or the
word naming an auth method is not.

The keychain header changed shape for the same class of reason. It was
Auto,Auto,*,Auto with the buttons last, so the slack column was the only thing
absorbing a change of width and five buttons fell off the right edge the moment
the type grew. That is how GENERATE lost the word KEY once already. The summary
sits in the star column and trims now, so the buttons always get their width.

HostSidebarTests became HostGridTests and moved to the grid with the gestures
it drives. docs/design-import-gaps.md gains a v3 section naming the five
toolbar controls in the design with nothing behind them — a view-mode switch, a
tag filter, a calendar, a share control and Serial — and manual-checks.md and
the README follow the controls that moved.
This commit is contained in:
2026-08-03 15:08:48 +02:00
parent 416f233657
commit 208443b932
38 changed files with 2252 additions and 1432 deletions
+29 -20
View File
@@ -22,10 +22,11 @@ Each item says what to do, what a pass looks like, and what a failure would mean
### 1.1 No screen is sliced at the WebView's left edge · **the important one**
Open two terminals, then visit every nav rail entry in turn — HOSTS, FILES, KEYS, TEAM, PREFS.
Open two terminals, then visit every nav rail entry in turn — Hosts, Keychain, Pins, Snippets, Logs, Teams,
Preferences — and both of the fixed tabs, SFTP and S3.
**Pass:** each screen draws whole, its buttons all clickable, and the tab strip stays across the top of all
five.
nine. The nav rail is there for the seven and gone for the two, because it belongs to the Vaults tab.
**Failure means:** a screen is not collapsing while the terminal shows. The terminal is a native child
window and composites above everything Avalonia paints, so the symptom is a screen cut off at the WebView's
@@ -268,8 +269,8 @@ single-process test can reach.
Open the hosts screen without creating any group.
**Pass:** the sidebar list is the flat list of hosts it always was — no headings, no UNGROUPED, nothing
saying the hosts are unfiled.
**Pass:** the grid is the flat wrap of host cards it always was — no GROUPS section above it, no headings
between the cards, no UNGROUPED, nothing saying the hosts are unfiled.
**Failure means:** the "invisible until used" property is gone, and every existing user gets a heading they
did not ask for. `RebuildSidebarRows` returns early when `Groups` is empty; that early return is the feature.
@@ -281,9 +282,13 @@ Make two groups, file some hosts into each through the host editor, then click a
**Pass:** the heading's chevron flips and its hosts disappear; the count on the heading does not change,
because it counts what is in the group rather than what is on screen. Clicking again brings them back.
**Also check:** clicking a heading does not change which host is selected — the buttons at the foot of the
sidebar go on acting on the same machine. This is asserted in a test, but the test drives the view model
directly; what it cannot see is whether the `ListBox` writes something else back through the binding first.
**Also check:** clicking a heading does not change which host is selected — the drawer stays open on the
same machine and its EDIT and DELETE go on acting on it. This is asserted in a test, but the test drives the
view model directly; what it cannot see is whether the `ListBox` writes something else back through the
binding first.
**Then press a group card.** The grid narrows to that group's hosts, the card is marked as chosen, and
SHOW ALL appears beside GROUPS. Pressing it brings the rest back and unmarks the card.
### 3.3 Deleting a group with hosts in it
@@ -639,10 +644,10 @@ back.
### 7.6 Dragging a host into a group · **least covered, like all drag and drop**
Make two groups and file a host into one. Drag a host row onto another group's heading; onto a host row
Make two groups and file a host into one. Drag a host card onto another group's heading; onto a host card
inside another group; and onto UNGROUPED.
**Pass:** the row under the pointer washes accent while the pointer is over it, the cursor shows a move
**Pass:** whatever is under the pointer washes accent while the pointer is over it, the cursor shows a move
rather than a refusal, and the drop files the host — it moves under that heading and the counts on both
headings change. Dropping onto its own group's heading is refused while still in the air.
@@ -651,16 +656,19 @@ automated. The write it performs is: `MovingAHostToAGroup_FilesItAndLeavesItSele
### 7.7 A click still selects, and a double click still connects
Click host rows; drag one a few pixels without releasing; double-click one.
Click host cards; drag one a few pixels without releasing; double-click one. Then double-click a group
heading.
**Pass:** a click selects, a small movement starts nothing, and a double click connects.
**Pass:** a click selects, a small movement starts nothing, and a double click connects. Double-clicking a
heading folds it and unfolds it again and connects to nothing.
**Failure means:** the 5-pixel threshold in `HostSidebar.axaml.cs` is not doing its job — the same failure
as 2.16 on the other screen, and here it would make the list unusable.
**Failure means:** the 5-pixel threshold in `HostsScreen.axaml.cs` is not doing its job — the same failure
as 2.16 on the other screen, and here it would make the grid unusable. A heading that connects means the
double-tap handler has lost its check that the pointer was over a card.
### 7.8 The highlight clears after a drag that goes nowhere
Drag a host over a heading and release outside the list, or press Escape mid-drag.
Drag a host over a heading and release outside the grid, or press Escape mid-drag.
**Pass:** the wash goes away.
@@ -672,8 +680,8 @@ With host A selected, right-click host B and choose Delete.
**Pass:** no menu opens at all, and the host selection has not moved.
**Failure means:** a menu acting on the selection rather than on the row under the pointer deletes the wrong
machine. `HostSidebarTests` covers both halves headlessly, so this is a confirmation that a real popup
**Failure means:** a menu acting on the selection rather than on the card under the pointer deletes the
wrong machine. `HostGridTests` covers both halves headlessly, so this is a confirmation that a real popup
behaves as the headless one did.
### 7.10 Clicking a host in the palette connects
@@ -800,11 +808,12 @@ side of it remains unmeasurable for the reasons in phase 8.
Open a host's editor with a keychain holding a dozen tags.
**Pass:** the editor pane scrolls, and FORGET HOST KEY is reachable at the bottom of it.
**Pass:** the drawer scrolls, and FORGET HOST KEY is reachable at the bottom of it.
**Failure means:** the pane's MaxHeight is gone or the ScrollViewer is. The layout harness skips anything
inside a ScrollViewer, so from that commit on it certifies the pane fits rather than the fields — it will
tell you the pane is fine while the last button sits below the window.
**Failure means:** the drawer's ScrollViewer is gone. The layout harness skips anything inside one, so from
that commit on it certifies the drawer fits rather than the fields — it will tell you the drawer is fine
while the last button sits below the window. The editor used to carry a MaxHeight of its own because the
host list shared its column; the drawer is alone in its column now, so the height is the window's.
### 9.2 A chip toggles and reads as toggled