diff --git a/README.md b/README.md
index e84efe9..bb3beb7 100644
--- a/README.md
+++ b/README.md
@@ -268,9 +268,14 @@ the reasoning behind it. Sign in, unlock, browse hosts, open a shell, and read t
host-key decisions and the counted delete confirmations are there too, and none of them were softened to
fit 360dp.
-Its interface is the **v2 design**: four destinations in a bottom bar — Hosts, Terminal, Vault and More —
-with snippets, SFTP, S3 buckets, logs and preferences one tap deeper behind the last. That is also where
-the product's palette now comes from, and both heads share it, so the desktop is blue too.
+Its interface is the **v2 design**: four destinations in a bottom bar — Hosts, Terminal, Keychain and More
+— with snippets, SFTP, S3 buckets, logs and preferences one tap deeper behind the last. Both heads are on
+that design now; the desktop's own v2 is a 190-pixel labelled sidebar in place of the icon rail, a centred
+search box in the titlebar, and session tabs as pills. Its light theme is not built — see
+[`docs/design-import-gaps.md`](docs/design-import-gaps.md) — so the application is dark on both.
+
+Widening the sidebar moved the desktop window's minimum from `880x560` to `1016x574`, which leaves every
+screen exactly the width it was designed against.
File transfer **is** here now, in the shape scoped storage allows: one remote pane and the queue, over
either an SFTP host or a bucket. There is no local pane, because there is no browsable local filesystem to
diff --git a/docs/android-port.md b/docs/android-port.md
index f72624b..3754a1b 100644
--- a/docs/android-port.md
+++ b/docs/android-port.md
@@ -105,7 +105,9 @@ desktop head and does not port; an Android head would be a sibling project shari
880×560 minimum, a 54-pixel nav rail, a 268-pixel host sidebar, a two-pane file browser with six columns per
pane, a tab strip, and a layout suite (`DodoSSH.Client.App.Layout.Tests`, 64 tests) whose entire premise is
-that everything fits at that minimum.
+that everything fits at that minimum. (Those are the numbers this audit was taken against. The desktop's own
+v2 has since widened the rail to 190 and the minimum to 1016×574, and the suite is 69 cases — which makes
+the point below larger rather than smaller.)
None of it survives a phone. What an Android client would be is a different product with the same core:
probably a host list, a terminal, and a single-pane file browser, with the keychain, snippets, logs and pins
diff --git a/docs/design-import-gaps.md b/docs/design-import-gaps.md
index 20c9161..d83e296 100644
--- a/docs/design-import-gaps.md
+++ b/docs/design-import-gaps.md
@@ -37,6 +37,38 @@ the chrome, hosts and terminals, file transfer, the vault, teams, and preference
> | A 14px rounded inset around the terminal | Not drawn. The renderer is a native child view composited above everything Avalonia draws, so a rounded frame behind it clips nothing. What the design was after comes from the page's own background, which is the design's `#171A26`. |
> | Instrument Sans / Fira Code | Inter and the system monospace stack, as before and for the same reason — see the note on `MonoFont` in `Palette.axaml`. |
> | A theme toggle (`☀`) in the header, and the desktop v2's light theme | Omitted. There is one theme by decision, and a switch with one position is a control that does nothing. |
+>
+> ## The desktop's v2
+>
+> **DodoSSH v2** — the desktop design in the same project — has since been imported too, dark only. Its
+> light theme is not built: the light accent is `#6D5AE6`, a different hue rather than a tint, so it needs
+> every colour doubled, a theme variant to switch on, the renderer's page switching with it, and contrast
+> checked twice. That is a piece of work, not a setting, and it is separable from the layout.
+>
+> **The window's minimum grew, and by exactly what v2 added.** The 54-pixel icon rail became a 190-pixel
+> labelled sidebar and the chrome went from 72 tall to 86, so `880x560` became `1016x574` — leaving every
+> screen the same `826x464` it was designed against. Four of the tables stop fitting at 690 wide, so
+> widening the sidebar without widening the window would have broken them where the layout suite was not
+> looking.
+>
+> **Buckets became a destination** rather than a toggle inside the files screen, matching the phone: the
+> `HOST` / `BUCKET` pair is gone and `ShellScreen.Buckets` draws the same `TransfersScreen` with the other
+> picker. That also settles an old disagreement — `TotalItemCount` excludes buckets and used to disagree
+> with the list under it, and now counts exactly what the keychain screen shows.
+>
+> | v2 element | What ships instead |
+> | --- | --- |
+> | The **FORWARDING** screen, the `⇄ N forwards` chip on the session strip, and the host detail's FORWARDING card | Nothing, as on the phone and for the same reason. Three surfaces for a feature the SSH layer does not have. |
+> | The host detail's 2×2 card grid — IDENTITY fingerprint, TAGS, LAST SESSION | Not drawn. There is no fingerprint on a stored key, no tag item type, and no last-used timestamp at any layer. The pane keeps the host editor the design has no equivalent of, which is the thing that is actually used. |
+> | The keychain detail's public-key block, USED BY hosts, and **Rotate** | The detail pane as it stands. Copy public key is real and already there; rotation is not a thing this client can do. |
+> | Logs: **FOLLOW**, the severity chips, and `tailing via ssh · journald + files` | The two logs it really has. Same as the phone — these are synced audit records, not a tail, and the footer sentence describes a feature rather than the screen. |
+> | Snippets: **RUNS ON** host tags and a last-run line | The command and the two buttons that name the terminal they type into. |
+> | The session footer: latency, forwards, `utf-8 · xterm-256color` | Omitted. The first two have no source; the third is a constant, and a constant printed as though it were a reading is decoration. |
+> | The terminal as an inset rounded panel | Not drawn, for the reason the phone does not draw it: it is a native child window that composites above everything Avalonia paints, so a rounded frame behind it clips nothing. |
+> | **Split ⌘D** | Still omitted — the renderer stacks panes and shows one; tiling needs a pane geometry it has not got. |
+> | macOS traffic lights, and `⌘K` | The window's own minimise/maximise/close, and `CTRL K`. Development is Windows-first and the chrome is `BorderOnly` for a documented reason. |
+> | No status bar | Kept, and cut down to the one thing the titlebar does not now carry: `Vault.Status`, which is the only channel this application has for saying a save failed or a merge picked a winner. The design is a mock-up of a working afternoon and has nowhere to put a sentence like that. |
+> | The sidebar's five destinations, and a **Team vault** card at its foot | Nine destinations, because Pins, Teams, Import and Preferences are built screens and dropping their entry would strand them. The card is not drawn: it is a second route to a screen already in the list, carrying a seat count nothing here produces. |
Most of it landed. This file is the rest: every element of that design with nothing behind it, which
project each piece would have to land in, and **what the shipped interface does instead**. That last
diff --git a/docs/manual-checks.md b/docs/manual-checks.md
index 97d69da..109c9ba 100644
--- a/docs/manual-checks.md
+++ b/docs/manual-checks.md
@@ -78,8 +78,8 @@ question that cannot be reached.
### 2.1 The fingerprint column is readable end to end
-Connect to two or three hosts, approving each fingerprint. Go to PINS and widen the window to its minimum
-(880px), then to something ordinary.
+Connect to two or three hosts, approving each fingerprint. Go to Pins and narrow the window to its minimum
+(1016px since the v2 sidebar; it was 880 while the rail was 54 wide), then widen it to something ordinary.
**Pass:** the full `SHA256:…` is on screen at both sizes, never cut off and never ellipsised.
diff --git a/src/DodoSSH.Client.App/App.axaml b/src/DodoSSH.Client.App/App.axaml
index fe071d5..a0055e1 100644
--- a/src/DodoSSH.Client.App/App.axaml
+++ b/src/DodoSSH.Client.App/App.axaml
@@ -48,7 +48,7 @@
-
+
@@ -87,7 +87,7 @@
@@ -101,8 +101,12 @@
+
+
+
+
+
+
+
+
+
+
+
+
-
-
-
+
-
+ ToolTip.Tip="Preferences, and this machine's device key">
+
+
+
+
+
diff --git a/src/DodoSSH.Client.App/Views/SignOutCard.axaml b/src/DodoSSH.Client.App/Views/SignOutCard.axaml
index fb15bcb..8e5d532 100644
--- a/src/DodoSSH.Client.App/Views/SignOutCard.axaml
+++ b/src/DodoSSH.Client.App/Views/SignOutCard.axaml
@@ -34,7 +34,9 @@
BorderThickness="1" CornerRadius="4" Padding="10,8"
IsVisible="{Binding HasLiveSessions, FallbackValue=False}">
-
+
diff --git a/src/DodoSSH.Client.App/Views/StatusBar.axaml b/src/DodoSSH.Client.App/Views/StatusBar.axaml
index e2ebfcf..edb1034 100644
--- a/src/DodoSSH.Client.App/Views/StatusBar.axaml
+++ b/src/DodoSSH.Client.App/Views/StatusBar.axaml
@@ -15,11 +15,21 @@
The last message is the load-bearing one, and it is why this bar is worth having at all: the vault's
status line used to be a cramped label in the account bar, and everything the application has to say
about a save, a sync, a refusal or a conflict goes through it.
+
+ ── v2 ────────────────────────────────────────────────────────────────────────────────────────────────
+ The design has no status bar. Two of the three things this one carried moved into the titlebar with it —
+ the sync word is up there beside its dot, and the shortcut hint is inside the search box that uses it —
+ so those two are gone from here rather than printed twice.
+
+ The third did not move, and is why this bar survives the design that deletes it. Vault.Status is the one
+ channel this application has for saying that a save failed, that a sync was refused, that a merge picked
+ a winner. The design is a mock-up of a working afternoon and has nowhere for a sentence like that to go;
+ dropping the bar would have meant dropping the sentence, or repeating it on six screens.
-->
-
+
-
-
-
-
diff --git a/src/DodoSSH.Client.App/Views/TerminalTabs.axaml b/src/DodoSSH.Client.App/Views/TerminalTabs.axaml
index b727bee..2bc3ed4 100644
--- a/src/DodoSSH.Client.App/Views/TerminalTabs.axaml
+++ b/src/DodoSSH.Client.App/Views/TerminalTabs.axaml
@@ -23,9 +23,17 @@
An ItemsControl of buttons rather than a TabStrip, because the selection lives on the shell — a tab
outlives the vault that opened it — and a strip that owned its own selection would be a second copy of
that state.
+
+ ── v2 ────────────────────────────────────────────────────────────────────────────────────────────────
+ Tabs became pills: taller, rounded, each with its own outline, on the sidebar's surface rather than the
+ chrome's. The design also puts a "Hosts" pill at the head of this strip and hides the sidebar while a
+ session is showing, so that pill is the only way back. This window keeps the sidebar up instead — it is
+ beside both surfaces rather than inside one — so the way back from a terminal is every destination at
+ once rather than a single pill leading to one of them. That makes the design's pill redundant, and a
+ redundant control on a strip this narrow is one the user has to rule out before finding the tabs.
-->
-
-
-
+
-
-
+
+
-
+
+
-
-
+
-
-
+
-
-
+
+
diff --git a/src/DodoSSH.Client.App/Views/TransfersScreen.axaml b/src/DodoSSH.Client.App/Views/TransfersScreen.axaml
index 47a068c..c62631a 100644
--- a/src/DodoSSH.Client.App/Views/TransfersScreen.axaml
+++ b/src/DodoSSH.Client.App/Views/TransfersScreen.axaml
@@ -77,25 +77,24 @@
-
+
+ Margin="0,0,12,0" IsVisible="{Binding ShowsHostPicker}" />
+
-
-
-
-
-
+
diff --git a/src/DodoSSH.Client.App/Views/UnlockCard.axaml b/src/DodoSSH.Client.App/Views/UnlockCard.axaml
index 6a45289..1e640f9 100644
--- a/src/DodoSSH.Client.App/Views/UnlockCard.axaml
+++ b/src/DodoSSH.Client.App/Views/UnlockCard.axaml
@@ -70,7 +70,9 @@
BorderThickness="1" CornerRadius="4" Padding="10,8"
IsVisible="{Binding HasLiveSessions, FallbackValue=False}">
-
+
diff --git a/src/DodoSSH.Client.Shell/ViewModels/MainWindowViewModel.cs b/src/DodoSSH.Client.Shell/ViewModels/MainWindowViewModel.cs
index 761c1ea..70a5b6f 100644
--- a/src/DodoSSH.Client.Shell/ViewModels/MainWindowViewModel.cs
+++ b/src/DodoSSH.Client.Shell/ViewModels/MainWindowViewModel.cs
@@ -2515,17 +2515,23 @@ internal sealed partial class MainWindowViewModel : ObservableObject, IAsyncDisp
[RelayCommand]
private void ShowFiles(RemoteKind kind)
{
+ // Refusing means staying put, not arriving somewhere and saying no. Moving Screen anyway would put
+ // the S3 entry in the sidebar over a screen still listing an SFTP host — two pieces of chrome
+ // disagreeing about where you are, which is worse than the navigation simply not happening.
if (Transfers.IsConnected && Transfers.Remote != kind)
{
Transfers.Status = kind is RemoteKind.Bucket
? "An SFTP session is open. Close it before opening a bucket."
: "A bucket is open. Close it before connecting to a host.";
- }
- else
- {
- Transfers.Remote = kind;
+
+ // Still show the screen the open session belongs to, so the message is somewhere it can be
+ // read — the button that was pressed is in the sidebar, which is on screen either way.
+ Screen = Transfers.Remote is RemoteKind.Bucket ? ShellScreen.Buckets : ShellScreen.Transfers;
+ Surface = ShellSurface.Page;
+ return;
}
+ Transfers.Remote = kind;
Screen = kind is RemoteKind.Bucket ? ShellScreen.Buckets : ShellScreen.Transfers;
Surface = ShellSurface.Page;
}
diff --git a/src/DodoSSH.Client.Shell/ViewModels/TransfersViewModel.cs b/src/DodoSSH.Client.Shell/ViewModels/TransfersViewModel.cs
index 98bdce6..489e834 100644
--- a/src/DodoSSH.Client.Shell/ViewModels/TransfersViewModel.cs
+++ b/src/DodoSSH.Client.Shell/ViewModels/TransfersViewModel.cs
@@ -557,9 +557,10 @@ internal sealed partial class TransfersViewModel : ObservableObject, IAsyncDispo
TypedPassword = string.Empty;
}
- /// Shows one of the two kinds of remote in the picker.
- [RelayCommand]
- private void ShowRemote(RemoteKind kind) => Remote = kind;
+ // ShowRemoteCommand was here, and it went with the toggle that invoked it. Which kind of remote this
+ // screen offers is a destination now rather than a control on the screen — both heads reach it through
+ // MainWindowViewModel.ShowFiles, which sets Remote directly because it has a refusal to make first.
+ // Keeping the command would have left one nothing could invoke.
/// Opens the chosen remote, whichever kind it is.
[RelayCommand]
diff --git a/tests/DodoSSH.Client.App.Layout.Tests/LayoutHarness.cs b/tests/DodoSSH.Client.App.Layout.Tests/LayoutHarness.cs
index 6c37c7e..cbac8d9 100644
--- a/tests/DodoSSH.Client.App.Layout.Tests/LayoutHarness.cs
+++ b/tests/DodoSSH.Client.App.Layout.Tests/LayoutHarness.cs
@@ -31,28 +31,28 @@ internal static class LayoutHarness
/// two constants still match the XAML, so the harness cannot quietly start measuring a window larger
/// than the one a user is allowed to drag to.
///
- internal const double MinimumWidth = 880;
+ internal const double MinimumWidth = 1016;
///
- internal const double MinimumHeight = 560;
+ internal const double MinimumHeight = 574;
/// The host sidebar's fixed width, from the hosts screen's ColumnDefinitions.
internal const double HostSidebarWidth = 268;
/// The nav rail's fixed width, from NavRail.axaml.
- internal const double NavRailWidth = 54;
+ internal const double NavRailWidth = 190;
///
/// What the titlebar, the tab strip and the status bar take off the window before any screen gets a
/// pixel.
///
///
- /// All three are fixed heights declared in their own markup — 38, 34 and 24 — rather than shapes that
+ /// All three are fixed heights declared in their own markup — 44, 42 and 24 — rather than shapes that
/// grow with their contents, which is what makes stating them here honest. Three tests hold the three
/// controls to those numbers, so the budget below cannot drift away from what the window actually
/// leaves.
///
- internal const double TitleBarHeight = 38;
+ internal const double TitleBarHeight = 44;
///
internal const double StatusBarHeight = 24;
@@ -63,10 +63,10 @@ internal static class LayoutHarness
///
/// It comes off every screen, not just the hosts screen, which is the layout consequence of the strip
/// spanning the window. The strip does not collapse when there are no tabs — a row of chrome that came
- /// and went would move every screen up and down by 34 pixels each time the last tab closed — so this is
+ /// and went would move every screen up and down by 42 pixels each time the last tab closed — so this is
/// a fixed cost rather than a conditional one, and the budget can be a constant.
///
- internal const double TerminalTabsHeight = 34;
+ internal const double TerminalTabsHeight = 42;
///
/// What a setup card leaves its contents: its maximum width, less the padding on both sides.
diff --git a/tests/DodoSSH.Client.App.Layout.Tests/ScreenLayoutTests.cs b/tests/DodoSSH.Client.App.Layout.Tests/ScreenLayoutTests.cs
index 6162d27..63cd9cc 100644
--- a/tests/DodoSSH.Client.App.Layout.Tests/ScreenLayoutTests.cs
+++ b/tests/DodoSSH.Client.App.Layout.Tests/ScreenLayoutTests.cs
@@ -905,7 +905,7 @@ public sealed class ScreenLayoutTests : IAsyncLifetime
/// silently dropping off the bottom would still pass every other assertion here.
///
[Fact]
- public async Task TheNavRailHoldsEightDestinationsAtTheWindowsMinimum()
+ public async Task TheNavRailHoldsNineDestinationsAtTheWindowsMinimum()
{
await LayoutHarness.OnTheUiThreadAsync(
() =>
@@ -918,17 +918,18 @@ public sealed class ScreenLayoutTests : IAsyncLifetime
{
var buttons = rail.GetVisualDescendants().OfType().ToList();
- buttons.Count.ShouldBe(8, "one per screen the rail reaches");
+ buttons.Count.ShouldBe(9, "one per screen the rail reaches");
foreach (var button in buttons)
{
button.Bounds.Height.ShouldBeGreaterThan(20);
- // One pixel narrower than the rail, because the rail draws its own divider down its
- // right edge and that comes out of the content. Stated exactly rather than as a
- // lower bound: a button that stopped filling the rail would leave a dead strip
- // beside every destination, which is precisely the kind of near-miss a bound hides.
- button.Bounds.Width.ShouldBe(LayoutHarness.NavRailWidth - 1);
+ // 190 wide, less the divider down the rail's right edge, less the 8 of inset on
+ // each side that v2 gives the rows so a filled one reads as a rounded row rather
+ // than as a full-width band. Stated exactly rather than as a lower bound: a button
+ // that stopped filling the row would leave a dead strip beside a destination, which
+ // is precisely the kind of near-miss a bound hides.
+ button.Bounds.Width.ShouldBe(LayoutHarness.NavRailWidth - 1 - 16);
}
LayoutHarness.Unreachable(window).ShouldBeEmpty();