From 208443b93253cbf3c72a9b8abe57b19e2894ab22 Mon Sep 17 00:00:00 2001 From: Jaap-Jan de Wit | DodoTech Date: Mon, 3 Aug 2026 15:08:48 +0200 Subject: [PATCH] Move SFTP and S3 into the tab strip, and the host list into a card grid MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- README.md | 16 +- docs/design-import-gaps.md | 46 +- docs/manual-checks.md | 49 +- src/DodoSSH.Client.App/App.axaml | 148 ++++- .../Views/ConfirmDeleteCard.axaml | 20 +- .../Views/ConfirmDeleteCard.axaml.cs | 5 +- .../Views/ConnectingCard.axaml | 10 +- src/DodoSSH.Client.App/Views/HostDrawer.axaml | 334 +++++++++++ .../Views/HostDrawer.axaml.cs | 23 + .../Views/HostSidebar.axaml | 366 ------------ .../Views/HostSidebar.axaml.cs | 340 ----------- .../Views/HostsScreen.axaml | 542 +++++++++++------- .../Views/HostsScreen.axaml.cs | 356 +++++++++++- .../Views/ImportScreen.axaml | 28 +- .../Views/KnownHostsScreen.axaml | 38 +- src/DodoSSH.Client.App/Views/LogsScreen.axaml | 54 +- src/DodoSSH.Client.App/Views/MainWindow.axaml | 58 +- src/DodoSSH.Client.App/Views/NavRail.axaml | 44 +- .../Views/NotBuiltScreen.axaml | 10 +- .../Views/PreferencesScreen.axaml | 42 +- .../Views/QuickConnect.axaml | 14 +- .../Views/SignOutCard.axaml | 10 +- .../Views/SnippetsScreen.axaml | 28 +- src/DodoSSH.Client.App/Views/StatusBar.axaml | 4 +- .../Views/TeamsScreen.axaml | 36 +- .../Views/TerminalTabs.axaml | 107 +++- src/DodoSSH.Client.App/Views/TitleBar.axaml | 20 +- .../Views/TransfersScreen.axaml | 89 +-- src/DodoSSH.Client.App/Views/UnlockCard.axaml | 6 +- .../Views/UnlockCard.axaml.cs | 2 +- .../Views/VaultScreen.axaml | 80 +-- src/DodoSSH.Client.Shell/Theme/Palette.axaml | 20 +- .../ViewModels/MainWindowViewModel.cs | 59 ++ .../ViewModels/VaultViewModel.cs | 169 +++++- .../{HostSidebarTests.cs => HostGridTests.cs} | 79 ++- .../LayoutHarness.cs | 9 +- .../ScreenLayoutTests.cs | 338 +++++++---- .../TerminalTabsTests.cs | 85 +++ 38 files changed, 2252 insertions(+), 1432 deletions(-) create mode 100644 src/DodoSSH.Client.App/Views/HostDrawer.axaml create mode 100644 src/DodoSSH.Client.App/Views/HostDrawer.axaml.cs delete mode 100644 src/DodoSSH.Client.App/Views/HostSidebar.axaml delete mode 100644 src/DodoSSH.Client.App/Views/HostSidebar.axaml.cs rename tests/DodoSSH.Client.App.Layout.Tests/{HostSidebarTests.cs => HostGridTests.cs} (67%) diff --git a/README.md b/README.md index 7bb53b7..112b81e 100644 --- a/README.md +++ b/README.md @@ -148,8 +148,9 @@ dotnet run --project src/DodoSSH.Client.App In the app, enter `http://localhost:5233` as the server. Your browser opens for sign-in — the realm ships `alice` / `alice` — then choose a vault passphrase and **write down the recovery code**, which cannot be skipped and cannot be recovered from the server. You can then add a host and open a shell on it — double-click -it in the sidebar, or select it and press **CONNECT**, which is the same command with the password box beside -it. Keycloak's admin console is at `http://localhost:18080` (`admin` / `admin`). +its card, or select it and press **CONNECT** in the drawer that opens beside the grid, which is the same +command with the password box above it. Keycloak's admin console is at `http://localhost:18080` +(`admin` / `admin`). You can also add an SSH key, which is stored in the vault like a host and synced the same way: paste the private key, then edit a host and pick that key from its **key** dropdown. From then on that host @@ -270,13 +271,20 @@ fit 360dp. 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 +that design now; the desktop's own v2 is a 190-pixel labelled nav rail 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 +Widening the rail moved the desktop window's minimum from `880x560` to `1016x574`, which leaves every screen exactly the width it was designed against. +A third desktop pass has since moved the furniture. The tab strip belongs to the window rather than to the +terminal: **Vaults**, **SFTP** and **S3** are fixed tabs at its head and open terminals follow them, which +took SFTP and S3 out of the nav rail — they are the two destinations you stay in while something runs. The +hosts screen became a grid of cards, groups above and hosts below, with a right-hand drawer for whichever +host is selected and for both editors; the 268-pixel host sidebar is gone. Text is white rather than the +design's blue-tinted `#E3E7F4`, and the type scale is a point larger. + 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 put in one — moving a file *in* from the phone needs the system document picker and is the next piece of diff --git a/docs/design-import-gaps.md b/docs/design-import-gaps.md index 336c364..078e601 100644 --- a/docs/design-import-gaps.md +++ b/docs/design-import-gaps.md @@ -68,7 +68,38 @@ the chrome, hosts and terminals, file transfer, the vault, teams, and preference > | **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. | +> | The sidebar's five destinations, and a **Team vault** card at its foot | Seven destinations, because Pins, Teams and Preferences are built screens and dropping their entry would strand them — and two fewer than v2 shipped with, because SFTP and S3 became tabs; see v3 below. The card is not drawn: it is a second route to a screen already in the list, carrying a seat count nothing here produces. | +> +> ## The desktop's v3 +> +> A third pass, and the smallest of the three: it moves furniture rather than adding screens. Nothing in it +> needed a layer below `client-app`, which is why it has no table of its own — there was nothing to omit. +> +> **The tab strip became the window's, not the terminal's.** Three fixed tabs sit at its head — Vaults, +> SFTP, S3 — and terminal tabs 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. The rail is drawn under Vaults alone, so SFTP, S3 and a terminal +> each get the full window width instead of `826`. See `MainWindowViewModel.IsVaultsTab` for why the tab is +> a page test rather than a fourth `ShellSurface`. +> +> **The hosts screen became a grid of cards** — groups above, hosts below — and the 268-pixel host sidebar +> went with it. That column was choosing among forty machines *and* editing one of them at two-thirds +> width; the grid took the first job at full width and a 304-pixel right-hand drawer took the second. The +> drawer collapses when nothing is selected, which is most of the time. Pressing a group card narrows the +> grid to that group; `SHOW ALL` is the way back. +> +> **The type scale went up a point and the text ramp went white.** `#E3E7F4` was 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. Every step of the ramp moved with the top, so the intervals the design +> chose are kept. Both are in the shared palette, so the phone has them too. +> +> | v3 element | What ships instead | +> | --- | --- | +> | The hosts toolbar's view-mode switch (grid / list / table) | One view. A switch between three layouts where only one is built is two disabled buttons. | +> | The hosts toolbar's tag filter, calendar and share control | Omitted. There are no saved filters, nothing in a vault carries a date, and sharing outside a team vault does not exist. Tags are real and are searched by being read off the cards. | +> | **Serial** beside **Terminal** in the toolbar | Omitted. Every session here is an SSH channel; a serial transport is not a button, it is a second session kind. | +> | The strip's tabs inside the titlebar row, with a hamburger | A row of its own under the titlebar. The titlebar already carries the drag region, the search box, the sync light and three window controls, and the strip scrolls — putting both on one 44-pixel row would make the thing that scrolls fight the thing that drags. | +> | A count on the S3 tab | Omitted. The rail entry it replaces carried one; a number on two of five tabs reads as a fact about those two rather than as the tab's own state, and a terminal tab has nothing to count. The count is on the S3 screen. | 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 @@ -219,7 +250,7 @@ caption buttons and window title drawn on top of the application's own — two s | Pane header `aes256-gcm` | client-ssh | **The closest miss on this list.** `SshNetConnection` holds the `SshClient`, so `ConnectionInfo.CurrentServerEncryption` is right there — it just is not on `ISshConnection` or surfaced by `TerminalWorkspace`. | Omitted; the tab strip shows the account and endpoint actually dialled. | | Pane header showing the running command and `following` | client-ssh | The host moves opaque bytes and never parses terminal output. Would need shell integration (OSC 133) on the remote. | Omitted. | | A `local · zsh` tab | client-ssh | Every session here is an SSH channel. Needs ConPTY and a second session kind. | Omitted. | -| Tab strip `+` button | ui | Not missing so much as redundant: the real operation is *select a host, press Connect*, which the sidebar already is. | Omitted. Connect opens a tab; Ctrl+K opens one by name. | +| Tab strip `+` button | ui | Not missing so much as redundant: the real operation is *select a host, press Connect*, which the hosts grid already is. | **Shipped**, as the palette rather than a menu: it opens what Ctrl+K opens, so the strip and the shortcut are one way of doing one thing. A `MenuFlyout` offering "SSH" and "local shell" is the nicer answer and is not verifiably safe above the terminal's native child window — and there is no local shell to offer. | | Terminal font size (`--termfs`, 11–16px) | client-storage | See preferences. | Fixed at the renderer's 13px. | --- @@ -356,10 +387,13 @@ lists the rest as absent rather than omitting it silently. ## Two things the import changed on purpose -**Hosts left the vault column.** They have their own sidebar beside the terminal, and the vault screen -holds keys, passwords and pinned host keys. This follows the design, and it is also the better split: the -host list is what you look at while you work, and the rest is what you go and manage. `VaultSection` lost -its `Hosts` member and gained `All`. +**Hosts left the vault column.** They have their own screen and the vault screen holds keys, passwords and +pinned host keys. This follows the design, and it is also the better split: the host list is what you look +at while you work, and the rest is what you go and manage. `VaultSection` lost its `Hosts` member and +gained `All`. + +That screen was a 268-pixel sidebar beside the terminal when this paragraph was written, and v3 made it a +grid of cards with a drawer — see above. The split it describes did not change; only which half is wide. **Tabs moved to the shell, not the vault.** Locking disposes the vault and deliberately leaves shells running, so a tab list rebuilt per unlock would lose track of sessions that are still connected — the very diff --git a/docs/manual-checks.md b/docs/manual-checks.md index 0acef2e..8b71440 100644 --- a/docs/manual-checks.md +++ b/docs/manual-checks.md @@ -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 diff --git a/src/DodoSSH.Client.App/App.axaml b/src/DodoSSH.Client.App/App.axaml index 26c5500..8ad5b88 100644 --- a/src/DodoSSH.Client.App/App.axaml +++ b/src/DodoSSH.Client.App/App.axaml @@ -35,13 +35,40 @@ wraps in the application and does not wrap in the harness makes every measured height too small, which is the one way this kind of test lies quietly. --> + + + + @@ -315,7 +342,7 @@ because a number that lit with its row would compete with the word beside it for the same emphasis. --> @@ -325,7 +352,7 @@ @@ -343,7 +370,7 @@ - + @@ -375,6 +402,19 @@ + + + + + + + + + + + + + + + + @@ -602,16 +712,16 @@ - - + +