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
@@ -79,21 +79,22 @@
<!--
The screen names whichever remote it is offering, because since v2 it is reached as two
destinations rather than one: SFTP and S3 are separate entries in the sidebar and this control
draws both. A single "FILES" heading over a bucket picker would name neither of them.
destinations rather than one: SFTP and S3 are separate tabs in the strip — they were rail entries
until v3 — and this control draws both. A single "FILES" heading over a bucket picker would name
neither of them.
-->
<TextBlock Grid.Column="0" Classes="mono" Text="SFTP" FontSize="11" FontWeight="SemiBold"
<TextBlock Grid.Column="0" Classes="mono" Text="SFTP" FontSize="12" FontWeight="SemiBold"
LetterSpacing="1" Foreground="{StaticResource Text}" VerticalAlignment="Center"
Margin="0,0,12,0" IsVisible="{Binding ShowsHostPicker}" />
<TextBlock Grid.Column="0" Classes="mono" Text="S3" FontSize="11" FontWeight="SemiBold"
<TextBlock Grid.Column="0" Classes="mono" Text="S3" FontSize="12" FontWeight="SemiBold"
LetterSpacing="1" Foreground="{StaticResource Text}" VerticalAlignment="Center"
Margin="0,0,12,0" IsVisible="{Binding ShowsBucketPicker}" />
<!--
The HOST / BUCKET pair that used to sit here is gone: which sort of remote this screen offers is
now the destination you chose in the sidebar, and a toggle that silently moved you to the other
one would leave the lit sidebar entry naming a screen you are no longer on. What sets it is
ShowFiles on the shell, which is what the sidebar calls.
now the tab you chose, and a toggle that silently moved you to the other one would leave the lit
tab naming a screen you are no longer on. What sets it is ShowFiles on the shell, which is what
the tab calls.
-->
<ComboBox Grid.Column="2" ItemsSource="{Binding Hosts}"
@@ -104,9 +105,9 @@
<ComboBox.ItemTemplate>
<DataTemplate x:DataType="vm:HostRowViewModel">
<StackPanel>
<TextBlock Classes="mono" Text="{Binding Label}" FontSize="11"
<TextBlock Classes="mono" Text="{Binding Label}" FontSize="12"
Foreground="{StaticResource Text}" />
<TextBlock Classes="mono" Text="{Binding Address}" FontSize="9"
<TextBlock Classes="mono" Text="{Binding Address}" FontSize="10"
Foreground="{StaticResource TextFaint}" />
</StackPanel>
</DataTemplate>
@@ -121,9 +122,9 @@
<ComboBox.ItemTemplate>
<DataTemplate x:DataType="vm:ObjectStoreRowViewModel">
<StackPanel>
<TextBlock Classes="mono" Text="{Binding Label}" FontSize="11"
<TextBlock Classes="mono" Text="{Binding Label}" FontSize="12"
Foreground="{StaticResource Text}" />
<TextBlock Classes="mono" Text="{Binding Description}" FontSize="9"
<TextBlock Classes="mono" Text="{Binding Description}" FontSize="10"
Foreground="{StaticResource TextFaint}" />
</StackPanel>
</DataTemplate>
@@ -160,7 +161,7 @@
mostly read before connecting; it is not now, because refusing to switch between SFTP and S3 while
a session is live reports itself here, which is precisely when the chip is on screen.
-->
<TextBlock Grid.Column="6" Classes="hint" Text="{Binding Status}" FontSize="10.5"
<TextBlock Grid.Column="6" Classes="hint" Text="{Binding Status}" FontSize="11.5"
Margin="12,0,0,0" VerticalAlignment="Center" TextTrimming="CharacterEllipsis"
TextWrapping="NoWrap" />
@@ -219,10 +220,10 @@
<Button Classes="flat" Padding="3,1"
Command="{Binding $parent[ItemsControl].((vm:TransfersViewModel)DataContext).GoLocalCommand}"
CommandParameter="{Binding Path}">
<TextBlock Classes="mono" Text="{Binding Name}" FontSize="10"
<TextBlock Classes="mono" Text="{Binding Name}" FontSize="11"
Foreground="{StaticResource TextDim}" />
</Button>
<TextBlock Classes="mono" Text="" FontSize="10"
<TextBlock Classes="mono" Text="" FontSize="11"
Foreground="{StaticResource TextFaint}" VerticalAlignment="Center" />
</StackPanel>
</DataTemplate>
@@ -230,9 +231,9 @@
</ItemsControl>
<Grid Grid.Row="2" ColumnDefinitions="2,*,84,110" Margin="0,2,12,4">
<TextBlock Grid.Column="1" Classes="label" Text="NAME" FontSize="8.5" Margin="12,0,8,0" />
<TextBlock Grid.Column="2" Classes="label" Text="SIZE" FontSize="8.5" />
<TextBlock Grid.Column="3" Classes="label" Text="MODIFIED" FontSize="8.5" />
<TextBlock Grid.Column="1" Classes="label" Text="NAME" FontSize="9.5" Margin="12,0,8,0" />
<TextBlock Grid.Column="2" Classes="label" Text="SIZE" FontSize="9.5" />
<TextBlock Grid.Column="3" Classes="label" Text="MODIFIED" FontSize="9.5" />
</Grid>
<ListBox Grid.Row="3" x:Name="LocalList" ItemsSource="{Binding LocalEntries}"
@@ -242,18 +243,18 @@
<Grid ColumnDefinitions="2,*,84,110" Margin="0,5,12,5">
<Border Grid.Column="0" Classes="rowmark" />
<TextBlock Grid.Column="1" Classes="mono entry" Classes.dir="{Binding IsNavigable}"
Text="{Binding Name}" FontSize="11"
Text="{Binding Name}" FontSize="12"
Margin="12,0,8,0" TextTrimming="CharacterEllipsis" />
<TextBlock Grid.Column="2" Classes="mono" Text="{Binding Size}" FontSize="9.5"
<TextBlock Grid.Column="2" Classes="mono" Text="{Binding Size}" FontSize="10.5"
Foreground="{StaticResource TextDim}" VerticalAlignment="Center" />
<TextBlock Grid.Column="3" Classes="mono" Text="{Binding Modified}" FontSize="9.5"
<TextBlock Grid.Column="3" Classes="mono" Text="{Binding Modified}" FontSize="10.5"
Foreground="{StaticResource TextFaint}" VerticalAlignment="Center" />
</Grid>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
<TextBlock Grid.Row="3" Classes="hint" FontSize="11" Margin="24" MaxWidth="260"
<TextBlock Grid.Row="3" Classes="hint" FontSize="12" Margin="24" MaxWidth="260"
HorizontalAlignment="Center" VerticalAlignment="Center" TextAlignment="Center"
Text="Nothing in this folder. Use the trail above to go somewhere else."
IsVisible="{Binding !HasLocalEntries}" />
@@ -322,12 +323,12 @@
BorderBrush="{StaticResource DangerSoft}" BorderThickness="0,0,0,1"
IsVisible="{Binding IsConfirmingRemoteDeletion}">
<StackPanel Spacing="7">
<TextBlock Classes="heading" FontSize="13" TextWrapping="Wrap"
<TextBlock Classes="heading" FontSize="14" TextWrapping="Wrap"
Text="{Binding PendingRemoteDeletion.Question}" />
<SelectableTextBlock Classes="mono" FontSize="10.5" TextWrapping="Wrap"
<SelectableTextBlock Classes="mono" FontSize="11.5" TextWrapping="Wrap"
Foreground="{StaticResource Danger}"
Text="{Binding PendingRemoteDeletion.FullPath}" />
<TextBlock Foreground="{StaticResource WarnText}" FontSize="11" TextWrapping="Wrap"
<TextBlock Foreground="{StaticResource WarnText}" FontSize="12" TextWrapping="Wrap"
Text="{Binding PendingRemoteDeletion.Consequence}" />
<StackPanel Orientation="Horizontal" Spacing="8">
<Button Classes="danger" Content="DELETE ON THE HOST"
@@ -347,12 +348,12 @@
<ItemsControl.ItemTemplate>
<DataTemplate x:DataType="vm:CrumbViewModel">
<StackPanel Orientation="Horizontal">
<TextBlock Classes="mono" Text="/" FontSize="10"
<TextBlock Classes="mono" Text="/" FontSize="11"
Foreground="{StaticResource TextFaint}" VerticalAlignment="Center" />
<Button Classes="flat" Padding="3,1"
Command="{Binding $parent[ItemsControl].((vm:TransfersViewModel)DataContext).GoRemoteCommand}"
CommandParameter="{Binding Path}">
<TextBlock Classes="mono" Text="{Binding Name}" FontSize="10"
<TextBlock Classes="mono" Text="{Binding Name}" FontSize="11"
Foreground="{StaticResource TextDim}" />
</Button>
</StackPanel>
@@ -373,10 +374,10 @@
</Grid>
<Grid Grid.Row="3" ColumnDefinitions="2,*,84,110,92" Margin="0,2,12,4">
<TextBlock Grid.Column="1" Classes="label" Text="NAME" FontSize="8.5" Margin="12,0,8,0" />
<TextBlock Grid.Column="2" Classes="label" Text="SIZE" FontSize="8.5" />
<TextBlock Grid.Column="3" Classes="label" Text="MODIFIED" FontSize="8.5" />
<TextBlock Grid.Column="4" Classes="label" Text="PERMS" FontSize="8.5" />
<TextBlock Grid.Column="1" Classes="label" Text="NAME" FontSize="9.5" Margin="12,0,8,0" />
<TextBlock Grid.Column="2" Classes="label" Text="SIZE" FontSize="9.5" />
<TextBlock Grid.Column="3" Classes="label" Text="MODIFIED" FontSize="9.5" />
<TextBlock Grid.Column="4" Classes="label" Text="PERMS" FontSize="9.5" />
</Grid>
<ListBox Grid.Row="4" x:Name="RemoteList" ItemsSource="{Binding RemoteEntries}"
@@ -387,14 +388,14 @@
<Border Grid.Column="0" Classes="rowmark" />
<TextBlock Grid.Column="1" Classes="mono entry" Classes.dir="{Binding IsNavigable}"
Classes.exec="{Binding IsExecutable}"
Text="{Binding Name}" FontSize="11"
Text="{Binding Name}" FontSize="12"
Margin="12,0,8,0" TextTrimming="CharacterEllipsis" />
<TextBlock Grid.Column="2" Classes="mono" Text="{Binding Size}" FontSize="9.5"
<TextBlock Grid.Column="2" Classes="mono" Text="{Binding Size}" FontSize="10.5"
Foreground="{StaticResource TextDim}" VerticalAlignment="Center" />
<TextBlock Grid.Column="3" Classes="mono" Text="{Binding Modified}" FontSize="9.5"
<TextBlock Grid.Column="3" Classes="mono" Text="{Binding Modified}" FontSize="10.5"
Foreground="{StaticResource TextFaint}" VerticalAlignment="Center" />
<TextBlock Grid.Column="4" Classes="mono perms" Classes.loose="{Binding IsWorldWritable}"
Text="{Binding Permissions}" FontSize="9.5" VerticalAlignment="Center" />
Text="{Binding Permissions}" FontSize="10.5" VerticalAlignment="Center" />
</Grid>
</DataTemplate>
</ListBox.ItemTemplate>
@@ -403,10 +404,10 @@
<StackPanel Grid.Row="4" Spacing="10" Margin="24" MaxWidth="300"
HorizontalAlignment="Center" VerticalAlignment="Center"
IsVisible="{Binding !HasRemoteEntries}">
<TextBlock Classes="hint" FontSize="11" TextAlignment="Center"
<TextBlock Classes="hint" FontSize="12" TextAlignment="Center"
Text="Connect to a host to browse its files. This opens its own SFTP connection, so the host records a second login — it is not the same channel as a terminal."
IsVisible="{Binding !IsConnected}" />
<TextBlock Classes="hint" FontSize="11" TextAlignment="Center"
<TextBlock Classes="hint" FontSize="12" TextAlignment="Center"
Text="Nothing in this directory."
IsVisible="{Binding IsConnected}" />
</StackPanel>
@@ -424,7 +425,7 @@
<Border Grid.Row="0" Grid.RowSpan="5" IsHitTestVisible="False"
Background="{StaticResource DangerWash}" BorderBrush="{StaticResource DangerSoft}"
BorderThickness="2" IsVisible="{Binding IsRemoteDropRefused}">
<TextBlock Classes="hint" Text="Connect to a host first." FontSize="11"
<TextBlock Classes="hint" Text="Connect to a host first." FontSize="12"
Foreground="{StaticResource Danger}"
HorizontalAlignment="Center" VerticalAlignment="Center" />
</Border>
@@ -441,7 +442,7 @@
<Border Grid.Row="0" Padding="12,7">
<Grid ColumnDefinitions="Auto,Auto,*,Auto,Auto">
<TextBlock Grid.Column="0" Classes="label" Text="TRANSFERS" VerticalAlignment="Center" />
<TextBlock Grid.Column="1" Classes="mono" FontSize="9.5" Margin="10,0,0,0"
<TextBlock Grid.Column="1" Classes="mono" FontSize="10.5" Margin="10,0,0,0"
Foreground="{StaticResource TextFaint}" VerticalAlignment="Center"
Text="one at a time · nothing lands at its final name until it is complete" />
</Grid>
@@ -449,7 +450,7 @@
<ScrollViewer Grid.Row="1">
<StackPanel>
<TextBlock Classes="hint" FontSize="10.5" Margin="12,4,12,14"
<TextBlock Classes="hint" FontSize="11.5" Margin="12,4,12,14"
IsVisible="{Binding !HasTransfers}"
Text="Nothing queued. Choose a file in either pane and press the arrow pointing the way you want it to go." />
@@ -457,12 +458,12 @@
<ItemsControl.ItemTemplate>
<DataTemplate x:DataType="vm:TransferRowViewModel">
<Grid ColumnDefinitions="16,150,*,190,Auto" Margin="12,4">
<TextBlock Grid.Column="0" Classes="mono" Text="{Binding Arrow}" FontSize="11"
<TextBlock Grid.Column="0" Classes="mono" Text="{Binding Arrow}" FontSize="12"
Foreground="{StaticResource Accent}" VerticalAlignment="Center" />
<StackPanel Grid.Column="1" Margin="0,0,8,0">
<TextBlock Classes="mono" Text="{Binding Name}" FontSize="10.5"
<TextBlock Classes="mono" Text="{Binding Name}" FontSize="11.5"
Foreground="{StaticResource Text}" TextTrimming="CharacterEllipsis" />
<TextBlock Classes="mono" Text="{Binding Path}" FontSize="9"
<TextBlock Classes="mono" Text="{Binding Path}" FontSize="10"
Foreground="{StaticResource TextFaint}"
TextTrimming="CharacterEllipsis" />
</StackPanel>
@@ -472,7 +473,7 @@
Foreground="{StaticResource Accent}"
Background="{StaticResource Raised}" />
<TextBlock Grid.Column="3" Classes="mono" Text="{Binding Progress}" FontSize="9.5"
<TextBlock Grid.Column="3" Classes="mono" Text="{Binding Progress}" FontSize="10.5"
Margin="10,0" VerticalAlignment="Center"
TextTrimming="CharacterEllipsis"
Foreground="{StaticResource TextDim}" />
@@ -517,7 +518,7 @@
<TextBlock Classes="heading" Text="Check this host's fingerprint" />
<TextBlock Classes="hint"
Text="This host has not been seen before. Compare the fingerprint with what the operator published. Trusting it here also trusts it for terminals, and on your other machines." />
<SelectableTextBlock Classes="mono" FontSize="11" Foreground="{StaticResource Text}"
<SelectableTextBlock Classes="mono" FontSize="12" Foreground="{StaticResource Text}"
TextWrapping="Wrap" Text="{Binding PendingHostKey.Fingerprint}" />
<StackPanel Orientation="Horizontal" Spacing="8">
<Button Classes="accent" Content="TRUST AND CONNECT" Command="{Binding TrustHostKeyCommand}" />