Public Access
Settings, Vaults and Preferences all opened the same full-window settings mode and differed only in which of its seven pages they landed on. SettingsNav, which lists every one of those pages including Vaults and Preferences, is on screen the whole time you are in there — so two of the three rows were deep links into a place the third one opens. They saved one click and cost a menu that read as though it offered three destinations. It was three rows for a reason that had stopped applying. Until v5c, "Settings" and "Preferences" were two doors onto the same bare Preferences screen, and splitting them was the fix for something real; Vaults arrived beside them when VaultsScreen became a settings page too. Folding those two back in is not a return to that state. They are pages now, with a rail of their own, and what a menu at the foot of the nav rail is for is getting into settings at all. Nothing became unreachable — both are one click deeper, from the rail that was always going to be the way to them. ◆ WHAT STAYS, AND WHY IT IS NOT THE SAME ARGUMENT. Logout stays and still lands on the Account page rather than on General, because its confirmation card is drawn inline on that one page while the vault is unlocked; arming it from anywhere else would raise a card nobody could see. The vault switches and New vault above the divider stay too — neither is a page of the settings mode, and the switches have no other home. ◆ THE ROWS ARE REMOVED FROM THE MARKUP, NOT UNWIRED. NavRailTests asserts their absence rather than only asserting that Settings still works, because the failure that guards against is silent: a row left in the markup with its handler deleted is a menu entry that does nothing when pressed, which reads as a broken application rather than a tidied menu. The helper it asserts through excludes the vault switches by their data context and not by their label — a vault is free to be called "Vaults", and a test that a menu entry is absent must not be defeatable by what somebody named a vault. Two facts go, one comes back plus the absence check; the layout suite is 166. Manual check 1.8 walked a reader through all three rows in turn and now walks them through the one, then through SettingsNav's seven — which is where the two went. design-import-gaps.md gains a v5c-5 section rather than an edit to the v5c table: what that table records is what shipped in that pass, and it did. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
305 lines
19 KiB
XML
305 lines
19 KiB
XML
<UserControl xmlns="https://github.com/avaloniaui"
|
|
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
|
xmlns:vm="using:DodoSSH.Client.Shell.ViewModels"
|
|
x:Class="DodoSSH.Client.App.Views.NavRail"
|
|
x:DataType="vm:MainWindowViewModel">
|
|
|
|
<!--
|
|
The window's destinations, down the left edge.
|
|
|
|
── v5b ──────────────────────────────────────────────────────────────────────────────────────────────
|
|
Redrawn against NavRail.dc.html, which changes more here than a fresh coat of paint. 190 pixels became
|
|
255. The counts v2 added beside every row are gone — the mock's own row is an icon and a word, nothing
|
|
else, and this pass follows it rather than keeping a feature the mock never had; nowhere else on screen
|
|
states the number instead, so it is simply not drawn any more. And two things this rail used to answer
|
|
to the tab strip now answer to the rail itself:
|
|
|
|
── THE SWITCHER, AT THE RAIL'S OWN HEAD. ─────────────────────────────────────────────────────────────
|
|
SSH, SFTP and S3 were the strip's three fixed tabs — Vaults, SFTP, S3 — until this pass moved the choice
|
|
here as a segmented control, which is where the mock always drew it. Vaults did not come with it: the
|
|
rail's own item list below is what that tab used to gate, so a fourth segment naming it would have been
|
|
a second way to reach exactly what six rows underneath already reach. SSH, SFTP and S3 are a true
|
|
three-way here rather than two live segments and one dimmed — the mock leaves S3 unstyled as future
|
|
work, and this application already has bucket browsing, so it is wired like its two neighbours. See
|
|
<c>MainWindowViewModel.IsSshShowing</c>, <c>IsTransfersShowing</c> and <c>IsBucketsShowing</c>.
|
|
|
|
── THE RAIL IS NO LONGER DRAWN ONLY UNDER ONE TAB. ───────────────────────────────────────────────────
|
|
It used to collapse whenever the strip was on SFTP, S3 or a terminal — see the version of this remark
|
|
the v3 file carried, and <c>MainWindowViewModel.IsVaultsTab</c>, which still exists and still answers
|
|
the question it always did. What changed is <c>MainWindow.axaml</c>: the rail is part of the window's
|
|
own furniture now, the same as the titlebar and the status bar, so it stays up beside SFTP and S3 and
|
|
beside an open terminal — which is exactly what makes the switcher above worth having here at all.
|
|
|
|
── THE FIRST ROW IS MODE-DEPENDENT, exactly as the mock's own <c>mode</c> prop is. ───────────────────
|
|
One row rather than three shown and hidden by turn: its icon, its label and what it runs all come from
|
|
<c>MainWindowViewModel.FirstRailItemIcon</c>/<c>FirstRailItemLabel</c>/<c>ShowFirstRailItemCommand</c>,
|
|
which read the same three flags the switcher above lights — so the row and the segment can never name
|
|
two different modes between them.
|
|
|
|
── Vaults and Preferences left the rail's own list for the user chip's popover, at the foot. ─────────
|
|
Both are still one click away; see the popover below the user chip. The chip itself carries the signed-
|
|
in identity this application actually has — a display name and, where the server sent one, an email —
|
|
which is also new: the titlebar drew an account name and a vault chip before this pass and does not any
|
|
more. See TitleBar.axaml and design-notes/v5b-fidelity-notes.md for the one deviation this rail still
|
|
keeps on purpose: the S3 segment above. Pins was the other, and it is gone — see the remark where that
|
|
row used to sit, between Keys and Snips.
|
|
|
|
Buttons rather than a TabStrip or a ListBox, still, for the reason the v3 remark gave: all three hold
|
|
the selection themselves, so a click would move the highlight before the shell decided anything, and a
|
|
Button carries no state to disagree with the screen that is actually showing.
|
|
-->
|
|
|
|
<Border Width="255" Background="{StaticResource DeepChrome}"
|
|
BorderBrush="{StaticResource Border}" BorderThickness="0,0,1,0">
|
|
<!--
|
|
Border rather than Chip for the right-hand rule, although the mock's own value — rgb(26,26,40) — is
|
|
Chip's exact #1A1A28. Chip is the fill behind a tag, and reusing it here as a line would answer a
|
|
later "why does a rail border share a key with a bucket chip" with "it doesn't, they just happen to
|
|
match" — where Border, at #1E1E2C, is one shade off and reads identically at one pixel wide.
|
|
-->
|
|
<DockPanel LastChildFill="False" Margin="14">
|
|
|
|
<StackPanel DockPanel.Dock="Top" Spacing="18">
|
|
|
|
<!--
|
|
The segmented switcher. Three equal columns in a Grid rather than a StackPanel with Width="*" on
|
|
each child — Avalonia gives a StackPanel's children their desired size, not an even split, and the
|
|
mock's three segments are exactly a third each.
|
|
-->
|
|
<Border Classes="navtrack">
|
|
<Grid ColumnDefinitions="*,*,*">
|
|
<Button Grid.Column="0" Classes="navseg" Classes.active="{Binding IsSshShowing}"
|
|
Command="{Binding ShowTerminalCommand}"
|
|
ToolTip.Tip="The terminal, and every shell you have open">
|
|
<TextBlock Text="SSH" />
|
|
</Button>
|
|
<Button Grid.Column="1" Classes="navseg" Classes.active="{Binding IsTransfersShowing}"
|
|
Command="{Binding ShowFilesCommand}"
|
|
CommandParameter="{x:Static vm:RemoteKind.Host}"
|
|
ToolTip.Tip="Move files to and from a host over SFTP">
|
|
<TextBlock Text="SFTP" />
|
|
</Button>
|
|
<Button Grid.Column="2" Classes="navseg" Classes.active="{Binding IsBucketsShowing}"
|
|
Command="{Binding ShowFilesCommand}"
|
|
CommandParameter="{x:Static vm:RemoteKind.Bucket}"
|
|
ToolTip.Tip="Objects in an S3-compatible bucket from your keychain">
|
|
<TextBlock Text="S3" />
|
|
</Button>
|
|
</Grid>
|
|
</Border>
|
|
|
|
<StackPanel Spacing="8">
|
|
|
|
<!--
|
|
The mode-dependent first row: Terminal, Files or Buckets, matching whichever segment above is
|
|
lit. Active follows !IsVaultsTab rather than a property of its own — that is already exactly
|
|
"the terminal surface, or the files screen, or the buckets screen", which is what this row is.
|
|
-->
|
|
<Button Classes="flat nav" Classes.active="{Binding !IsVaultsTab}"
|
|
Command="{Binding ShowFirstRailItemCommand}"
|
|
ToolTip.Tip="The terminal while SSH is selected, or whichever file picker SFTP or S3 chose">
|
|
<StackPanel Orientation="Horizontal" Spacing="10">
|
|
<TextBlock Classes="navicon" Text="{Binding FirstRailItemIcon}" />
|
|
<TextBlock Classes="navlabel" Text="{Binding FirstRailItemLabel}" />
|
|
</StackPanel>
|
|
</Button>
|
|
|
|
<Button Classes="flat nav" Classes.active="{Binding IsHostsShowing}"
|
|
Command="{Binding ShowScreenCommand}"
|
|
CommandParameter="{x:Static vm:ShellScreen.Hosts}"
|
|
ToolTip.Tip="Your hosts, and what is known about the one you have selected">
|
|
<StackPanel Orientation="Horizontal" Spacing="10">
|
|
<TextBlock Classes="navicon" Text="" />
|
|
<TextBlock Classes="navlabel" Text="Hosts" />
|
|
</StackPanel>
|
|
</Button>
|
|
|
|
<!--
|
|
Keys, not Keychain — the mock's own word for this screen, which still holds SSH keys and stored
|
|
passwords; see ShellScreen.Keychain for the name that did not move with the label.
|
|
-->
|
|
<Button Classes="flat nav" Classes.active="{Binding IsKeychainShowing}"
|
|
Command="{Binding ShowScreenCommand}"
|
|
CommandParameter="{x:Static vm:ShellScreen.Keychain}"
|
|
ToolTip.Tip="Your keychain: SSH keys and stored passwords">
|
|
<StackPanel Orientation="Horizontal" Spacing="10">
|
|
<TextBlock Classes="navicon" Text="" />
|
|
<TextBlock Classes="navlabel" Text="Keys" />
|
|
</StackPanel>
|
|
</Button>
|
|
|
|
<!--
|
|
◆ NO Pins ROW. The pins screen is still here and still reached in one click — from "Host keys"
|
|
on the Keys screen's own header, which is where a list of approved host keys belongs: they are
|
|
keychain material, and that button was already the second way to reach them. Two rail rows away
|
|
from each other, both landing on the same screen, is a rail that has to be read twice.
|
|
|
|
It is also the last of the rail's own deviations from the mock to go. The row was kept in v5b on
|
|
the grounds that the design has no screen for approved host keys at all — see the file-level
|
|
remark — which is true of the design and was never a reason for a rail entry once the keychain
|
|
had a door to the same place.
|
|
-->
|
|
|
|
<Button Classes="flat nav" Classes.active="{Binding IsSnippetsShowing}"
|
|
Command="{Binding ShowScreenCommand}"
|
|
CommandParameter="{x:Static vm:ShellScreen.Snippets}"
|
|
ToolTip.Tip="Commands you have saved, and how to put one into a terminal">
|
|
<StackPanel Orientation="Horizontal" Spacing="10">
|
|
<TextBlock Classes="navicon" Text="" />
|
|
<TextBlock Classes="navlabel" Text="Snips" />
|
|
</StackPanel>
|
|
</Button>
|
|
|
|
<Button Classes="flat nav" Classes.active="{Binding IsLogsShowing}"
|
|
Command="{Binding ShowScreenCommand}"
|
|
CommandParameter="{x:Static vm:ShellScreen.Logs}"
|
|
ToolTip.Tip="What has been connected to, and what has been changed in this keychain">
|
|
<StackPanel Orientation="Horizontal" Spacing="10">
|
|
<TextBlock Classes="navicon" Text="" />
|
|
<TextBlock Classes="navlabel" Text="Logs" />
|
|
</StackPanel>
|
|
</Button>
|
|
|
|
</StackPanel>
|
|
|
|
</StackPanel>
|
|
|
|
<!--
|
|
The rail's foot: the signed-in identity, and everything the strip's old Vaults tab used to gate
|
|
behind a caret. A Flyout is safe here without any ordering games: it opens inside the rail's own
|
|
255-pixel column, which the terminal's native child window never occupies — there is no rectangle
|
|
here a popup could be composited underneath, unlike the window-wide tab strip this rail's own
|
|
switcher replaced, which sat directly above that rectangle and had to select a page before opening
|
|
one for exactly that reason.
|
|
-->
|
|
<Button x:Name="UserChip" DockPanel.Dock="Bottom" Classes="flat navuser" Click="OnUserChipPressed">
|
|
<Grid ColumnDefinitions="Auto,*">
|
|
<Border Grid.Column="0" Width="20" Height="20" CornerRadius="60"
|
|
Background="{StaticResource AvatarGradient}">
|
|
<TextBlock Text="{Binding AvatarInitials}" FontWeight="Bold" FontSize="7.5" LetterSpacing="0.2"
|
|
Foreground="White" HorizontalAlignment="Center" VerticalAlignment="Center" />
|
|
</Border>
|
|
<StackPanel Grid.Column="1" Orientation="Horizontal" Margin="10,0,0,0" Spacing="4">
|
|
<TextBlock FontWeight="Bold" FontSize="10.5" LetterSpacing="0.1"
|
|
Foreground="{StaticResource TextGhost}" VerticalAlignment="Center"
|
|
Text="{Binding AccountName}" TextTrimming="CharacterEllipsis" />
|
|
<TextBlock FontSize="8" Foreground="{StaticResource TextGhost}" VerticalAlignment="Center"
|
|
Text="▼" />
|
|
</StackPanel>
|
|
</Grid>
|
|
|
|
<FlyoutBase.AttachedFlyout>
|
|
<!--
|
|
FlyoutPresenterClasses, because a Flyout's own panel is not in this markup's visual tree to be
|
|
styled from here — see FlyoutPresenter.poppanel in App.axaml for what the class carries and why
|
|
the shared popup rule was not simply widened to cover it.
|
|
-->
|
|
<Flyout Placement="TopEdgeAlignedLeft" FlyoutPresenterClasses="poppanel">
|
|
<StackPanel Width="227" Spacing="4">
|
|
|
|
<!--
|
|
The real email, when the server sent one — verified against MainWindowViewModel.Email rather
|
|
than assumed, and simply absent from the popover when it has not. No " · Org" suffix: there
|
|
is no organisation concept behind a vault, only the vault itself, which the rows below name.
|
|
-->
|
|
<TextBlock FontSize="10.5" FontWeight="Medium" LetterSpacing="0.1" Margin="11,4,11,6"
|
|
Foreground="{StaticResource TextGhost}"
|
|
Text="{Binding Email}" TextTrimming="CharacterEllipsis"
|
|
IsVisible="{Binding Email, Converter={x:Static StringConverters.IsNotNullOrEmpty}}" />
|
|
|
|
<!--
|
|
One row per readable vault — the strip's old "SHOW ITEMS FROM" chips, restyled: a 14-pixel
|
|
initial square (Chip's own fill, since no per-vault colour exists to draw honestly) and a
|
|
magenta check where the vault's items are shown. Toggling one leaves the Flyout open, the
|
|
same as the chips it replaces did — this is a switch to flip, not a place to leave from.
|
|
-->
|
|
<ItemsControl ItemsSource="{Binding VaultToggles}" IsVisible="{Binding HasVaultSwitches}">
|
|
<ItemsControl.ItemTemplate>
|
|
<DataTemplate x:DataType="vm:VaultToggleViewModel">
|
|
<Button Classes="poprow"
|
|
Command="{Binding $parent[ItemsControl].((vm:MainWindowViewModel)DataContext).ToggleVaultCommand}"
|
|
CommandParameter="{Binding}">
|
|
<Grid ColumnDefinitions="Auto,*,Auto">
|
|
<Border Grid.Column="0" Width="14" Height="14" CornerRadius="2"
|
|
Background="{StaticResource Chip}">
|
|
<TextBlock Text="{Binding Initial}" FontWeight="Bold" FontSize="9"
|
|
Foreground="{StaticResource TextDim}"
|
|
HorizontalAlignment="Center" VerticalAlignment="Center" />
|
|
</Border>
|
|
<TextBlock Grid.Column="1" Margin="10,0" FontSize="10"
|
|
Foreground="{StaticResource Text}" VerticalAlignment="Center"
|
|
Text="{Binding Display}" TextTrimming="CharacterEllipsis" />
|
|
<Border Grid.Column="2" Classes="vaultcheck" IsVisible="{Binding IsShown}">
|
|
<TextBlock FontFamily="{StaticResource IconFont}" Text="" FontSize="10"
|
|
Foreground="White"
|
|
HorizontalAlignment="Center" VerticalAlignment="Center" />
|
|
</Border>
|
|
</Grid>
|
|
</Button>
|
|
</DataTemplate>
|
|
</ItemsControl.ItemTemplate>
|
|
</ItemsControl>
|
|
|
|
<Button Classes="poprow" Click="OnPopoverNewVaultPressed"
|
|
ToolTip.Tip="Names a vault you can share, and opens it on the Vaults screen so you can add people to it and give them roles">
|
|
<Grid ColumnDefinitions="*,Auto">
|
|
<TextBlock Text="New vault" FontSize="10" Foreground="{StaticResource TextGhost}" />
|
|
<TextBlock Grid.Column="1" FontFamily="{StaticResource IconFont}" Text=""
|
|
FontSize="12" Foreground="{StaticResource TextGhost}" />
|
|
</Grid>
|
|
</Button>
|
|
|
|
<Border Height="1" Margin="11,4" Background="{StaticResource BorderMid}" />
|
|
|
|
<!--
|
|
◆ ONE ROW, WHERE v5c PUT THREE.
|
|
|
|
Settings, Vaults and Preferences all opened the same full-window settings mode — see
|
|
MainWindowViewModel.EnterSettings and SettingsView.axaml — differing only in which of its
|
|
seven pages they landed on. Two of the three were therefore deep links into a place the third
|
|
one opens, and SettingsNav is already on screen the whole time listing every page including
|
|
those two, so the shortcut saved one click and cost a menu that read as though it offered
|
|
three destinations.
|
|
|
|
It was three rows because settings mode had only just been built out of what used to be two
|
|
bare ShellScreens: "Settings" and "Preferences" were the same click before that wave, and
|
|
splitting them was the fix for a real thing. Folding the other two back in is not a return to
|
|
that state — Vaults and Preferences are pages now, reached from the rail inside settings, and
|
|
what this menu is for is getting there at all.
|
|
|
|
What still lands on a page rather than on General is Logout below, and it has to: its
|
|
confirmation card is drawn inline on the Account page and nowhere else.
|
|
-->
|
|
<Button Classes="poprow" Click="OnPopoverSettingsPressed">
|
|
<StackPanel Orientation="Horizontal" Spacing="10">
|
|
<TextBlock FontFamily="{StaticResource IconFont}" Text="" FontSize="12"
|
|
Foreground="{StaticResource TextGhost}" />
|
|
<TextBlock Text="Settings" FontSize="10" Foreground="{StaticResource Text}" />
|
|
</StackPanel>
|
|
</Button>
|
|
|
|
<Border Height="1" Margin="11,4" Background="{StaticResource BorderMid}" />
|
|
|
|
<!--
|
|
The existing sign-out flow, with its own confirm card — see
|
|
MainWindowViewModel.SignOutFromPopover for why this goes through the Account settings page
|
|
rather than calling SignOutCommand directly from wherever the popover happened to be opened.
|
|
-->
|
|
<Button Classes="poprow" Click="OnPopoverLogoutPressed">
|
|
<StackPanel Orientation="Horizontal" Spacing="10">
|
|
<TextBlock FontFamily="{StaticResource IconFont}" Text="" FontSize="12"
|
|
Foreground="{StaticResource TextGhost}" />
|
|
<TextBlock Text="Logout" FontSize="10" Foreground="{StaticResource Text}" />
|
|
</StackPanel>
|
|
</Button>
|
|
|
|
</StackPanel>
|
|
</Flyout>
|
|
</FlyoutBase.AttachedFlyout>
|
|
</Button>
|
|
|
|
</DockPanel>
|
|
</Border>
|
|
|
|
</UserControl>
|