diff --git a/src/DodoSSH.Client.App/App.axaml b/src/DodoSSH.Client.App/App.axaml
index e4089f3..f906eb0 100644
--- a/src/DodoSSH.Client.App/App.axaml
+++ b/src/DodoSSH.Client.App/App.axaml
@@ -305,6 +305,22 @@
+
+
+
+
-
+
-
-
-
-
-
-
+
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
@@ -77,209 +96,227 @@
-
+
-
+
-
+
-
-
-
-
+
+
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
-
+ ◆ v5b's session shell, wrapping this screen's existing content rather than replacing it — the
+ two-pane grid and the queue inside TransfersScreen.axaml are untouched; wave C restyles their
+ internals. What is new here is everything design-notes/v5b-fidelity-notes.md calls the session
+ shell: a 26px padded column, an in-screen tab row, a bordered rounded-bottom container holding
+ a host header and a status bar around the screen's own content, and a 300px sidebar.
+ Gated on IsTransfersScreen exactly as before — IsShowingPages means "the Avalonia page area,
+ not the WebView", which is the occlusion question, and which mode the switcher is on is this
+ wrapper's own separate question.
+
+ The tab row's own click does not select a terminal tab — there is no per-tab SFTP session in
+ this application, and building one is out of this wave's scope; see the notes' own open
+ question and MainWindowViewModel.SelectFilesHostCommand for how this resolves it: a click
+ reuses the same "Browse files" plumbing a pin click already does, honestly opening (or
+ reusing) a second, SFTP-specific connection to that tab's host rather than pretending a session
+ exists that does not.
+ -->
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
-
+
+
-
-
+
+
-
-
-
+
-
+
-
-
-
-
-
-
-
-
+
-
-
-
-
-
-
-
+
+
+
+
+
+
+
-
-
-
-
-
-
+
+
+
+
+
+
-
+
+
+
+
+
+
- It shares a word with the tab strip's first tab, which is a different level of the window: that
- tab is "this application rather than SFTP or S3", and this is one of the nine screens under it.
- -->
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/DodoSSH.Client.App/Views/NavRail.axaml.cs b/src/DodoSSH.Client.App/Views/NavRail.axaml.cs
index 607203f..38747f7 100644
--- a/src/DodoSSH.Client.App/Views/NavRail.axaml.cs
+++ b/src/DodoSSH.Client.App/Views/NavRail.axaml.cs
@@ -1,9 +1,85 @@
using Avalonia.Controls;
+using Avalonia.Controls.Primitives;
+using Avalonia.Interactivity;
+using DodoSSH.Client.Shell.ViewModels;
namespace DodoSSH.Client.App.Views;
-/// The five destinations down the left edge of the unlocked window.
+/// The window's destinations, down the left edge — the switcher, the six rail rows and the user chip.
internal sealed partial class NavRail : UserControl
{
public NavRail() => InitializeComponent();
+
+ /// Opens the user popover.
+ ///
+ /// A handler rather than relying on the click that opening a Flyout answers to on its own: a
+ /// named method is a thing a test can call directly, where an implicit open is not.
+ ///
+ private void OnUserChipPressed(object? sender, RoutedEventArgs e)
+ {
+ if (sender is Control chip)
+ {
+ FlyoutBase.ShowAttachedFlyout(chip);
+ }
+ }
+
+ /// Hides the popover, whatever handler is about to navigate.
+ private void ClosePopover()
+ {
+ if (this.FindControl("UserChip") is { } chip)
+ {
+ FlyoutBase.GetAttachedFlyout(chip)?.Hide();
+ }
+ }
+
+ /// Leaves for the vaults screen with the new-vault form open, shutting the popover behind it.
+ ///
+ /// The popover is closed first: the command navigates, and a popup left open would be hanging over a
+ /// screen it has nothing to do with. A Flyout does not close on its own when something inside it
+ /// is pressed — which is what the vault switches above it want, and not what this wants.
+ ///
+ private void OnPopoverNewVaultPressed(object? sender, RoutedEventArgs e)
+ {
+ ClosePopover();
+
+ if (DataContext is MainWindowViewModel shell)
+ {
+ shell.ShowNewVaultCommand.Execute(null);
+ }
+ }
+
+ /// Settings and Preferences both land here — see the remark in the markup.
+ private void OnPopoverPreferencesPressed(object? sender, RoutedEventArgs e)
+ {
+ ClosePopover();
+
+ if (DataContext is MainWindowViewModel shell)
+ {
+ shell.ShowScreenCommand.Execute(ShellScreen.Preferences);
+ }
+ }
+
+ private void OnPopoverVaultsPressed(object? sender, RoutedEventArgs e)
+ {
+ ClosePopover();
+
+ if (DataContext is MainWindowViewModel shell)
+ {
+ shell.ShowScreenCommand.Execute(ShellScreen.Vaults);
+ }
+ }
+
+ ///
+ /// Starts a sign-out, through Preferences so the confirmation card has somewhere to be seen — see
+ /// .
+ ///
+ private void OnPopoverLogoutPressed(object? sender, RoutedEventArgs e)
+ {
+ ClosePopover();
+
+ if (DataContext is MainWindowViewModel shell)
+ {
+ shell.SignOutFromPopoverCommand.Execute(null);
+ }
+ }
}
diff --git a/src/DodoSSH.Client.App/Views/SessionHeader.axaml b/src/DodoSSH.Client.App/Views/SessionHeader.axaml
new file mode 100644
index 0000000..d9bd990
--- /dev/null
+++ b/src/DodoSSH.Client.App/Views/SessionHeader.axaml
@@ -0,0 +1,49 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/DodoSSH.Client.App/Views/SessionHeader.axaml.cs b/src/DodoSSH.Client.App/Views/SessionHeader.axaml.cs
new file mode 100644
index 0000000..ee4dd87
--- /dev/null
+++ b/src/DodoSSH.Client.App/Views/SessionHeader.axaml.cs
@@ -0,0 +1,58 @@
+using System.Windows.Input;
+using Avalonia;
+using Avalonia.Controls;
+
+namespace DodoSSH.Client.App.Views;
+
+///
+/// The v5b session shell's host header: the address, and a ghost button that crosses to the other surface.
+/// See the remark at the top of SessionHeader.axaml.
+///
+internal sealed partial class SessionHeader : UserControl
+{
+ /// What the cross-surface ghost button says — "Open SFTP" or "Open terminal".
+ internal static readonly StyledProperty OpenLabelProperty =
+ AvaloniaProperty.Register(nameof(OpenLabel));
+
+ /// What the cross-surface ghost button runs.
+ ///
+ /// The terminal usage binds SelectFilesHostCommand with the selected tab as its parameter; the
+ /// SFTP usage binds OpenTerminalForFilesHostCommand, which needs none — see the remark on both in
+ /// MainWindowViewModel for why the two directions are not symmetrical.
+ ///
+ internal static readonly StyledProperty OpenCommandProperty =
+ AvaloniaProperty.Register(nameof(OpenCommand));
+
+ internal static readonly StyledProperty
-
-
-
-
-
+
+
+
+
+
+
-
-
-
+
+
-
-
+ Text="{Binding Name}" FontSize="13.5"
+ Margin="10,0,8,0" VerticalAlignment="Center" TextTrimming="CharacterEllipsis" />
+
+
+ Text="{Binding Permissions}" FontSize="10" Margin="6,0,0,0"
+ HorizontalAlignment="Right" VerticalAlignment="Center" TextTrimming="CharacterEllipsis" />
@@ -392,10 +522,11 @@
HorizontalAlignment="Center" VerticalAlignment="Center">
-
-
-
+
+
+
+
+
+
+
+
+
+ Text="{Binding ActiveTransfersLabel}" />
+
+
-
+
+
+
+
- It is here rather than beside DISCONNECT because this row spans the window and that one spans
- half of it: what the screen has to say about a session is a sentence, and a sentence needs the
- width. Only while something is open — the other half of the time it is inside the invitation
- in the remote pane, next to the button that provoked it, which is where a refusal has to be.
+
+
+
+
+
+
+
- Right-aligned in a free column, so it reads as this row's other end rather than as a third
- clause of the sentence to its left.
- -->
-
-
-
+
-
-
-
+
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+