Restyle the drawer, pin folders on a host, and say when it was last connected

This commit is contained in:
2026-08-07 18:12:31 +02:00
parent c3ef4bd8b4
commit 2ba7c14e35
8 changed files with 1392 additions and 202 deletions
+71 -9
View File
@@ -585,6 +585,30 @@
<Setter Property="Foreground" Value="{StaticResource TextFaint}" />
</Style>
<!--
◆ THE ADDRESS FIELD, the one input the host drawer draws with an accent border rather than the
ordinary grey hairline — the mock's own choice, kept, because it is the one field a session cannot be
opened without. Taller than the drawer's other fields too, at 42 rather than the TextBox default.
No drawn caret: the mock blinks one inside the field's own text, but this is a real TextBox with a real
one already, so painting a second would either fight the true caret for the same pixel or lie about
where typing lands. See HostDrawer.axaml's ADDRESS section, on both the pane and the editor.
Declared after the base TextBox rules — including :pointerover and :focus — for the reason this file
states three times already for Button.tab, Border.rowmark and the tiles list: Avalonia has no
specificity, and a rule declared before an exception it overrides does nothing at all. Placed here
rather than beside them so both still read as one un-interrupted block of "what every TextBox looks
like" with the one exception after it, not threaded through the middle.
-->
<Style Selector="TextBox.address">
<Setter Property="Height" Value="42" />
<Setter Property="FontFamily" Value="{StaticResource MonoFont}" />
</Style>
<Style Selector="TextBox.address /template/ Border#PART_BorderElement">
<Setter Property="BorderBrush" Value="{StaticResource Accent}" />
<Setter Property="BorderThickness" Value="1.5" />
</Style>
<!--
Lists. The rows in this design are drawn by their own templates and marked as selected with an accent
strip down the left edge, so the theme's rounded blue selection block has to go — it would sit behind
@@ -642,16 +666,18 @@
into a grid: equal columns, and a card that grew a third line of tags is taller than its neighbours
rather than narrower.
◆ 232 IS DERIVED, and the arithmetic is written out because getting it wrong is invisible. The grid's
column at the window's minimum is 1016 less the rail's 190 and the drawer's 304, which is 522. The
◆ 224 IS DERIVED, and the arithmetic is written out because getting it wrong is invisible. The grid's
column at the window's minimum is 1016 less the rail's 190 and the drawer's 320, which is 506. The
scrolling stack inside it takes 16 of margin on each side, and the vertical scrollbar takes its own —
call the usable width 490. A WrapPanel fits floor(490 / (Width + 10)) per row, so two columns needs
Width no more than 235.
call the usable width 474. A WrapPanel fits floor(474 / (Width + 10)) per row, so two columns needs
Width no more than 227.
The first number here was 248, from the same reasoning with the two margins left out. It laid out
cleanly and the layout harness passed it, because the harness asks whether a control is inside the
window and not how many of them fit on a line — so the grid quietly became one column wide at exactly
the size this application guarantees, which is the shape the cards exist to avoid.
the size this application guarantees, which is the shape the cards exist to avoid. The second was 232,
derived the same way against the drawer's own 304; v5 widened the drawer to 320 for the ADDRESS field's
breathing room, which narrowed the budget this number is drawn from and had to move it down in step.
-->
<Style Selector="Border.tile">
<Setter Property="Background" Value="{StaticResource Raised}" />
@@ -659,7 +685,7 @@
<Setter Property="BorderThickness" Value="1" />
<Setter Property="CornerRadius" Value="12" />
<Setter Property="Padding" Value="12,10" />
<Setter Property="Width" Value="232" />
<Setter Property="Width" Value="224" />
<Setter Property="Margin" Value="0,0,10,10" />
</Style>
<Style Selector="ListBoxItem:pointerover Border.tile">
@@ -749,8 +775,9 @@
The hosts drawer is a column of grouped fields rather than a run of labelled controls — address here,
the general facts here, the connection and its credentials here — which is what the design draws and
what a 304-pixel column needs to stay readable: three cards is three things to find, where fourteen
stacked controls is fourteen.
what a 320-pixel column needs to stay readable: three cards is three things to find, where fourteen
stacked controls is fourteen. v5 widened the column from 304 to 320 for the ADDRESS field's own
breathing room, and moved every affected number in this file and its neighbours with it.
Raised on Sidebar, which is the one pairing that works in this column. Panel and Chrome are the same
value as each other and near enough Sidebar to vanish against it. Field no longer reads as a well
@@ -769,8 +796,12 @@
<!--
A card's heading, and deliberately not TextBlock.label. That style is the small tracked-out capitals
this design uses for a *section of a screen* — GROUPS, HOSTS, the vault's categories — and three of
them inside a 304-pixel column would claim the same weight as the screen headings beside it. These are
them inside a 320-pixel column would claim the same weight as the screen headings beside it. These are
captions on a card, so they are sentence case at the body size.
Still what the group editor's two cards use — GROUP, and what its hosts inherit — because that editor
is not the mock this file's ADDRESS/GENERAL/CONNECTION/QUICK ACCESS labels were drawn from and nothing
asked it to match a screen it is not on.
-->
<Style Selector="TextBlock.sectionhead">
<Setter Property="FontSize" Value="13" />
@@ -778,6 +809,24 @@
<Setter Property="Foreground" Value="{StaticResource Text}" />
</Style>
<!--
◆ THE OTHER CARD HEADING, v5's own, and the two are not interchangeable.
TextBlock.sectionhead above is sentence case at the body size — a caption on a card, the same weight
the design's own prose is drawn at. This is the v5 mock's own idiom for the same job, on the one screen
it was drawn for: ADDRESS, GENERAL, CONNECTION and QUICK ACCESS in the host drawer, tracked-out capitals
at a third the size. Both exist because the group editor's cards were not restyled to the v5 mock — see
the remark above — so the drawer now carries a heading in each of the two idioms depending on which
panel is showing, and a single class could not have been both.
-->
<Style Selector="TextBlock.sectionlabel">
<Setter Property="FontFamily" Value="{StaticResource MonoFont}" />
<Setter Property="FontSize" Value="10" />
<Setter Property="FontWeight" Value="SemiBold" />
<Setter Property="LetterSpacing" Value="1.2" />
<Setter Property="Foreground" Value="{StaticResource TextGhost}" />
</Style>
<!--
◆ A ROW IN THE DETAIL PANE, AND IT IS A BUTTON THAT LOOKS LIKE A FIELD.
@@ -817,6 +866,19 @@
<Setter Property="BorderBrush" Value="{StaticResource BorderHover}" />
</Style>
<!--
The detail pane's own ADDRESS row, matched to TextBox.address above: taller, and bordered in accent
rather than the ordinary hairline even under the pointer — declared after the :pointerover rule for the
reason that class's own remark gives, so it wins in both states rather than only the resting one.
-->
<Style Selector="Button.fieldrow.address">
<Setter Property="MinHeight" Value="42" />
</Style>
<Style Selector="Button.fieldrow.address /template/ ContentPresenter#PART_ContentPresenter">
<Setter Property="BorderBrush" Value="{StaticResource Accent}" />
<Setter Property="BorderThickness" Value="1.5" />
</Style>
<!--
The glyph at the head of such a row. A fixed width so that four rows in a card line their text up, and
the faintest step in the ramp because it is a mark to skip rather than a fact to read: what the row
+211 -57
View File
@@ -31,16 +31,23 @@
could disagree: IsShowingHostDetail is defined as "neither editor is open and the pane has been asked
for", so no two of these can be true at once. The whole control collapses when none of them is — see
IsDrawerOpen, which the hosts screen binds — rather than standing there empty, because an empty
300-pixel column beside a grid is 300 pixels the grid could have had.
320-pixel column beside a grid is 320 pixels the grid could have had.
◆ AND IT IS ASKED FOR NOW, rather than arriving with a selection. Touching a card used to open this,
which charged the width of the pane for the act of choosing; the pencil on a card is the asking. See
VaultViewModel.IsHostPaneOpen and the item template in HostsScreen.axaml.
── THE THREE PANELS ARE THE SAME THREE CARDS ────────────────────────────────────────────────────────
Address, General, Connection first as rows stating what the host is, then as boxes for changing it.
The detail pane's rows are buttons that open the editor, so a box that looks editable turns out to be,
one step along; see Button.fieldrow in App.axaml for why they are not inputs that save as you type.
── THE TWO PANELS ARE THE SAME CARDS ────────────────────────────────────────────────────────────────
Address, General, Connection, and — since v5 — Quick access: first as rows stating what the host is,
then as boxes for changing it. QUICK ACCESS is the one card the detail pane draws conditionally rather
than always: a host with no pinned paths has nothing to state, where an empty Address or General would
be a fact about the host rather than an absent one. The detail pane's rows are buttons that open the
editor, so a box that looks editable turns out to be, one step along; see Button.fieldrow in App.axaml
for why they are not inputs that save as you type.
The editor's four cards carry v5's own section labels — ADDRESS, GENERAL, CONNECTION, QUICK ACCESS — the
mock's tracked-out capitals rather than TextBlock.sectionhead's sentence case; see TextBlock.sectionlabel
in App.axaml and the remark on the editor's StackPanel below for why the detail pane did not move too.
Its data context is the VaultViewModel, so every binding here is a property of the vault. The hosts
screen hands it over.
@@ -62,7 +69,7 @@
move is a re-seal into one and a tombstone in the other, and the host takes a new id.
-->
<Border Width="304" Background="{StaticResource Sidebar}"
<Border Width="320" Background="{StaticResource Sidebar}"
BorderBrush="{StaticResource Border}" BorderThickness="1,0,0,0">
<Grid RowDefinitions="Auto,*,Auto">
@@ -79,14 +86,19 @@
menu instead, next to the two other things that happen to a whole host. Choosing the vault at the
moment a host is created is a different question, and it is in the editor beside the name.
-->
<Border Grid.Row="0" Padding="14,10" Background="{StaticResource Panel}"
<Border Grid.Row="0" Padding="22,16" Background="{StaticResource Panel}"
BorderBrush="{StaticResource Border}" BorderThickness="0,0,0,1">
<Grid ColumnDefinitions="*,Auto,Auto">
<StackPanel Grid.Column="0" VerticalAlignment="Center" Spacing="1">
<TextBlock Text="{Binding DrawerTitle}" FontSize="14" FontWeight="SemiBold"
<!--
Title 20 bold and subtitle 12.5, the mock's own header sizes. What the subtitle says is
unchanged — see DrawerSubtitle's own remarks below the header for why it is the vault's name
rather than the mock's "Saving to X vault" sentence with a picker's chevron inside it.
-->
<StackPanel Grid.Column="0" VerticalAlignment="Center" Spacing="2">
<TextBlock Text="{Binding DrawerTitle}" FontSize="20" FontWeight="Bold"
Foreground="{StaticResource Text}" TextTrimming="CharacterEllipsis" />
<TextBlock Text="{Binding DrawerSubtitle}" FontSize="11"
<TextBlock Text="{Binding DrawerSubtitle}" FontSize="12.5"
Foreground="{StaticResource TextFaint}" TextTrimming="CharacterEllipsis"
IsVisible="{Binding DrawerSubtitle,
Converter={x:Static StringConverters.IsNotNullOrEmpty}}" />
@@ -138,7 +150,7 @@
<Panel>
<!-- ============ WHAT THIS HOST IS ============ -->
<StackPanel Margin="12" Spacing="10" IsVisible="{Binding IsShowingHostDetail}">
<StackPanel Margin="16,14" Spacing="10" IsVisible="{Binding IsShowingHostDetail}">
<!--
============ ADDRESS ============
@@ -155,7 +167,12 @@
Foreground="{StaticResource AccentText}"
HorizontalAlignment="Center" VerticalAlignment="Center" />
</Border>
<Button Grid.Column="1" Classes="fieldrow" Margin="10,0,0,0"
<!--
Classes="address" is the v5 restyle: 42 tall and bordered in accent rather than the
ordinary hairline, matched by TextBox.address in App.axaml, which the editor's own
hostname box below wears too — the two are the same field, one step apart.
-->
<Button Grid.Column="1" Classes="fieldrow address" Margin="10,0,0,0"
Command="{Binding EditSelectedHostCommand}"
ToolTip.Tip="Opens this host's editor.">
<TextBlock Classes="mono" Text="{Binding SelectedHost.Host.Hostname}" FontSize="12"
@@ -338,6 +355,36 @@
</StackPanel>
</Border>
<!--
============ QUICK ACCESS (read-only) ============
The folders pinned on this host, in the pane's own fieldrow idiom rather than the editor's rows
with a close box on each one: one Button opens the editor on a press, same as every other row
here, and what is inside it is a list rather than a single fact — the same shape the tags row
above takes for the same reason. Hidden entirely rather than shown empty, for the reason Notes
is: a card titled QUICK ACCESS with nothing under it would answer a question nobody asked here.
-->
<Border Classes="section" IsVisible="{Binding SelectedHost.HasPins}">
<StackPanel Spacing="6">
<TextBlock Classes="sectionhead" Text="Quick access" Margin="0,0,0,2" />
<Button Classes="fieldrow" Command="{Binding EditSelectedHostCommand}">
<Grid ColumnDefinitions="Auto,*">
<TextBlock Grid.Column="0" FontFamily="{StaticResource IconFont}" FontSize="13"
Foreground="{StaticResource TextFaint}" Width="16" VerticalAlignment="Top"
Margin="0,3,0,0" Text="&#xE2C7;" />
<ItemsControl Grid.Column="1" ItemsSource="{Binding SelectedHost.Host.PinnedPaths}">
<ItemsControl.ItemTemplate>
<DataTemplate x:DataType="x:String">
<TextBlock Classes="mono" Text="{Binding}" FontSize="12"
TextTrimming="CharacterEllipsis" Margin="0,0,0,2" />
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</Grid>
</Button>
</StackPanel>
</Border>
<TextBlock Classes="hint" FontSize="11" TextWrapping="Wrap"
Text="Double-clicking the card does the same as CONNECT. The terminal opens as a tab in the strip above and stays there while you look at anything else." />
@@ -345,22 +392,35 @@
<!-- ============ THE HOST EDITOR ============ -->
<!--
The editor doubles as the "add" form; there is no separate dialog. Same three cards as the pane
The editor doubles as the "add" form; there is no separate dialog. Same four cards as the pane
above, with the boxes real — which is what makes pressing a row up there lead somewhere that
looks like where it was pressed.
looks like where it was pressed. QUICK ACCESS is the one card that always draws here even when
the pane above hid it for having nothing to show: an empty editor section is where a first pin
gets added, so it cannot be conditional on already having one.
-->
<StackPanel Margin="12" Spacing="10" IsVisible="{Binding IsEditing}">
<StackPanel Margin="16,14" Spacing="10" IsVisible="{Binding IsEditing}">
<!--
v5's own section labels — ADDRESS, GENERAL, CONNECTION, QUICK ACCESS — replace sentence-case
headings on this one panel, the editor, because it is the panel the mock actually draws; the
detail pane above keeps TextBlock.sectionhead, its own established idiom, since the mock has no
read-only view to restyle it against. See TextBlock.sectionlabel in App.axaml.
-->
<Border Classes="section">
<StackPanel Spacing="8">
<TextBlock Classes="sectionhead" Text="Address" />
<TextBlock Classes="sectionlabel" Text="ADDRESS" />
<Grid ColumnDefinitions="Auto,*">
<Border Grid.Column="0" Classes="tileicon" Background="{StaticResource Chip}">
<TextBlock Classes="mono" Text="&gt;_" FontSize="11" FontWeight="Bold"
Foreground="{StaticResource AccentText}"
HorizontalAlignment="Center" VerticalAlignment="Center" />
</Border>
<TextBox Grid.Column="1" Margin="10,0,0,0" Text="{Binding EditorHostname}"
<!--
No drawn caret: this TextBox has a real one, and the mock's blinking bar is what a real
caret looks like in a screenshot that cannot show motion. Classes="address" is the 42-tall,
accent-bordered restyle shared with the detail pane's own Hostname row above.
-->
<TextBox Grid.Column="1" Classes="address" Margin="10,0,0,0" Text="{Binding EditorHostname}"
PlaceholderText="hostname or address" />
</Grid>
</StackPanel>
@@ -368,9 +428,9 @@
<Border Classes="section">
<StackPanel Spacing="6">
<TextBlock Classes="sectionhead" Text="General" Margin="0,0,0,2" />
<TextBlock Classes="sectionlabel" Text="GENERAL" Margin="0,0,0,2" />
<TextBox Text="{Binding EditorLabel}" PlaceholderText="name" />
<TextBox Text="{Binding EditorLabel}" PlaceholderText="name" Height="40" />
<!--
◆ WHICH VAULT THIS HOST WILL LIVE IN, asked here because it decides who can read it and
@@ -413,7 +473,7 @@
-->
<ComboBox ItemsSource="{Binding EditorGroupChoices}"
SelectedItem="{Binding EditorSelectedGroup}"
HorizontalAlignment="Stretch">
HorizontalAlignment="Stretch" Height="40">
<ComboBox.ItemTemplate>
<DataTemplate x:DataType="vm:GroupChoice">
<TextBlock Text="{Binding Label}" />
@@ -453,37 +513,48 @@
</ItemsControl.ItemTemplate>
</ItemsControl>
<!--
Add button is 40x40 and a glyph rather than the word ADD, matched to the field beside it —
the v5 mock draws every add affordance in this drawer as a square icon button; QUICK ACCESS's
own add row below is the same idiom at 36x36, one size down for its shorter field.
-->
<Grid ColumnDefinitions="*,6,Auto">
<TextBox Grid.Column="0" Text="{Binding EditorNewTag}" PlaceholderText="new tag">
<TextBox Grid.Column="0" Text="{Binding EditorNewTag}" PlaceholderText="new tag"
Height="40">
<TextBox.KeyBindings>
<KeyBinding Gesture="Enter" Command="{Binding AddEditorTagCommand}" />
</TextBox.KeyBindings>
</TextBox>
<Button Grid.Column="2" Classes="ghost" Content="ADD"
Command="{Binding AddEditorTagCommand}" />
<Button Grid.Column="2" Classes="ghost" Content="+" Width="40" Height="40"
FontSize="16" HorizontalContentAlignment="Center"
Command="{Binding AddEditorTagCommand}" ToolTip.Tip="Adds this tag" />
</Grid>
<TextBox Text="{Binding EditorNotes}" PlaceholderText="notes" AcceptsReturn="True"
Height="56" TextWrapping="Wrap" />
Height="58" TextWrapping="Wrap" />
</StackPanel>
</Border>
<Border Classes="section">
<StackPanel Spacing="6">
<TextBlock Classes="sectionhead" Text="Connection" Margin="0,0,0,2" />
<TextBlock Classes="sectionlabel" Text="CONNECTION" Margin="0,0,0,2" />
<!--
Both boxes are allowed to be empty, and empty means "take the group's" rather than "unset".
The watermark is what the host will actually use if it is left that way, which is why it is
bound rather than literal: it changes when the group picker above moves.
Username first and port 78 wide second, the mock's own order — reversed from the field's
earlier "port, username" layout, which put the number that is usually left blank ahead of
the one that usually is not. Both boxes are still allowed to be empty, and empty still means
"take the group's" rather than "unset"; the watermark is what the host will actually use if
it is left that way, which is why it is bound rather than literal — it changes when the
group picker above moves.
-->
<Grid ColumnDefinitions="*,8,*">
<NumericUpDown Grid.Column="0" Value="{Binding EditorPort}" Minimum="1" Maximum="65535"
FormatString="0" ShowButtonSpinner="False"
PlaceholderText="{Binding EditorPortPlaceholder}" />
<TextBox Grid.Column="2" Text="{Binding EditorUsername}"
PlaceholderText="{Binding EditorUsernamePlaceholder}" />
<Grid ColumnDefinitions="*,8,78">
<TextBox Grid.Column="0" Text="{Binding EditorUsername}"
PlaceholderText="{Binding EditorUsernamePlaceholder}" Height="40" />
<NumericUpDown Grid.Column="2" FontFamily="{StaticResource MonoFont}"
Value="{Binding EditorPort}" Minimum="1" Maximum="65535" FormatString="0"
ShowButtonSpinner="False" PlaceholderText="{Binding EditorPortPlaceholder}"
Height="40" />
</Grid>
<!--
@@ -500,7 +571,7 @@
-->
<ComboBox ItemsSource="{Binding EditorAuthenticationChoices}"
SelectedItem="{Binding EditorSelectedAuthentication}"
HorizontalAlignment="Stretch">
HorizontalAlignment="Stretch" Height="40">
<ComboBox.ItemTemplate>
<DataTemplate x:DataType="vm:AuthenticationChoice">
<StackPanel Orientation="Horizontal" Spacing="6">
@@ -513,26 +584,42 @@
</ComboBox.ItemTemplate>
</ComboBox>
<CheckBox IsChecked="{Binding EditorRelayEnabled}"
Content="Connect through the server relay (not wired up yet)" />
<!--
Stated at the moment the decision is made, which is the only place it means anything. With
relay off the server stores no address at all; with it on the server must be able to
resolve the target, or it becomes an authenticated open proxy into the operator's network.
◆ AND IT SAYS, FIRST, THAT TICKING IT BUYS NOTHING TODAY. The server half of the relay is
built and this client has no path to it: VaultViewModel dials the address directly whether
this is ticked or not. So the box as it stood spent the one deliberate privacy concession
in the design — the address in plaintext columns — and delivered a connection that failed
exactly as it had before, with nothing saying why.
Left tickable rather than disabled, because a host that already carries the flag has to be
able to lose it, and a disabled control would trap the concession on. See
docs/reaching-a-host-you-cannot-dial.md, which is the plan that makes this sentence
removable.
◆ THE RELAY CARD, restyled to the mock's nested-card shape — radius 12, a checkbox with the
title beside it rather than under it — but NOT to the mock's copy. The sentence stays
exactly what it was, "(not wired up yet)" included: the design's own wording implies a relay
this client can dial, and it cannot. See the remark under the hint below, which is unchanged
and still the reason this box says what it says.
-->
<TextBlock Classes="hint" FontSize="11"
Text="Not built yet: this client always dials the host itself, so ticking this stores the address on the server and changes nothing about how the host is reached. When it does work, the relay will dial on your behalf — which is why the address and port have to be stored in plain text. Everything else about the host stays encrypted either way." />
<Border CornerRadius="12" Background="{StaticResource Field}"
BorderBrush="{StaticResource Border}" BorderThickness="1" Padding="12">
<StackPanel Spacing="6">
<CheckBox IsChecked="{Binding EditorRelayEnabled}" VerticalContentAlignment="Center">
<TextBlock Text="Connect through the server relay (not wired up yet)" FontSize="13"
FontWeight="SemiBold" Foreground="{StaticResource Text}"
TextWrapping="Wrap" />
</CheckBox>
<!--
Stated at the moment the decision is made, which is the only place it means anything.
With relay off the server stores no address at all; with it on the server must be able
to resolve the target, or it becomes an authenticated open proxy into the operator's
network.
◆ AND IT SAYS, FIRST, THAT TICKING IT BUYS NOTHING TODAY. The server half of the relay
is built and this client has no path to it: VaultViewModel dials the address directly
whether this is ticked or not. So the box as it stood spent the one deliberate privacy
concession in the design — the address in plaintext columns — and delivered a
connection that failed exactly as it had before, with nothing saying why.
Left tickable rather than disabled, because a host that already carries the flag has to
be able to lose it, and a disabled control would trap the concession on. See
docs/reaching-a-host-you-cannot-dial.md, which is the plan that makes this sentence
removable.
-->
<TextBlock Classes="hint" FontSize="11.5"
Text="Not built yet: this client always dials the host itself, so ticking this stores the address on the server and changes nothing about how the host is reached. When it does work, the relay will dial on your behalf — which is why the address and port have to be stored in plain text. Everything else about the host stays encrypted either way." />
</StackPanel>
</Border>
<!--
Withdrawing host key trust lives here, in the host's own settings, because a changed host
@@ -550,6 +637,66 @@
</StackPanel>
</Border>
<!--
============ QUICK ACCESS ============
The folders pinned on this host. Staged on VaultViewModel.EditorPinnedPaths the way the tag
picker stages editorTagIds — see that field's own remarks — populated when the editor opens,
read back by BuildHost on Save, and left alone by CANCEL for the same reason every other field
here is: the whole editor is abandoned together.
-->
<Border Classes="section">
<StackPanel Spacing="8">
<TextBlock Classes="sectionlabel" Text="QUICK ACCESS" />
<ItemsControl ItemsSource="{Binding EditorPinnedPaths}">
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate><StackPanel Spacing="6" /></ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
<ItemsControl.ItemTemplate>
<DataTemplate x:DataType="x:String">
<Border Height="33" CornerRadius="9" Background="{StaticResource Field}"
BorderBrush="{StaticResource Border}" BorderThickness="1" Padding="8,0">
<Grid ColumnDefinitions="Auto,*,Auto">
<TextBlock Grid.Column="0" FontFamily="{StaticResource IconFont}" FontSize="14"
Foreground="{StaticResource TextFaint}" VerticalAlignment="Center"
Text="&#xE2C7;" />
<TextBlock Grid.Column="1" Classes="mono" Text="{Binding}" FontSize="12.5"
Margin="8,0" VerticalAlignment="Center"
TextTrimming="CharacterEllipsis" />
<!--
$parent[ItemsControl] rather than #Board: this ItemsControl's own DataContext is
already the VaultViewModel — HostDrawer's root x:DataType is that type directly —
so one level up is enough, the same single-hop case ToggleEditorTagCommand above
already uses.
-->
<Button Grid.Column="2" Classes="flat paneicon" Content="✕" Width="22" Height="22"
FontSize="11"
Command="{Binding $parent[ItemsControl].((vm:VaultViewModel)DataContext).RemoveEditorPinCommand}"
CommandParameter="{Binding}" ToolTip.Tip="Unpins this path" />
</Grid>
</Border>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
<Grid ColumnDefinitions="*,6,Auto">
<TextBox Grid.Column="0" FontFamily="{StaticResource MonoFont}"
Text="{Binding EditorNewPin}"
PlaceholderText="/path/to/folder" Height="36">
<TextBox.KeyBindings>
<KeyBinding Gesture="Enter" Command="{Binding AddEditorPinCommand}" />
</TextBox.KeyBindings>
</TextBox>
<Button Grid.Column="2" Classes="ghost" Content="+" Width="36" Height="36" FontSize="15"
HorizontalContentAlignment="Center"
Command="{Binding AddEditorPinCommand}" ToolTip.Tip="Pins this path" />
</Grid>
<TextBlock Classes="hint" FontSize="11.5" TextWrapping="Wrap"
Text="Pinned folders appear above the terminal for this host." />
</StackPanel>
</Border>
</StackPanel>
<!-- ============ THE GROUP EDITOR ============ -->
@@ -727,10 +874,17 @@
</StackPanel>
</StackPanel>
<StackPanel Orientation="Horizontal" Spacing="6" IsVisible="{Binding IsEditing}">
<Button Classes="accent" Content="SAVE" Command="{Binding SaveHostCommand}" />
<Button Classes="ghost" Content="CANCEL" Command="{Binding CancelEditCommand}" />
</StackPanel>
<!--
Save host flex-grows and Cancel is a fixed 92, the mock's own footer proportions — a Grid rather
than the StackPanel every other footer row uses, because a StackPanel measures each child at its
own size and has nothing that means "the rest of the row".
-->
<Grid ColumnDefinitions="*,8,92" IsVisible="{Binding IsEditing}">
<Button Grid.Column="0" Classes="accent" Content="Save host" Height="44"
HorizontalContentAlignment="Center" Command="{Binding SaveHostCommand}" />
<Button Grid.Column="2" Classes="ghost" Content="Cancel" Height="44"
HorizontalContentAlignment="Center" Command="{Binding CancelEditCommand}" />
</Grid>
<StackPanel Orientation="Horizontal" Spacing="6" IsVisible="{Binding IsEditingGroup}">
<Button Classes="accent" Content="{Binding GroupSaveLabel}"
+48 -1
View File
@@ -101,7 +101,53 @@
-->
<views:NavRail Grid.Column="0" IsVisible="{Binding IsVaultsTab}" />
<Panel Grid.Column="1">
<Grid Grid.Column="1" RowDefinitions="Auto,*">
<!--
◆ THE PIN STRIP, ABOVE THE TERMINAL AND NOTHING ELSE.
A Grid row rather than a sibling in the Panel below it — the terminal, the pages and the
connecting card are all layered on top of one another there, which is right for three things
that occupy the same rectangle and wrong for a row that is supposed to sit above it. Auto height
and IsVisible="False" collapse to nothing when there is nothing to show, so a page screen or an
empty terminal loses no height to a row it never draws — see ShowsPinStrip, which is false on
every page and every tab whose host has no pins.
ActiveTabPinnedPaths is MainWindowViewModel's, not the vault's: which tab is selected and
whether its session is live are the shell's business, and RefreshConnectedHosts is where the
two lists — Tabs and Vault.Hosts — are already walked together to paint the status dots. This
rides along on the same walk rather than opening a subscription of its own.
-->
<Border Grid.Row="0" Padding="14,8" Background="{StaticResource Panel}"
BorderBrush="{StaticResource Border}" BorderThickness="0,0,0,1"
IsVisible="{Binding ShowsPinStrip, FallbackValue=False}">
<ItemsControl ItemsSource="{Binding ActiveTabPinnedPaths}">
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate><StackPanel Orientation="Horizontal" Spacing="6" /></ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
<ItemsControl.ItemTemplate>
<DataTemplate x:DataType="x:String">
<!--
A single level of $parent[ItemsControl] is enough to reach back to the shell: this
ItemsControl's own DataContext is MainWindowViewModel, the window's root, so one hop up
from the chip's string DataContext lands on it directly — no #Board-style named-element
indirection needed, because there is no second level of template nesting here.
-->
<Button Classes="ghost" Padding="8,4" ToolTip.Tip="{Binding}"
Command="{Binding $parent[ItemsControl].((vm:MainWindowViewModel)DataContext).OpenPinnedPathCommand}"
CommandParameter="{Binding}">
<StackPanel Orientation="Horizontal" Spacing="5">
<TextBlock Text="&#xE2C7;" FontFamily="{StaticResource IconFont}" FontSize="12" />
<TextBlock FontFamily="{StaticResource MonoFont}" Text="{Binding}" FontSize="11.5"
MaxWidth="220" TextTrimming="CharacterEllipsis" />
</StackPanel>
</Button>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</Border>
<Panel Grid.Row="1">
<!-- ============ THE PAGES ============ -->
<Panel IsVisible="{Binding IsShowingPages}">
@@ -232,6 +278,7 @@
IsVisible="{Binding IsTerminalShowing, FallbackValue=False}" />
</Panel>
</Grid>
</Grid>
</Grid>