Restyle the keychain, the snips and the logs to their v5b shapes

This commit is contained in:
2026-08-08 00:49:59 +02:00
parent 43c939b697
commit d91729c0b8
3 changed files with 1117 additions and 794 deletions
+295 -185
View File
@@ -26,17 +26,117 @@
when somebody is in a team — but it is still not a selector, because every table on this screen already
spans every keychain this session holds a key for and each row names its own. What it carries instead is
the one keychain question with an answer: where a new item is filed.
── v5b — Keychain.dc.html ──────────────────────────────────────────────────────────────────────────────
A fidelity pass over the shape above, not a new one. What moved:
◆ THE HEADER. "Keychain" 33 bold, a count chip, and on the right a "Host keys" ghost button — the
design's own doorway to the pins screen, wired to the shell's existing ShowScreenCommand via
$parent[Window] since this screen's own DataContext is the vault rather than the shell — and one
"+ New key" accent button rather than the five GENERATE / + SSH KEY / + PASSWORD / + TAG / + BUCKET
buttons the toolbar used to spread across the table's own header. ◆ DECIDED DEVIATION: the design draws
one button because its mock has one "add" concept; this application has five, and folding five capabilities
into one visible button without losing any of them means the button opens a menu naming all five, in the
same order the old toolbar had them, rather than guessing which one the design's single button "really"
meant. GENERATE keeps its own tooltip; the strip's own essay comment about why it lost the word KEY is now
moot — the width pressure that produced it left with the buttons.
◆ THE RAIL. 200px, Sidebar-bg, KEYCHAIN tracked label, category rows restyled to Button.cat's own idiom
(unchanged binding, new width). Below the divider: SCOPES stays — this session's current vault, named,
since that fact is real and the design's mock is a single-vault sample with nothing to show it — and then
NEW ITEMS FILE TO, the design's own wording for the picker this screen already had as "NEW ITEMS GO TO".
Adopted rather than kept: nothing in this file's own essay comments ever defended "GO TO" over "FILE TO",
and the comment above the picker already says "where a new item is filed" — the design's word was this
screen's own vocabulary already.
◆ THE TABLE. A 44px sub-toolbar (the section summary, left; a 240px filter box, right — this table had no
filter box before, and the design's has one) and tracked column headers. The design's own five are
NAME/TYPE/VAULT/USED BY/MODIFIED; MODIFIED is dropped — VaultItem carries no timestamp of any kind (id,
secret, version, three sync flags — see docs/design-import-gaps.md) — and USED BY is real for a key or a
credential (which hosts authenticate with it, the same fact VaultViewModel.HostsBoundTo already computes
for the deletion warning) and empty for a tag (its own host count already covers the same ground) or a
bucket (nothing in this codebase resolves a host's authentication to an object store). Rows: a type glyph,
the mono name, the type word, and the vault chip — VaultItemRowViewModel.VaultBadge, empty except where
more than one vault is in play, the same convention every other list in this application follows.
◆ THE DETAIL PANE, 300px. PUBLIC KEY draws when the selected key has one stored. FINGERPRINT is not
drawn at all — this codebase has never computed one; see docs/design-import-gaps.md's own recorded gap,
"no algorithm field, no fingerprint, and computing either means parsing armour the type stores verbatim."
USED BY draws real host rows — the same HostsBoundTo scan, with each host's own two-state dot — only for
a key or a credential, and only while at least one host actually authenticates with the selected item; the
"in use · N hosts" chip beside the vault chip is gated the same way. The action buttons keep their
existing commands and their existing honesty: COPY PUBLIC KEY only for a key, MOVE only where
CanMoveSelectedItem says there is somewhere to move to, DELETE always. "Choose something on the left…"
stays as the empty state, restyled.
-->
<Grid ColumnDefinitions="176,*,244">
<Grid RowDefinitions="Auto,*" Margin="26">
<!-- Categories and scopes -->
<!-- ============ THE HEADER ============ -->
<Grid Grid.Row="0" Margin="0,0,0,20" ColumnDefinitions="Auto,Auto,*,Auto,Auto">
<TextBlock Grid.Column="0" Text="Keychain" FontSize="33" FontWeight="Bold" LetterSpacing="-0.5"
Foreground="{StaticResource Text}" VerticalAlignment="Center" />
<Border Grid.Column="1" Margin="12,0,0,0" MinWidth="34" Height="30" CornerRadius="9" Padding="8,0"
Background="{StaticResource Chip}" VerticalAlignment="Center">
<TextBlock Classes="mono" Text="{Binding TotalItemCount}" FontSize="12.5"
Foreground="{StaticResource TextDim}" HorizontalAlignment="Center"
VerticalAlignment="Center" />
</Border>
<!--
◆ "Host keys", to the pins screen. $parent[Window] is what reaches the shell from here: this
control's own DataContext is the vault, not MainWindowViewModel, and Window is the nearest ancestor
whose DataContext is the shell — see MainWindow.axaml, which sets this screen's DataContext to
{Binding Vault} rather than putting IsKeychainScreen and the vault on one element.
-->
<Button Grid.Column="3" Classes="ghost" Height="40" Margin="0,0,10,0"
Command="{Binding $parent[Window].((vm:MainWindowViewModel)DataContext).ShowScreenCommand}"
CommandParameter="{x:Static vm:ShellScreen.KnownHosts}"
ToolTip.Tip="Host keys you have approved, and how to withdraw one">
<StackPanel Orientation="Horizontal" Spacing="8">
<TextBlock FontFamily="{StaticResource IconFont}" FontSize="15" Text="&#xE90D;"
Foreground="{StaticResource TextFaint}" />
<TextBlock Text="Host keys" FontSize="13.5" FontWeight="SemiBold"
Foreground="{StaticResource TextDim}" />
</StackPanel>
</Button>
<!--
◆ "+ New key": one button standing in for the five this screen can still do. See the file-level
remark above for why a menu rather than a guess at which one the design meant.
-->
<Button Grid.Column="4" Classes="accent" Height="40" FontSize="13.5" Content="+ New key">
<Button.Flyout>
<MenuFlyout Placement="BottomEdgeAlignedRight">
<MenuItem Header="Generate a key…" Command="{Binding NewGeneratedKeyCommand}"
ToolTip.Tip="Makes a new key pair here, so the private half never becomes a file on this disk." />
<MenuItem Header="Add SSH key…" Command="{Binding NewKeyCommand}"
ToolTip.Tip="Pastes in a key you already have." />
<MenuItem Header="Add password…" Command="{Binding NewCredentialCommand}" />
<MenuItem Header="Add tag…" Command="{Binding NewTagCommand}"
ToolTip.Tip="A name to put on hosts. Usually made from a host's editor instead; this is for setting a scheme up before there is anything to put it on." />
<MenuItem Header="Add bucket…" Command="{Binding NewObjectStoreCommand}"
ToolTip.Tip="An S3-compatible bucket, to browse beside a host on the Files screen." />
</MenuFlyout>
</Button.Flyout>
</Button>
</Grid>
<!-- ============ THE BORDERED BODY ============ -->
<Border Grid.Row="1" BorderBrush="{StaticResource Border}" BorderThickness="1" CornerRadius="12"
ClipToBounds="True">
<Grid ColumnDefinitions="200,*,300">
<!-- ============ Categories and scopes ============ -->
<Border Grid.Column="0" Background="{StaticResource Sidebar}"
BorderBrush="{StaticResource Border}" BorderThickness="0,0,1,0">
<ScrollViewer>
<StackPanel Margin="0,12">
<StackPanel Margin="10,16">
<TextBlock Classes="label" Text="KEYCHAIN" Margin="14,0,14,8" />
<TextBlock Classes="label" Text="KEYCHAIN" Margin="12,0,12,10" FontSize="10" />
<Button Classes="flat cat" Command="{Binding ShowSectionCommand}"
CommandParameter="{x:Static vm:VaultSection.All}"
@@ -44,7 +144,7 @@
<Grid ColumnDefinitions="Auto,*,Auto">
<Border Grid.Column="0" Classes="rowmark catmark" />
<TextBlock Grid.Column="1" Text="ALL" Margin="12,0,0,0" />
<TextBlock Grid.Column="2" Text="{Binding TotalItemCount}"
<TextBlock Grid.Column="2" Classes="mono" Text="{Binding TotalItemCount}"
Foreground="{StaticResource TextFaint}" />
</Grid>
</Button>
@@ -55,7 +155,7 @@
<Grid ColumnDefinitions="Auto,*,Auto">
<Border Grid.Column="0" Classes="rowmark catmark" />
<TextBlock Grid.Column="1" Text="SSH KEYS" Margin="12,0,0,0" />
<TextBlock Grid.Column="2" Text="{Binding Keys.Count}"
<TextBlock Grid.Column="2" Classes="mono" Text="{Binding Keys.Count}"
Foreground="{StaticResource TextFaint}" />
</Grid>
</Button>
@@ -66,21 +166,21 @@
<Grid ColumnDefinitions="Auto,*,Auto">
<Border Grid.Column="0" Classes="rowmark catmark" />
<TextBlock Grid.Column="1" Text="PASSWORDS" Margin="12,0,0,0" />
<TextBlock Grid.Column="2" Text="{Binding Credentials.Count}"
<TextBlock Grid.Column="2" Classes="mono" Text="{Binding Credentials.Count}"
Foreground="{StaticResource TextFaint}" />
</Grid>
</Button>
<!--
Buckets. A category here rather than a screen of its own, unlike the approved host keys: a bucket
is something somebody creates, edits and keeps a secret for, which is what the other two
categories are. A pin is a decision recorded at connect time and is not.
Buckets. A category here rather than a screen of its own, unlike the approved host keys: a
bucket is something somebody creates, edits and keeps a secret for, which is what the other
two categories are. A pin is a decision recorded at connect time and is not.
-->
<!--
Tags. The odd category: it is the only one holding nothing secret — a tag is a name. It is here
because the reason a tag is an item at all is that renaming it should be one write instead of
twenty, and a rename needs somewhere to happen; so does deleting, or the host editor's picker
fills with names nobody uses and never empties.
Tags. The odd category: it is the only one holding nothing secret — a tag is a name. It is
here because the reason a tag is an item at all is that renaming it should be one write
instead of twenty, and a rename needs somewhere to happen; so does deleting, or the host
editor's picker fills with names nobody uses and never empties.
-->
<Button Classes="flat cat" Command="{Binding ShowSectionCommand}"
CommandParameter="{x:Static vm:VaultSection.Tags}"
@@ -88,7 +188,7 @@
<Grid ColumnDefinitions="Auto,*,Auto">
<Border Grid.Column="0" Classes="rowmark catmark" />
<TextBlock Grid.Column="1" Text="TAGS" Margin="12,0,0,0" />
<TextBlock Grid.Column="2" Text="{Binding Tags.Count}"
<TextBlock Grid.Column="2" Classes="mono" Text="{Binding Tags.Count}"
Foreground="{StaticResource TextFaint}" />
</Grid>
</Button>
@@ -99,33 +199,35 @@
<Grid ColumnDefinitions="Auto,*,Auto">
<Border Grid.Column="0" Classes="rowmark catmark" />
<TextBlock Grid.Column="1" Text="BUCKETS" Margin="12,0,0,0" />
<TextBlock Grid.Column="2" Text="{Binding ObjectStores.Count}"
<TextBlock Grid.Column="2" Classes="mono" Text="{Binding ObjectStores.Count}"
Foreground="{StaticResource TextFaint}" />
</Grid>
</Button>
<Border Height="1" Background="{StaticResource BorderSubtle}" Margin="14,10" />
<Border Height="1" Background="{StaticResource BorderSubtle}" Margin="12,12" />
<TextBlock Classes="label" Text="SCOPES" Margin="14,0,14,8" />
<TextBlock Classes="label" Text="SCOPES" Margin="12,0,12,8" FontSize="10" />
<!--
Still not a selector. Every list on this screen now spans every vault this session holds a key
for, and each row names its own vault — so there is nothing to switch to. What the picker below
chooses is where a *new* item is filed, which is a different question and the only one that has
an answer worth asking for.
Still not a selector. Every list on this screen now spans every vault this session holds a
key for, and each row names its own vault — so there is nothing to switch to. What the
picker below chooses is where a *new* item is filed, which is a different question and the
only one that has an answer worth asking for.
-->
<StackPanel Orientation="Horizontal" Margin="14,2" Spacing="7">
<StackPanel Orientation="Horizontal" Margin="12,2" Spacing="7">
<Ellipse Width="6" Height="6" Fill="{StaticResource Accent}" VerticalAlignment="Center" />
<TextBlock Classes="mono" Text="{Binding HostsHeading}" FontSize="11"
Foreground="{StaticResource Text}" VerticalAlignment="Center" />
</StackPanel>
<!--
Hidden at one vault, which is where most people stay. A control offering a single option is a
question with no answer.
Hidden at one vault, which is where most people stay. A control offering a single option is
a question with no answer. "NEW ITEMS FILE TO" — the design's own wording, adopted: this
screen's own comments already call the act "filing" (see above), so the design's label was
this application's own vocabulary already.
-->
<StackPanel Margin="14,10,14,0" Spacing="4" IsVisible="{Binding HasVaultChoice}">
<TextBlock Classes="label" Text="NEW ITEMS GO TO" />
<StackPanel Margin="12,14,12,0" Spacing="4" IsVisible="{Binding HasVaultChoice}">
<TextBlock Classes="label" Text="NEW ITEMS FILE TO" FontSize="10" />
<ComboBox ItemsSource="{Binding TargetVaults}"
SelectedItem="{Binding SelectedTargetVault}"
HorizontalAlignment="Stretch">
@@ -140,11 +242,11 @@
</StackPanel>
<!--
Items that would not decrypt. Shown here rather than only in the status line because this is the
screen the number is about, and because a non-zero count after a rekey is the signal that new
grants are needed rather than a transient.
Items that would not decrypt. Shown here rather than only in the status line because this is
the screen the number is about, and because a non-zero count after a rekey is the signal that
new grants are needed rather than a transient.
-->
<Border Classes="chip warn" Margin="14,12,14,0" HorizontalAlignment="Left"
<Border Classes="chip warn" Margin="12,14,12,0" HorizontalAlignment="Left"
IsVisible="{Binding HasUnreadableItems}">
<TextBlock Text="{Binding UnreadableSummary}" />
</Border>
@@ -153,99 +255,67 @@
</ScrollViewer>
</Border>
<!-- The table -->
<Grid Grid.Column="1" RowDefinitions="Auto,Auto,*">
<!-- ============ The table ============ -->
<Grid Grid.Column="1" RowDefinitions="Auto,Auto,*" Background="{StaticResource Pane}">
<Border Grid.Row="0" Padding="14,0" Height="44"
<Border Grid.Row="0" Padding="16,0" Height="44"
BorderBrush="{StaticResource Border}" BorderThickness="0,0,0,1">
<!--
◆ THE SUMMARY IS THE COLUMN THAT GIVES WAY, and that is what stops this header overflowing again.
It used to be Auto,Auto,*,Auto — a fixed title, a fixed summary, slack, then the buttons — so the
slack column was the only thing absorbing a change of width, and the strip fell off the right edge
the moment the five buttons wanted more than it had. That is not hypothetical: it is why GENERATE
lost the word KEY (see below), and it happened again the moment the type scale went up a point.
Buying pixels by shortening a caption fixes one instance of a shape that keeps producing them.
So the summary sits in the star column and trims, and the buttons are Auto and always get their
full width. That is the rule worth encoding rather than the pixels: a trimmed summary is a fact
you can read by widening the window, and a clipped button is a dead end. The titlebar's search box
is arranged this way for the same reason.
-->
<Grid ColumnDefinitions="Auto,*,Auto" VerticalAlignment="Center">
<TextBlock Grid.Column="0" Classes="mono" Text="{Binding SectionTitle}" FontSize="12"
FontWeight="SemiBold" LetterSpacing="1" Foreground="{StaticResource Text}"
VerticalAlignment="Center" />
<TextBlock Grid.Column="1" Classes="mono" Text="{Binding SectionSummary}" FontSize="10.5"
Foreground="{StaticResource TextFaint}" Margin="10,0,10,0" VerticalAlignment="Center"
<Grid ColumnDefinitions="*,Auto" VerticalAlignment="Center">
<TextBlock Grid.Column="0" Classes="mono" Text="{Binding SectionSummary}" FontSize="10.5"
Foreground="{StaticResource TextFaint}" Margin="0,0,10,0" VerticalAlignment="Center"
TextTrimming="CharacterEllipsis" />
<StackPanel Grid.Column="2" Orientation="Horizontal" Spacing="6">
<!--
Always offered. Every category left on this screen is one things can be added to — the one
that was not, HOST KEYS, is now its own screen, and a pin still cannot be typed in there
either. See KnownHostsScreen.
GENERATE carries no KEY, which it lost when a fifth button arrived and the strip could still
overflow. The arrangement above is what keeps it inside now; the short caption stays because
its tooltip carries what the word did and the two key buttons are adjacent, so which one
generates is not in doubt.
-->
<Button Classes="ghost" Content="GENERATE" Command="{Binding NewGeneratedKeyCommand}"
ToolTip.Tip="Makes a new key pair here, so the private half never becomes a file on this disk." />
<Button Classes="ghost" Content="+ SSH KEY" Command="{Binding NewKeyCommand}"
ToolTip.Tip="Pastes in a key you already have." />
<Button Classes="ghost" Content="+ PASSWORD" Command="{Binding NewCredentialCommand}" />
<Button Classes="ghost" Content="+ TAG" Command="{Binding NewTagCommand}"
ToolTip.Tip="A name to put on hosts. Usually made from a host's editor instead; this is for setting a scheme up before there is anything to put it on." />
<Button Classes="accent" Content="+ BUCKET" Command="{Binding NewObjectStoreCommand}"
ToolTip.Tip="An S3-compatible bucket, to browse beside a host on the Files screen." />
</StackPanel>
<TextBox Grid.Column="1" x:Name="ItemFilterBox" Text="{Binding ItemFilter}" Width="240"
Height="30" CornerRadius="9" PlaceholderText="filter items"
VerticalAlignment="Center" />
</Grid>
</Border>
<!--
The design's columns are NAME / TYPE / FINGERPRINT / SCOPE / ACCESS / LAST. Three of those six have
nothing behind them: there is one scope, no roles, and no item carries a last-used or modified time —
VaultItem is (id, secret, version, three sync flags) and nothing else. What replaces them is the one
thing this build does know and the design had no column for: whether a change is still sitting in
this machine's outbox.
The design's columns are NAME / TYPE / VAULT / USED BY / MODIFIED. MODIFIED is not here — no
item this application stores carries a timestamp; see the file-level remark above and
docs/design-import-gaps.md.
-->
<Grid Grid.Row="1" ColumnDefinitions="2,1.3*,74,*,88" Margin="0,6,14,6"
<Grid Grid.Row="1" ColumnDefinitions="2,2.2*,1*,1*,1.4*" Margin="0,10,16,6"
IsVisible="{Binding HasVaultItems}">
<TextBlock Grid.Column="1" Classes="label" Text="NAME" FontSize="9.5" LetterSpacing="1"
Margin="12,0,8,0" />
<TextBlock Grid.Column="2" Classes="label" Text="TYPE" FontSize="9.5" LetterSpacing="1" />
<TextBlock Grid.Column="3" Classes="label" Text="DETAIL" FontSize="9.5" LetterSpacing="1" />
<TextBlock Grid.Column="4" Classes="label" Text="STATE" FontSize="9.5" LetterSpacing="1" />
<TextBlock Grid.Column="3" Classes="label" Text="VAULT" FontSize="9.5" LetterSpacing="1" />
<TextBlock Grid.Column="4" Classes="label" Text="USED BY" FontSize="9.5" LetterSpacing="1" />
</Grid>
<ListBox Grid.Row="2" x:Name="ItemList" Focusable="True"
<ListBox Grid.Row="2" x:Name="ItemList" Classes="filerows" Focusable="True"
ItemsSource="{Binding VaultItems}"
SelectedItem="{Binding SelectedVaultItem}">
SelectedItem="{Binding SelectedVaultItem}"
Margin="8,0,8,10">
<ListBox.ItemTemplate>
<DataTemplate x:DataType="vm:VaultItemRowViewModel">
<Grid ColumnDefinitions="2,1.3*,74,*,88" Margin="0,7,14,7">
<Border Grid.Column="0" Classes="rowmark" />
<TextBlock Grid.Column="1" Classes="mono" Text="{Binding Name}" FontSize="12"
FontWeight="Medium" Foreground="{StaticResource Text}" Margin="12,0,8,0"
TextTrimming="CharacterEllipsis" />
<TextBlock Grid.Column="2" Classes="mono" Text="{Binding Type}" FontSize="10"
Foreground="{StaticResource TextDim}" VerticalAlignment="Center" />
<TextBlock Grid.Column="3" Classes="mono" Text="{Binding Detail}" FontSize="10.5"
Foreground="{StaticResource TextFaint}" Margin="0,0,8,0"
TextTrimming="CharacterEllipsis" VerticalAlignment="Center" />
<Border Grid.Column="4" Classes="chip warn" HorizontalAlignment="Left"
VerticalAlignment="Center" IsVisible="{Binding HasBadge}">
<TextBlock Text="{Binding Badge}" FontSize="9.5" />
<Grid ColumnDefinitions="2,2.2*,1*,1*,1.4*" Height="40" Margin="6,0,10,0">
<StackPanel Grid.Column="1" Orientation="Horizontal" Spacing="10" Margin="10,0,8,0"
VerticalAlignment="Center">
<TextBlock FontFamily="{StaticResource IconFont}" FontSize="15"
Text="{Binding IconGlyph}" Foreground="{StaticResource AccentText}"
VerticalAlignment="Center" />
<TextBlock Classes="mono" Text="{Binding Name}" FontSize="12.5" FontWeight="Medium"
Foreground="{StaticResource Text}" TextTrimming="CharacterEllipsis" />
</StackPanel>
<TextBlock Grid.Column="2" Classes="mono" Text="{Binding Type}" FontSize="10.5"
Foreground="{StaticResource TextGhost}" VerticalAlignment="Center" />
<Border Grid.Column="3" Classes="chip" HorizontalAlignment="Left"
VerticalAlignment="Center" IsVisible="{Binding HasVaultBadge}">
<TextBlock Text="{Binding VaultBadge}" FontSize="10" />
</Border>
<TextBlock Grid.Column="4" Classes="mono" Text="{Binding UsedBySummary}" FontSize="10.5"
Foreground="{StaticResource TextFaint}" VerticalAlignment="Center"
TextTrimming="CharacterEllipsis" IsVisible="{Binding HasUsedBySummary}" />
</Grid>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
<!--
The empty state says which category is empty and what to do about it, rather than showing an empty
grid that reads as a list still loading.
The empty state says which category is empty and what to do about it, rather than showing an
empty grid that reads as a list still loading.
-->
<TextBlock Grid.Row="2" Classes="hint" Text="{Binding EmptySectionMessage}" FontSize="12"
Margin="24" HorizontalAlignment="Center" VerticalAlignment="Center"
@@ -254,54 +324,107 @@
</Grid>
<!-- The detail pane, and the editors -->
<!-- ============ The detail pane, and the editors ============ -->
<Border Grid.Column="2" Background="{StaticResource Sidebar}"
BorderBrush="{StaticResource Border}" BorderThickness="1,0,0,0">
<ScrollViewer>
<StackPanel Margin="14,16">
<StackPanel Margin="18,20" Spacing="14">
<!-- Nothing selected. -->
<TextBlock Classes="hint" FontSize="12"
Text="Choose something on the left to see what is known about it."
IsVisible="{Binding !HasSelectedVaultItem}" />
<StackPanel Spacing="6" IsVisible="{Binding HasSelectedVaultItem}">
<TextBlock Classes="mono" Text="{Binding SelectedVaultItem.Name}" FontSize="13"
FontWeight="SemiBold" Foreground="{StaticResource Text}" TextWrapping="Wrap" />
<StackPanel Spacing="14" IsVisible="{Binding HasSelectedVaultItem}">
<StackPanel Orientation="Horizontal" Spacing="12">
<Border Width="36" Height="36" CornerRadius="10" Background="{StaticResource AccentWash}"
VerticalAlignment="Center">
<TextBlock FontFamily="{StaticResource IconFont}" FontSize="17"
Text="{Binding SelectedVaultItem.IconGlyph}"
Foreground="{StaticResource AccentText}"
HorizontalAlignment="Center" VerticalAlignment="Center" />
</Border>
<StackPanel Spacing="4" VerticalAlignment="Center">
<TextBlock Classes="mono" Text="{Binding SelectedVaultItem.Name}" FontSize="13.5"
FontWeight="Bold" Foreground="{StaticResource Text}" TextWrapping="Wrap" />
<TextBlock Classes="mono" Text="{Binding SelectedVaultItem.Detail}" FontSize="10.5"
Foreground="{StaticResource TextGhost}" TextWrapping="Wrap" />
</StackPanel>
</StackPanel>
<StackPanel Orientation="Horizontal" Spacing="6">
<Border Classes="chip">
<TextBlock Text="{Binding SelectedVaultItem.Type}" />
</Border>
<Border Classes="chip accent">
<TextBlock Text="{Binding HostsHeading}" />
<!--
"in use · N hosts" — real only for a key or a credential with at least one host actually
bound to it, off the same VaultViewModel.HostsBoundTo scan the USED BY list below reads.
-->
<Border CornerRadius="5" Background="{StaticResource LiveWash}" Padding="9,0" Height="21"
VerticalAlignment="Center" IsVisible="{Binding HasSelectedItemInUseSummary}">
<TextBlock Text="{Binding SelectedItemInUseSummary}" FontSize="11" FontWeight="Medium"
Foreground="{StaticResource Live}" VerticalAlignment="Center" />
</Border>
</StackPanel>
<TextBlock Classes="label" Text="{Binding SelectedDetailHeading}" Margin="0,12,0,4" />
<Border Background="{StaticResource Raised}" BorderBrush="{StaticResource Border}"
BorderThickness="1" CornerRadius="4" Padding="8">
<SelectableTextBlock Classes="mono" Text="{Binding SelectedVaultItem.Detail}"
FontSize="10.5" Foreground="{StaticResource TextDim}"
TextWrapping="Wrap" />
<!--
PUBLIC KEY, when the selected key has one stored — never the private half. FINGERPRINT is
not drawn: this codebase has never computed one, and computing it here would mean parsing
armour the type stores verbatim; see docs/design-import-gaps.md's own recorded gap.
-->
<StackPanel Spacing="6" IsVisible="{Binding SelectedItemIsKey}">
<TextBlock Classes="label" Text="PUBLIC KEY" FontSize="10" />
<Border CornerRadius="10" Background="{StaticResource Pane}"
BorderBrush="{StaticResource BorderMid}" BorderThickness="1" Padding="12,10"
IsVisible="{Binding SelectedKey.Key.PublicKey, Converter={x:Static StringConverters.IsNotNullOrEmpty}, FallbackValue=False}">
<SelectableTextBlock Classes="mono" Text="{Binding SelectedKey.Key.PublicKey}"
FontSize="10.5" LineHeight="17"
Foreground="{StaticResource TextGhost}" TextWrapping="Wrap" />
</Border>
<TextBlock Classes="hint" FontSize="10.5"
Text="No public half is stored for this key."
IsVisible="{Binding !SelectedKey.Key.PublicKey, FallbackValue=False}" />
</StackPanel>
<!--
What the design puts here — who added it, when, who it is shared with, and a TEST CONNECT
button — has nothing behind it. Items carry no author, no timestamps and no sharing, and
nothing can exercise a credential without a host to exercise it against. Rather than five
empty rows, this says what is missing in one line.
USED BY: real host rows, off the same scan the "in use" chip above reads, with each host's
own two-state dot — never a third colour, since nothing here pings anything.
-->
<TextBlock Classes="hint" FontSize="10.5" Margin="0,12,0,0"
Text="Keychain items record no author, no timestamps and no sharing yet, so there is nothing more to show here." />
<StackPanel Spacing="6" IsVisible="{Binding HasSelectedItemUsedByHosts}">
<TextBlock Classes="label" Text="USED BY" FontSize="10" />
<StackPanel Spacing="2">
<ItemsControl ItemsSource="{Binding SelectedItemUsedByHosts}">
<ItemsControl.ItemTemplate>
<DataTemplate x:DataType="vm:UsedByHostRowViewModel">
<StackPanel Orientation="Horizontal" Spacing="9" Height="28">
<Ellipse Classes="dot" Classes.live="{Binding IsConnected}"
VerticalAlignment="Center" />
<TextBlock Classes="mono" Text="{Binding Label}" FontSize="12"
Foreground="{StaticResource TextFaint}" VerticalAlignment="Center" />
</StackPanel>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</StackPanel>
</StackPanel>
<StackPanel Orientation="Horizontal" Spacing="6" Margin="0,14,0,0"
IsVisible="{Binding ShowsItemActions}">
<!--
What the design puts here beyond what is above — who added it, when, and a TEST CONNECT
button — has nothing behind it: items carry no author or timestamp, and nothing can
exercise a credential without a host to exercise it against.
-->
<TextBlock Classes="hint" FontSize="10.5"
Text="Keychain items record no author or timestamp yet, so there is nothing more to show here." />
<StackPanel Orientation="Horizontal" Spacing="6" IsVisible="{Binding ShowsItemActions}">
<Button Classes="ghost" Content="EDIT" Command="{Binding EditSelectedItemCommand}" />
<!--
Only where there is somewhere to move to, unlike EDIT beside it, which is the same rule the
host's MOVE follows on the phone: a button that answers with "this is the only vault you can
write to" is a button that should not have been drawn. Keys and passwords only — a tag and a
bucket are read from the active vault alone, so "another vault" is not a question they have.
Only where there is somewhere to move to, unlike EDIT beside it, which is the same rule
the host's MOVE follows on the phone: a button that answers with "this is the only vault
you can write to" is a button that should not have been drawn. Keys and passwords only —
a tag and a bucket are read from the active vault alone, so "another vault" is not a
question they have.
-->
<Button Classes="ghost" Content="MOVE" Command="{Binding MoveSelectedItemCommand}"
IsVisible="{Binding CanMoveSelectedItem}"
@@ -310,17 +433,23 @@
</StackPanel>
<!--
◆ MOVING THE ITEM TO ANOTHER VAULT, in the place those buttons were. The host's panel, over
here — see HostDrawer.axaml — and what it is for is the thing a shared vault could not do until
now: a key typed into a personal vault before the team existed was stuck there, and the only
way across was to paste the private half into a second item and delete the first.
The two sentences under the picker are the whole of the decision. The first says what a move
is; the second says what points at this key, because everything that does is re-aimed at it in
its new vault and somebody moving a key twenty machines use should see the twenty first.
The public half only, and there is no button for the other one. Installing a key means
pasting this line into a host's authorized_keys; a private key on the clipboard is a
private key in every application on the machine.
-->
<StackPanel Spacing="8" Margin="0,14,0,0" IsVisible="{Binding IsMovingItem}">
<TextBlock Classes="label" Text="MOVE TO VAULT" />
<Button Classes="ghost" Content="Copy public key" HorizontalAlignment="Left"
IsVisible="{Binding SelectedItemIsKey}"
Command="{Binding CopyPublicKeyCommand}"
ToolTip.Tip="Copies the authorized_keys line for this key, which is what a host needs to let it in." />
<!--
◆ MOVING THE ITEM TO ANOTHER VAULT, in the place those buttons were. See HostDrawer.axaml —
the thing a shared vault could not do until now: a key typed into a personal vault before
the team existed was stuck there, and the only way across was to paste the private half
into a second item and delete the first.
-->
<StackPanel Spacing="8" IsVisible="{Binding IsMovingItem}">
<TextBlock Classes="label" Text="MOVE TO VAULT" FontSize="10" />
<ComboBox HorizontalAlignment="Stretch" ItemsSource="{Binding MoveItemVaultChoices}"
SelectedItem="{Binding SelectedMoveItemVault}">
<ComboBox.ItemTemplate>
@@ -342,24 +471,11 @@
</StackPanel>
<!--
The public half only, and there is no button for the other one. Installing a key means pasting
this line into a host's authorized_keys; a private key on the clipboard is a private key in
every application on the machine.
-->
<Button Classes="ghost" Content="COPY PUBLIC KEY" Margin="0,6,0,0"
HorizontalAlignment="Left"
IsVisible="{Binding SelectedItemIsKey}"
Command="{Binding CopyPublicKeyCommand}"
ToolTip.Tip="Copies the authorized_keys line for this key, which is what a host needs to let it in." />
<!--
The question DELETE asks, in the place those two buttons were. Here rather than over the
screen, because this pane is where the item being deleted is described: the name, the kind and
what is stored are all still on screen above it, which is most of what somebody checks before
answering. See ConfirmDeleteCard.
The question DELETE asks, in the place the buttons above were. Here rather than over the
screen, because this pane is where the item being deleted is described.
-->
<Border Background="{StaticResource DangerWash}" BorderBrush="{StaticResource DangerSoft}"
BorderThickness="1" CornerRadius="4" Padding="10" Margin="0,14,0,0"
BorderThickness="1" CornerRadius="10" Padding="12"
IsVisible="{Binding IsConfirmingDeletion}">
<views:ConfirmDeleteCard />
</Border>
@@ -367,19 +483,13 @@
</StackPanel>
<!--
Making a key, as opposed to pasting one in. A step of its own and a short one: an algorithm, a
comment, and a button. What it produces lands in the editor below, unsaved — so there is still
exactly one thing on this screen that writes a key, and it is still SAVE.
Making a key, as opposed to pasting one in. A step of its own and a short one: an algorithm,
a comment, and a button.
-->
<StackPanel Spacing="6" IsVisible="{Binding IsGeneratingKey}">
<TextBlock Classes="label" Text="NEW SSH KEY" Margin="0,0,0,4" />
<TextBlock Classes="label" Text="NEW SSH KEY" FontSize="10" />
<StackPanel Orientation="Horizontal" Spacing="6">
<!--
Buttons and a command rather than a selector bound to the algorithm, which is the same
choice the category rail makes and for the same reason: a selector moves its own highlight
before anything can refuse, so it can end up showing a choice nobody made.
-->
<Button Classes="flat choice" Content="ED25519"
Classes.active="{Binding GeneratesEd25519}"
Command="{Binding ChooseKeyAlgorithmCommand}"
@@ -396,11 +506,6 @@
<TextBlock Classes="hint" FontSize="10.5" TextWrapping="Wrap"
Text="This is what the key is called here and what is written into it, so the line on a host says where it came from." />
<!--
Said plainly rather than left to be discovered. Writing an encrypted openssh-key-v1 file needs
bcrypt_pbkdf, which .NET has no primitive for — and the defence it buys is one this product
already makes: a passphrase protects a key file on a disk, and this key is never on one.
-->
<TextBlock Classes="hint" FontSize="10.5" TextWrapping="Wrap" Margin="0,4,0,0"
Text="The key file itself has no passphrase. Your keychain passphrase is what protects it, and it never reaches the server in a form it can read." />
@@ -413,12 +518,12 @@
<!-- The key editor. -->
<StackPanel Spacing="6" IsVisible="{Binding IsEditingKey}">
<TextBlock Classes="label" Text="SSH KEY" Margin="0,0,0,4" />
<TextBlock Classes="label" Text="SSH KEY" FontSize="10" />
<TextBox Text="{Binding KeyEditorLabel}" PlaceholderText="name" />
<!--
Not a password box. The armour has to be visible to be pasted and checked — a masked
multi-line box makes "did the whole key arrive?" unanswerable — and the mistake this actually
prevents is pasting the .pub file, which SshKeySecret.TryValidate rejects by name.
multi-line box makes "did the whole key arrive?" unanswerable — and the mistake this
actually prevents is pasting the .pub file, which SshKeySecret.TryValidate rejects by name.
-->
<TextBox Text="{Binding KeyEditorPrivateKey}"
PlaceholderText="-----BEGIN OPENSSH PRIVATE KEY-----"
@@ -439,19 +544,19 @@
<!-- The password editor. -->
<StackPanel Spacing="6" IsVisible="{Binding IsEditingCredential}">
<TextBlock Classes="label" Text="PASSWORD" Margin="0,0,0,4" />
<TextBlock Classes="label" Text="PASSWORD" FontSize="10" />
<TextBox Text="{Binding CredentialEditorLabel}" PlaceholderText="name" />
<!--
Optional, and the reason a credential is worth being its own item rather than two more fields on
a host: one account on twenty machines is described once and rotated once. Left blank, each host
supplies its own username and only the password is shared.
Optional, and the reason a credential is worth being its own item rather than two more
fields on a host: one account on twenty machines is described once and rotated once. Left
blank, each host supplies its own username and only the password is shared.
-->
<TextBox Text="{Binding CredentialEditorUsername}"
PlaceholderText="username (blank: use each host's own)" />
<!--
Masked, unlike the private key box, and the difference is not inconsistency. A key's armour has
to be visible to be checked for truncation after a paste; a password is short, usually typed,
and shoulder-surfing is the likelier problem.
Masked, unlike the private key box, and the difference is not inconsistency. A key's armour
has to be visible to be checked for truncation after a paste; a password is short, usually
typed, and shoulder-surfing is the likelier problem.
-->
<TextBox Text="{Binding CredentialEditorPassword}" PlaceholderText="password" PasswordChar="•" />
<TextBox Text="{Binding CredentialEditorNotes}" PlaceholderText="notes" AcceptsReturn="True"
@@ -465,12 +570,12 @@
</StackPanel>
<!--
The tag editor, and the whole of it is one box. What it does not have is the point: renaming a
tag touches no host, because every host wearing it names its id. That is the entire reason a tag
is an item rather than a string repeated inside twenty payloads.
The tag editor, and the whole of it is one box. What it does not have is the point: renaming
a tag touches no host, because every host wearing it names its id. That is the entire reason
a tag is an item rather than a string repeated inside twenty payloads.
-->
<StackPanel Spacing="6" IsVisible="{Binding IsEditingTag}">
<TextBlock Classes="label" Text="TAG" Margin="0,0,0,4" />
<TextBlock Classes="label" Text="TAG" FontSize="10" />
<TextBox Text="{Binding TagEditorLabel}" PlaceholderText="name">
<TextBox.KeyBindings>
<KeyBinding Gesture="Enter" Command="{Binding SaveTagCommand}" />
@@ -486,20 +591,21 @@
<!-- The bucket editor. -->
<StackPanel Spacing="6" IsVisible="{Binding IsEditingObjectStore}">
<TextBlock Classes="label" Text="BUCKET" Margin="0,0,0,4" />
<TextBlock Classes="label" Text="BUCKET" FontSize="10" />
<TextBox Text="{Binding BucketEditorLabel}" PlaceholderText="name" />
<TextBox Text="{Binding BucketEditorBucket}" PlaceholderText="bucket" />
<TextBox Text="{Binding BucketEditorAccessKeyId}" PlaceholderText="access key id" />
<!--
Masked, like a password and for the same reason: a secret access key is one. The access key id
beside it is an identifier and is shown, which is also why the two are separate boxes.
Masked, like a password and for the same reason: a secret access key is one. The access
key id beside it is an identifier and is shown, which is also why the two are separate
boxes.
-->
<TextBox Text="{Binding BucketEditorSecretAccessKey}" PlaceholderText="secret access key"
PasswordChar="•" />
<TextBox Text="{Binding BucketEditorRegion}" PlaceholderText="region (e.g. eu-west-1)" />
<!--
Blank means Amazon, and then the region resolves the host. Anything else is a full URL, which
is what makes this work against a self-hosted service.
Blank means Amazon, and then the region resolves the host. Anything else is a full URL,
which is what makes this work against a self-hosted service.
-->
<TextBox Text="{Binding BucketEditorEndpoint}"
PlaceholderText="endpoint (blank: Amazon S3)" />
@@ -507,7 +613,8 @@
Content="Address the bucket as a path" />
<!--
Said where the decision is made. Getting this wrong produces a DNS failure whose message
mentions neither buckets nor this setting, which is the worst kind of thing to leave to a guess.
mentions neither buckets nor this setting, which is the worst kind of thing to leave to a
guess.
-->
<TextBlock Classes="hint" FontSize="10.5"
Text="Off for Amazon S3. On for most self-hosted services — MinIO and Ceph have no wildcard DNS, so the bucket cannot be a subdomain." />
@@ -526,5 +633,8 @@
</Border>
</Grid>
</Border>
</Grid>
</UserControl>
+157 -56
View File
@@ -18,39 +18,114 @@
The connections list shows anything still open at the top, marked "still open" rather than with a dash. A
dash would read as a missing recording, and the two are opposite facts — an entry is written once, when a
connection closes, so a live session is deliberately not in the vault yet.
── v5b — Logs.dc.html ──────────────────────────────────────────────────────────────────────────────────
A fidelity pass, not a new shape. What moved:
◆ THE HEADER. "Logs" 33 bold plus the two-segment CONNECTIONS/KEYCHAIN control, restyled onto
Border.navtrack/Button.navseg — the same h31 track the rail's own SSH/SFTP/S3 switcher already uses,
reused rather than redrawn, since it is the identical shape at the identical size. Two segments and not
three: LOGS is the screen's own title, not a tab, so the design's own two — CONNECTIONS and KEYCHAIN —
are the whole of what this screen switches between, exactly matching LogSection's own two values. No
deviation to record here. On the right: a mono status sentence — LogsViewModel.HeaderStatusLine, which
shows a refresh error when there is one and otherwise the fact this screen's own header comment already
states about whichever log is showing — and REFRESH, unchanged.
◆ THE BODY. A bordered radius-12 container on Pane bg wraps each table now, in place of the plain
background either one drew before. Tracked column headers are unchanged — HOST/ADDRESS/LASTED/KIND/
STARTED/FROM for connections, ITEM/TYPE/WHAT/FIELDS/WHEN for keychain activity, both already matching the
design exactly. Rows keep their real two-state host dot (green only for a session genuinely open right
now, per HasVisible on ConnectionLogRowViewModel.IsLive — never a third colour) and their existing
"failed"/"host key refused" amber badges. WHAT, on the activity table, is now a small coloured chip rather
than plain text — green for created, purple for a plain change, red for deleted — reading the same
ActivityOperation this screen already resolved to a word; a chip rather than a fabricated new fact.
◆ THE FOOTER. Logs.dc.html's own lock-glyph sentence — "Both logs are ordinary synced keychain items —
end-to-end encrypted. The server learns only that rows exist and when they were written." — verified
against this file's own header remark above and ADR 0001 before shipping it: both are true, so the
sentence is drawn as literal text rather than reworded.
-->
<Grid RowDefinitions="Auto,*">
<UserControl.Styles>
<Style Selector="Border.logtable">
<Setter Property="BorderBrush" Value="{StaticResource Border}" />
<Setter Property="BorderThickness" Value="1" />
<Setter Property="CornerRadius" Value="12" />
<Setter Property="Background" Value="{StaticResource Pane}" />
<Setter Property="ClipToBounds" Value="True" />
</Style>
<Style Selector="ListBox.logrows > ListBoxItem /template/ ContentPresenter#PART_ContentPresenter">
<Setter Property="CornerRadius" Value="8" />
</Style>
<Style Selector="ListBox.logrows > ListBoxItem:pointerover /template/ ContentPresenter#PART_ContentPresenter">
<Setter Property="Background" Value="{StaticResource Track}" />
</Style>
<!--
WHAT, on the activity table: created/changed/deleted, coloured the way the item badges are. "changed"
is the base rule's own colour — it is the default ActivityLogRowViewModel.Operation falls through to —
and .created/.deleted are declared after it so they win; Avalonia has no specificity and settles two
matching rules by declaration order, the same trap this application's other screens record.
-->
<Style Selector="Border.opchip">
<Setter Property="Background" Value="{StaticResource AccentWash}" />
</Style>
<Style Selector="Border.opchip > TextBlock">
<Setter Property="Foreground" Value="{StaticResource AccentText}" />
</Style>
<Style Selector="Border.opchip.created">
<Setter Property="Background" Value="{StaticResource LiveWash}" />
</Style>
<Style Selector="Border.opchip.created > TextBlock">
<Setter Property="Foreground" Value="{StaticResource Live}" />
</Style>
<Style Selector="Border.opchip.deleted">
<Setter Property="Background" Value="{StaticResource DangerWash}" />
</Style>
<Style Selector="Border.opchip.deleted > TextBlock">
<Setter Property="Foreground" Value="{StaticResource Danger}" />
</Style>
</UserControl.Styles>
<Border Grid.Row="0" Padding="14,0" Height="44"
BorderBrush="{StaticResource Border}" BorderThickness="0,0,0,1">
<Grid ColumnDefinitions="Auto,Auto,Auto,*,Auto" VerticalAlignment="Center">
<TextBlock Grid.Column="0" Classes="mono" Text="LOGS" FontSize="12" FontWeight="SemiBold"
LetterSpacing="1" Foreground="{StaticResource Text}" VerticalAlignment="Center"
Margin="0,0,14,0" />
<Grid RowDefinitions="Auto,*" Margin="26">
<Button Grid.Column="1" Classes="flat cat" Content="CONNECTIONS"
Classes.active="{Binding ShowsConnections}"
Command="{Binding ShowSectionCommand}"
<!-- ============ THE HEADER ============ -->
<Grid Grid.Row="0" Margin="0,0,0,20" ColumnDefinitions="Auto,Auto,*,Auto,Auto">
<TextBlock Grid.Column="0" Text="Logs" FontSize="33" FontWeight="Bold" LetterSpacing="-0.5"
Foreground="{StaticResource Text}" VerticalAlignment="Center" />
<Border Grid.Column="1" Classes="navtrack" Margin="14,0,0,0" Width="200" VerticalAlignment="Center">
<Grid ColumnDefinitions="*,*">
<Button Grid.Column="0" Classes="navseg" Classes.active="{Binding ShowsConnections}"
Content="CONNECTIONS" Command="{Binding ShowSectionCommand}"
CommandParameter="{x:Static vm:LogSection.Connections}" />
<Button Grid.Column="2" Classes="flat cat" Content="KEYCHAIN"
Classes.active="{Binding ShowsActivity}"
Command="{Binding ShowSectionCommand}"
<Button Grid.Column="1" Classes="navseg" Classes.active="{Binding ShowsActivity}"
Content="KEYCHAIN" Command="{Binding ShowSectionCommand}"
CommandParameter="{x:Static vm:LogSection.Activity}" />
<TextBlock Grid.Column="3" Classes="mono" Text="{Binding Status}" FontSize="10.5"
Foreground="{StaticResource TextFaint}" Margin="14,0,0,0"
TextTrimming="CharacterEllipsis" VerticalAlignment="Center" />
<Button Grid.Column="4" Classes="ghost" Content="REFRESH" Command="{Binding RefreshCommand}"
IsEnabled="{Binding !IsBusy}" />
</Grid>
</Border>
<!-- ============ Connections ============ -->
<Grid Grid.Row="1" RowDefinitions="Auto,*" IsVisible="{Binding ShowsConnections}">
<TextBlock Grid.Column="3" Classes="mono" Text="{Binding HeaderStatusLine}" FontSize="10.5"
Foreground="{StaticResource TextFaint}" Margin="0,0,14,0"
TextTrimming="CharacterEllipsis" VerticalAlignment="Center" />
<Grid Grid.Row="0" ColumnDefinitions="1.2*,1.6*,88,72,90,*" Margin="14,6,14,6"
<Button Grid.Column="4" Classes="ghost" Height="40"
Command="{Binding RefreshCommand}" IsEnabled="{Binding !IsBusy}">
<StackPanel Orientation="Horizontal" Spacing="8">
<TextBlock FontFamily="{StaticResource IconFont}" FontSize="15" Text="&#xE5D5;"
Foreground="{StaticResource TextFaint}" />
<TextBlock Text="Refresh" FontSize="13.5" FontWeight="SemiBold"
Foreground="{StaticResource TextDim}" />
</StackPanel>
</Button>
</Grid>
<!-- ============ Connections ============ -->
<Border Grid.Row="1" Classes="logtable" IsVisible="{Binding ShowsConnections}">
<Grid RowDefinitions="Auto,*,Auto">
<Grid Grid.Row="0" ColumnDefinitions="1.2*,1.6*,88,72,90,*" Margin="20,14,20,10"
IsVisible="{Binding HasConnections}">
<TextBlock Grid.Column="0" Classes="label" Text="HOST" FontSize="9.5" LetterSpacing="1" />
<TextBlock Grid.Column="1" Classes="label" Text="ADDRESS" FontSize="9.5" LetterSpacing="1" />
@@ -60,37 +135,35 @@
<TextBlock Grid.Column="5" Classes="label" Text="FROM" FontSize="9.5" LetterSpacing="1" />
</Grid>
<ListBox Grid.Row="1" x:Name="ConnectionList" Focusable="True"
ItemsSource="{Binding Connections}">
<ListBox Grid.Row="1" x:Name="ConnectionList" Classes="logrows" Focusable="True"
ItemsSource="{Binding Connections}" Margin="12,0,12,10">
<ListBox.ItemTemplate>
<DataTemplate x:DataType="vm:ConnectionLogRowViewModel">
<Grid ColumnDefinitions="1.2*,1.6*,88,72,90,*" Margin="0,6,14,6">
<StackPanel Grid.Column="0" Orientation="Horizontal" Spacing="6" Margin="14,0,8,0">
<Grid ColumnDefinitions="1.2*,1.6*,88,72,90,*" Height="40" Margin="8,0,10,0">
<StackPanel Grid.Column="0" Orientation="Horizontal" Spacing="9" Margin="12,0,8,0"
VerticalAlignment="Center">
<Ellipse Classes="dot" Classes.live="{Binding IsLive}" VerticalAlignment="Center" />
<TextBlock Classes="mono" Text="{Binding HostLabel}" FontSize="12" FontWeight="Medium"
Foreground="{StaticResource Text}" TextTrimming="CharacterEllipsis"
VerticalAlignment="Center" />
<TextBlock Classes="mono" Text="{Binding HostLabel}" FontSize="12.5" FontWeight="Medium"
Foreground="{StaticResource Text}" TextTrimming="CharacterEllipsis" />
</StackPanel>
<TextBlock Grid.Column="1" Classes="mono" Text="{Binding Address}" FontSize="10.5"
Foreground="{StaticResource TextDim}" Margin="0,0,8,0"
<TextBlock Grid.Column="1" Classes="mono" Text="{Binding Address}" FontSize="11"
Foreground="{StaticResource TextGhost}" Margin="0,0,8,0"
TextTrimming="CharacterEllipsis" VerticalAlignment="Center" />
<StackPanel Grid.Column="2" Orientation="Horizontal" Spacing="6" VerticalAlignment="Center">
<TextBlock Classes="mono" Text="{Binding Duration}" FontSize="10.5"
Foreground="{StaticResource TextDim}" />
</StackPanel>
<TextBlock Grid.Column="3" Classes="mono" Text="{Binding Kind}" FontSize="10"
<TextBlock Grid.Column="2" Classes="mono" Text="{Binding Duration}" FontSize="11"
Foreground="{StaticResource TextGhost}" VerticalAlignment="Center" />
<TextBlock Grid.Column="3" Classes="mono" Text="{Binding Kind}" FontSize="10.5"
Foreground="{StaticResource TextFaint}" VerticalAlignment="Center" />
<TextBlock Grid.Column="4" Classes="mono" Text="{Binding Started}" FontSize="10"
<TextBlock Grid.Column="4" Classes="mono" Text="{Binding Started}" FontSize="10.5"
Foreground="{StaticResource TextFaint}" VerticalAlignment="Center" />
<StackPanel Grid.Column="5" Orientation="Horizontal" Spacing="6" VerticalAlignment="Center">
<TextBlock Classes="mono" Text="{Binding DeviceName}" FontSize="10"
<StackPanel Grid.Column="5" Orientation="Horizontal" Spacing="8" VerticalAlignment="Center">
<TextBlock Classes="mono" Text="{Binding DeviceName}" FontSize="10.5"
Foreground="{StaticResource TextFaint}"
TextTrimming="CharacterEllipsis" />
<!--
Only when there is something to say. A connection that opened and closed says nothing
here; one that was refused says so, and that is the row worth finding in a long list.
-->
<Border Classes="chip warn" Padding="4,0" IsVisible="{Binding HasOutcome}">
<Border Classes="chip warn" Padding="7,0" Height="18" IsVisible="{Binding HasOutcome}">
<TextBlock Text="{Binding Outcome}" FontSize="9.5" />
</Border>
</StackPanel>
@@ -103,12 +176,27 @@
Margin="24" HorizontalAlignment="Center" VerticalAlignment="Center"
TextAlignment="Center" MaxWidth="420"
IsVisible="{Binding !HasConnections}" />
<!-- ◆ THE FOOTER. Verified against this file's own header remark and ADR 0001; see the file-level note. -->
<Border Grid.Row="2" BorderBrush="{StaticResource Border}" BorderThickness="0,1,0,0"
Padding="20,12">
<StackPanel Orientation="Horizontal" Spacing="10">
<TextBlock FontFamily="{StaticResource IconFont}" FontSize="13" Text="&#xE897;"
Foreground="{StaticResource TextFaint}" />
<TextBlock Classes="hint" FontSize="11" TextWrapping="Wrap"
Foreground="{StaticResource TextFaint}"
Text="Both logs are ordinary synced keychain items — end-to-end encrypted. The server learns only that rows exist and when they were written." />
</StackPanel>
</Border>
</Grid>
</Border>
<!-- ============ Keychain changes ============ -->
<Grid Grid.Row="1" RowDefinitions="Auto,*" IsVisible="{Binding ShowsActivity}">
<Border Grid.Row="1" Classes="logtable" IsVisible="{Binding ShowsActivity}">
<Grid RowDefinitions="Auto,*,Auto">
<Grid Grid.Row="0" ColumnDefinitions="1.2*,90,96,*,90" Margin="14,6,14,6"
<Grid Grid.Row="0" ColumnDefinitions="1.4*,0.8*,0.8*,1.6*,0.8*" Margin="20,14,20,10"
IsVisible="{Binding HasActivity}">
<TextBlock Grid.Column="0" Classes="label" Text="ITEM" FontSize="9.5" LetterSpacing="1" />
<TextBlock Grid.Column="1" Classes="label" Text="TYPE" FontSize="9.5" LetterSpacing="1" />
@@ -117,26 +205,26 @@
<TextBlock Grid.Column="4" Classes="label" Text="WHEN" FontSize="9.5" LetterSpacing="1" />
</Grid>
<ListBox Grid.Row="1" x:Name="ActivityList" Focusable="True" ItemsSource="{Binding Activity}">
<ListBox Grid.Row="1" x:Name="ActivityList" Classes="logrows" Focusable="True"
ItemsSource="{Binding Activity}" Margin="12,0,12,10">
<ListBox.ItemTemplate>
<DataTemplate x:DataType="vm:ActivityLogRowViewModel">
<Grid ColumnDefinitions="1.2*,90,96,*,90" Margin="14,6,14,6">
<TextBlock Grid.Column="0" Classes="mono" Text="{Binding ItemLabel}" FontSize="12"
FontWeight="Medium" Foreground="{StaticResource Text}" Margin="0,0,8,0"
<Grid ColumnDefinitions="1.4*,0.8*,0.8*,1.6*,0.8*" Height="40" Margin="8,0,10,0">
<TextBlock Grid.Column="0" Classes="mono" Text="{Binding ItemLabel}" FontSize="12.5"
FontWeight="Medium" Foreground="{StaticResource Text}" Margin="4,0,8,0"
TextTrimming="CharacterEllipsis" VerticalAlignment="Center" />
<TextBlock Grid.Column="1" Classes="mono" Text="{Binding ItemKind}" FontSize="10"
Foreground="{StaticResource TextFaint}" VerticalAlignment="Center" />
<TextBlock Grid.Column="2" Classes="mono" Text="{Binding Operation}" FontSize="10.5"
Foreground="{StaticResource TextDim}" VerticalAlignment="Center" />
<!--
The names of the fields that changed, and never what they changed to. A log that recorded
an old password would be a plaintext credential store with a vault drawn around it.
-->
<TextBlock Grid.Column="1" Classes="mono" Text="{Binding ItemKind}" FontSize="10.5"
Foreground="{StaticResource TextGhost}" VerticalAlignment="Center" />
<Border Grid.Column="2" Classes="opchip" Classes.created="{Binding IsCreated}"
Classes.deleted="{Binding IsDeleted}" Height="19" CornerRadius="5" Padding="8,0"
HorizontalAlignment="Left" VerticalAlignment="Center">
<TextBlock Text="{Binding Operation}" FontSize="10" FontWeight="SemiBold" />
</Border>
<TextBlock Grid.Column="3" Classes="mono" Text="{Binding ChangedFields}" FontSize="10.5"
Foreground="{StaticResource TextFaint}" Margin="0,0,8,0"
TextTrimming="CharacterEllipsis" VerticalAlignment="Center"
IsVisible="{Binding HasChangedFields}" />
<TextBlock Grid.Column="4" Classes="mono" Text="{Binding At}" FontSize="10"
<TextBlock Grid.Column="4" Classes="mono" Text="{Binding At}" FontSize="10.5"
Foreground="{StaticResource TextFaint}" VerticalAlignment="Center" />
</Grid>
</DataTemplate>
@@ -147,7 +235,20 @@
Margin="24" HorizontalAlignment="Center" VerticalAlignment="Center"
TextAlignment="Center" MaxWidth="420"
IsVisible="{Binding !HasActivity}" />
<Border Grid.Row="2" BorderBrush="{StaticResource Border}" BorderThickness="0,1,0,0"
Padding="20,12">
<StackPanel Orientation="Horizontal" Spacing="10">
<TextBlock FontFamily="{StaticResource IconFont}" FontSize="13" Text="&#xE897;"
Foreground="{StaticResource TextFaint}" />
<TextBlock Classes="hint" FontSize="11" TextWrapping="Wrap"
Foreground="{StaticResource TextFaint}"
Text="Both logs are ordinary synced keychain items — end-to-end encrypted. The server learns only that rows exist and when they were written." />
</StackPanel>
</Border>
</Grid>
</Border>
</Grid>
+219 -107
View File
@@ -10,56 +10,134 @@
The list and the writing belong to the vault, as every other item kind's do; this screen is the filter,
the editor and the insert over the top. See SnippetsViewModel.
The two buttons at the bottom right are the whole safety design, and their wording is load-bearing.
A terminal is one input stream with no notion of being at a prompt — the remote may be inside vi, or at
a sudo password prompt with echo off — so this application cannot say "run this command", only "type
this into whatever is there". RUN appears solely for a snippet whose own flag says it runs, which makes
that a decision taken once while writing it rather than a button beside every one of them.
The two buttons at the bottom right of the detail pane are the whole safety design, and their wording is
load-bearing. A terminal is one input stream with no notion of being at a prompt — the remote may be
inside vi, or at a sudo password prompt with echo off — so this application cannot say "run this
command", only "type this into whatever is there". RUN appears solely for a snippet whose own flag says
it runs, which makes that a decision taken once while writing it rather than a button beside every one
of them.
── v5b — Snippets.dc.html ──────────────────────────────────────────────────────────────────────────────
A fidelity pass, not a new shape. What moved:
◆ THE SCREEN'S OWN TITLE. The design renames this screen "Snips" — adopted here on screen, matching the
nav rail, which already says Snips (see NavRail.axaml's own remark on the mock's wording for this
destination).
◆ THE HEADER. "Snips" 33 bold, a count chip, the existing filter box restyled to 280px, and a
"+ New snip" accent button in place of the old ghost "+ NEW SNIPPET" — same NewCommand.
◆ THE LIST. Card rows rather than a flat list — radius 10, Track fill and an accent ring on the selected
one, Track on hover — with a { } glyph, the mono name, the "runs immediately" chip (real: it is
RunsOnInsert, already tracked per snippet), the vault chip, and the mono command preview beneath. No
modified date: SnippetSecret carries none, and neither does any other item kind — VaultItem is (id,
secret, version, three sync flags) and nothing else; see docs/design-import-gaps.md. The bottom action
row keeps EDIT / MOVE TO VAULT… / DELETE, restyled and DELETE now danger-coloured to match.
◆ THE DETAIL/EDITOR SIDEBAR, widened to 340px per the design. Two modes exist as they always have — the
editor while IsEditing, the selected snippet's own facts and its insert controls otherwise — because the
two are genuinely different forms doing different things, not a cosmetic split; merging them would let
somebody fixing a typo re-file a snippet by leaving a vault picker where they found it. VAULT draws a
lock glyph beside the vault name, per the design. "Runs on insert"/"runs immediately" keeps this screen's
own longer caption rather than the design's shorter one — it states the operational consequence
("typed at the prompt and waits for you") where the design's states only the rationale for having the
setting once; both are true, the existing one is more actionable. The insert button keeps its own dynamic
label — "TYPE INTO {tab}" or "NO TERMINAL OPEN" — over the design's static "Type into terminal", because
naming the destination tab is a fact the design's caption does not carry and this screen already had.
Its own caption similarly stays: it names vi and a sudo prompt with echo off as the two ways "at a
prompt" can be wrong, which is more than the design's own sentence says.
◆ THE DELETE CONFIRMATION. Snippets.dc.html's own modal — vault-wide reach, a tombstone, no undo — is
exactly what VaultViewModel.HowFarADeletionGoes already says for every other kind of item, so
SnippetsViewModel.RequestDelete/ConfirmDelete/CancelDelete say it in the same words. Additive next to the
existing uncounted DeleteCommand rather than a change to it — that command is what the phone's own DELETE
row still calls, with no confirmation card on that screen to answer one; see SnippetsViewModel's own
remarks on both commands.
-->
<Grid ColumnDefinitions="*,300">
<Grid Grid.Column="0" RowDefinitions="Auto,*,Auto">
<Border Grid.Row="0" Padding="14,0" Height="44"
BorderBrush="{StaticResource Border}" BorderThickness="0,0,0,1">
<Grid ColumnDefinitions="Auto,*,Auto" VerticalAlignment="Center">
<TextBlock Grid.Column="0" Classes="mono" Text="SNIPPETS" FontSize="12"
FontWeight="SemiBold" LetterSpacing="1" Foreground="{StaticResource Text}"
VerticalAlignment="Center" />
<TextBlock Grid.Column="1" Classes="mono" Text="{Binding Status}" FontSize="10.5"
Foreground="{StaticResource TextFaint}" Margin="10,0,0,0"
TextTrimming="CharacterEllipsis" VerticalAlignment="Center" />
<UserControl.Styles>
<!--
The command is searched as well as the name: half of what anybody remembers about a saved
command is a word that was inside it.
A card, not a full-bleed row: Track fill on hover, Track fill plus an accent ring on the selected one,
matching the design's own "selected = Track bg + accent ring, hover Track" — distinct from
ListBox.filerows, which draws no ring, because a snippet card is chosen for one editor at a time rather
than opened directly.
-->
<TextBox Grid.Column="2" x:Name="SnippetFilter" Text="{Binding Filter}" Width="240"
PlaceholderText="filter by name or command" VerticalAlignment="Center" />
</Grid>
<Style Selector="ListBox.snipcards > ListBoxItem /template/ ContentPresenter#PART_ContentPresenter">
<Setter Property="CornerRadius" Value="10" />
<Setter Property="BorderThickness" Value="1" />
<Setter Property="BorderBrush" Value="Transparent" />
</Style>
<Style Selector="ListBox.snipcards > ListBoxItem:pointerover /template/ ContentPresenter#PART_ContentPresenter">
<Setter Property="Background" Value="{StaticResource Track}" />
</Style>
<Style Selector="ListBox.snipcards > ListBoxItem:selected /template/ ContentPresenter#PART_ContentPresenter">
<Setter Property="Background" Value="{StaticResource Track}" />
<Setter Property="BorderBrush" Value="{StaticResource Accent}" />
</Style>
<Style Selector="ListBox.snipcards > ListBoxItem:selected:pointerover /template/ ContentPresenter#PART_ContentPresenter">
<Setter Property="Background" Value="{StaticResource Track}" />
<Setter Property="BorderBrush" Value="{StaticResource Accent}" />
</Style>
</UserControl.Styles>
<Grid RowDefinitions="Auto,*" Margin="26">
<!-- ============ THE HEADER ============ -->
<Grid Grid.Row="0" Margin="0,0,0,20" ColumnDefinitions="Auto,Auto,*,Auto,Auto">
<TextBlock Grid.Column="0" Text="Snips" FontSize="33" FontWeight="Bold" LetterSpacing="-0.5"
Foreground="{StaticResource Text}" VerticalAlignment="Center" />
<Border Grid.Column="1" Margin="12,0,0,0" MinWidth="34" Height="30" CornerRadius="9" Padding="8,0"
Background="{StaticResource Chip}" VerticalAlignment="Center">
<TextBlock Classes="mono" Text="{Binding Visible.Count}" FontSize="12.5"
Foreground="{StaticResource TextDim}" HorizontalAlignment="Center"
VerticalAlignment="Center" />
</Border>
<ListBox Grid.Row="1" x:Name="SnippetList" Focusable="True"
<!--
The command is searched as well as the name: half of what anybody remembers about a saved command is
a word that was inside it.
-->
<TextBox Grid.Column="3" x:Name="SnippetFilter" Text="{Binding Filter}" Width="280" Height="40"
CornerRadius="10" Margin="0,0,10,0"
PlaceholderText="filter by name or command" VerticalAlignment="Center" />
<Button Grid.Column="4" Classes="accent" Height="40" FontSize="13.5" Content="+ New snip"
Command="{Binding NewCommand}" />
</Grid>
<!-- ============ THE BORDERED BODY ============ -->
<Border Grid.Row="1" BorderBrush="{StaticResource Border}" BorderThickness="1" CornerRadius="12"
ClipToBounds="True">
<Grid ColumnDefinitions="*,340">
<Grid Grid.Column="0" RowDefinitions="*,Auto" Background="{StaticResource Pane}">
<ListBox Grid.Row="0" x:Name="SnippetList" Classes="snipcards" Focusable="True"
ItemsSource="{Binding Visible}"
SelectedItem="{Binding Selected}">
SelectedItem="{Binding Selected}"
Margin="10,10,10,4">
<ListBox.ItemTemplate>
<DataTemplate x:DataType="vm:SnippetRowViewModel">
<Grid ColumnDefinitions="2,*" Margin="0,7,14,7">
<Border Grid.Column="0" Classes="rowmark" />
<StackPanel Grid.Column="1" Margin="12,0,0,0" Spacing="2">
<StackPanel Orientation="Horizontal" Spacing="6">
<TextBlock Classes="mono" Text="{Binding Label}" FontSize="12" FontWeight="Medium"
Foreground="{StaticResource Text}" TextTrimming="CharacterEllipsis" />
<StackPanel Margin="6,7" Spacing="6">
<StackPanel Orientation="Horizontal" Spacing="8">
<TextBlock Classes="mono" Text="{}{ }" FontSize="11" FontWeight="Bold"
Foreground="{StaticResource AccentText}" VerticalAlignment="Center" />
<TextBlock Classes="mono" Text="{Binding Label}" FontSize="13" FontWeight="Medium"
Foreground="{StaticResource Text}" VerticalAlignment="Center"
TextTrimming="CharacterEllipsis" />
<!--
The flag, where the decision is made. A snippet that presses Enter for you is not the
same kind of thing as one that does not, and the list is where somebody chooses between
them.
-->
<Border Classes="chip warn" Padding="4,0" IsVisible="{Binding RunsOnInsert}">
<Border Classes="chip warn" Padding="7,0" Height="18" VerticalAlignment="Center"
IsVisible="{Binding RunsOnInsert}">
<TextBlock Text="runs immediately" FontSize="9.5" />
</Border>
<Border Classes="chip warn" Padding="4,0"
<Border Classes="chip warn" Padding="7,0" Height="18" VerticalAlignment="Center"
IsVisible="{Binding Badge, Converter={x:Static StringConverters.IsNotNullOrEmpty}}">
<TextBlock Text="{Binding Badge}" FontSize="9.5" />
</Border>
@@ -68,7 +146,8 @@
snippet is a command the rest of a team can read and insert; the row is where somebody
decides whether the thing they are about to edit is theirs alone.
-->
<Border Classes="chip" Padding="4,0" IsVisible="{Binding HasVaultBadge}">
<Border Classes="chip" Padding="7,0" Height="18" VerticalAlignment="Center"
IsVisible="{Binding HasVaultBadge}">
<TextBlock Text="{Binding VaultBadge}" FontSize="9.5" />
</Border>
</StackPanel>
@@ -76,37 +155,35 @@
Newlines shown as ⏎ rather than dropped. A three-line snippet flattened into one run of
text reads as a single command, which is the thing being decided about on this row.
-->
<TextBlock Classes="mono" Text="{Binding Preview}" FontSize="10.5"
<TextBlock Classes="mono" Text="{Binding Preview}" FontSize="11.5"
Foreground="{StaticResource TextFaint}" TextTrimming="CharacterEllipsis" />
</StackPanel>
</Grid>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
<TextBlock Grid.Row="1" Classes="hint" Text="{Binding EmptyMessage}" FontSize="12"
<TextBlock Grid.Row="0" Classes="hint" Text="{Binding EmptyMessage}" FontSize="12"
Margin="24" HorizontalAlignment="Center" VerticalAlignment="Center"
TextAlignment="Center" MaxWidth="360"
IsVisible="{Binding !HasVisible}" />
<Border Grid.Row="2" Padding="14,8" BorderBrush="{StaticResource BorderSubtle}"
<Border Grid.Row="1" Padding="16,10" BorderBrush="{StaticResource BorderSubtle}"
BorderThickness="0,1,0,0">
<StackPanel Orientation="Horizontal" Spacing="6">
<Button Classes="ghost" Content="+ NEW SNIPPET" Command="{Binding NewCommand}" />
<Button Classes="ghost" Content="EDIT" Command="{Binding EditCommand}"
IsEnabled="{Binding HasSelection}" />
<Button Classes="ghost" Content="DELETE" Command="{Binding DeleteCommand}"
<StackPanel Orientation="Horizontal" Spacing="8">
<Button Classes="ghost" Content="Edit" Command="{Binding EditCommand}"
IsEnabled="{Binding HasSelection}" />
<!--
Sharing a snippet, which is what moving one into a team's vault is. Beside EDIT rather than
inside it: the two vaults are encrypted under different keys, so this is a re-seal into one and
a tombstone in the other — nothing a SAVE could do — and a picker inside the form would let
somebody correcting a typo hand a command to a team by leaving it where they found it. The
picker itself opens beside the snippet, in the pane on the right.
inside it: the two vaults are encrypted under different keys, so this is a re-seal into one
and a tombstone in the other — nothing a SAVE could do — and a picker inside the form would
let somebody correcting a typo hand a command to a team by leaving it where they found it.
The picker itself opens beside the snippet, in the pane on the right.
-->
<Button Classes="ghost" Content="MOVE TO VAULT…" Command="{Binding MoveCommand}"
<Button Classes="ghost" Content="Move to vault…" Command="{Binding MoveCommand}"
IsEnabled="{Binding CanMove}"
ToolTip.Tip="Re-encrypts this snippet with another vault's key. Everybody who holds that key can then read and insert it." />
<Button Classes="danger" Content="Delete" Command="{Binding RequestDeleteCommand}"
IsEnabled="{Binding HasSelection}" />
</StackPanel>
</Border>
@@ -115,20 +192,22 @@
<Border Grid.Column="1" Background="{StaticResource Sidebar}"
BorderBrush="{StaticResource Border}" BorderThickness="1,0,0,0">
<ScrollViewer>
<StackPanel Margin="14,16" Spacing="8">
<StackPanel Margin="18,20" Spacing="10">
<!-- ============ The editor ============ -->
<StackPanel Spacing="6" IsVisible="{Binding IsEditing}">
<TextBox Text="{Binding EditorLabel}" PlaceholderText="name" />
<StackPanel Spacing="8" IsVisible="{Binding IsEditing}">
<TextBlock Classes="label" Text="NAME" FontSize="10" />
<TextBox Text="{Binding EditorLabel}" PlaceholderText="name" FontFamily="{StaticResource MonoFont}" />
<!--
Which vault a *new* snippet is filed into, asked on the form it is being typed into rather
than through a standing preference elsewhere. Not drawn for an existing snippet — its vault is
not a field of this form, and changing it is MOVE — and not drawn at all where there is only
one vault to choose between, because a control offering one option is a question nobody asked.
than through a standing preference elsewhere. Not drawn for an existing snippet — its vault
is not a field of this form, and changing it is MOVE — and not drawn at all where there is
only one vault to choose between, because a control offering one option is a question
nobody asked.
-->
<StackPanel Spacing="4" IsVisible="{Binding ShowsEditorVaultChoice}">
<TextBlock Classes="label" Text="VAULT" />
<TextBlock Classes="label" Text="VAULT" FontSize="10" />
<ComboBox HorizontalAlignment="Stretch" ItemsSource="{Binding EditorVaultChoices}"
SelectedItem="{Binding EditorSelectedVault}">
<ComboBox.ItemTemplate>
@@ -141,94 +220,104 @@
Text="A shared vault means everybody holding its key can read this command and insert it into their own terminals." />
</StackPanel>
<TextBlock Classes="label" Text="COMMAND" FontSize="10" />
<!--
Stored exactly as typed — no trimming, no newline normalisation. A here-document's terminator
has to arrive on a line of its own, and tidying the trailing newline off it leaves the shell
waiting for one that never comes.
Stored exactly as typed — no trimming, no newline normalisation. A here-document's
terminator has to arrive on a line of its own, and tidying the trailing newline off it
leaves the shell waiting for one that never comes.
-->
<TextBox Text="{Binding EditorCommand}" PlaceholderText="the command" AcceptsReturn="True"
Height="140" TextWrapping="NoWrap" FontFamily="{StaticResource MonoFont}"
FontSize="12" />
<TextBox Text="{Binding EditorNotes}" PlaceholderText="notes" AcceptsReturn="True"
Height="48" TextWrapping="Wrap" />
<CheckBox IsChecked="{Binding EditorRunsOnInsert}"
Content="Press Enter after inserting this" />
<TextBlock Classes="hint" FontSize="11" TextWrapping="Wrap"
<Grid ColumnDefinitions="*,Auto" Margin="0,4,0,0">
<StackPanel Grid.Column="0" Spacing="3">
<TextBlock Text="Runs on insert" FontSize="12.5" FontWeight="SemiBold"
Foreground="{StaticResource TextDim}" />
<TextBlock Classes="hint" FontSize="10.5" TextWrapping="Wrap"
Text="Off means the command is typed at the prompt and waits for you. That single Enter is the only thing standing between a saved command and a running one, so leave it off unless you meant it." />
<StackPanel Orientation="Horizontal" Spacing="6">
<Button Classes="accent" Content="SAVE" Command="{Binding SaveCommand}" />
<Button Classes="ghost" Content="CANCEL" Command="{Binding CancelCommand}" />
</StackPanel>
<CheckBox Grid.Column="1" IsChecked="{Binding EditorRunsOnInsert}"
VerticalAlignment="Top" Margin="10,0,0,0" />
</Grid>
<StackPanel Orientation="Horizontal" Spacing="6" Margin="0,6,0,0">
<Button Classes="accent" Content="Save" Command="{Binding SaveCommand}" />
<Button Classes="ghost" Content="Cancel" Command="{Binding CancelCommand}" />
</StackPanel>
</StackPanel>
<!-- ============ The selected snippet ============ -->
<StackPanel Spacing="6" IsVisible="{Binding !IsEditing}">
<StackPanel Spacing="10" IsVisible="{Binding !IsEditing}">
<TextBlock Classes="hint" FontSize="12"
Text="Choose a snippet to see it in full and put it into a terminal."
IsVisible="{Binding !HasSelection}" />
<StackPanel Spacing="6" IsVisible="{Binding HasSelection}">
<TextBlock Classes="mono" Text="{Binding Selected.Label}" FontSize="13"
FontWeight="SemiBold" Foreground="{StaticResource Text}" TextWrapping="Wrap" />
<StackPanel Spacing="10" IsVisible="{Binding HasSelection}">
<TextBlock Classes="mono" Text="{Binding Selected.Label}" FontSize="15"
FontWeight="Bold" Foreground="{StaticResource Text}" TextWrapping="Wrap" />
<!--
Named here as well as on the row, because this pane is where somebody decides to put a
command into a production terminal, and who else holds a key to it is part of that decision.
The row's badge is off the screen by the time this is being read.
-->
<Border Classes="chip" Padding="4,0" HorizontalAlignment="Left"
IsVisible="{Binding HasSelectionVaultBadge}">
<TextBlock Text="{Binding SelectionVaultBadge}" FontSize="9.5" />
</Border>
<TextBlock Classes="label" Text="COMMAND" Margin="0,10,0,4" />
<Border Background="{StaticResource Raised}" BorderBrush="{StaticResource Border}"
BorderThickness="1" CornerRadius="4" Padding="8">
<TextBlock Classes="label" Text="COMMAND" FontSize="10" />
<Border CornerRadius="10" Background="{StaticResource Pane}"
BorderBrush="{StaticResource BorderMid}" BorderThickness="1" Padding="12">
<SelectableTextBlock Classes="mono" Text="{Binding Selected.Snippet.Command}"
FontSize="10.5" Foreground="{StaticResource TextDim}"
TextWrapping="Wrap" />
FontSize="11.5" LineHeight="19"
Foreground="{StaticResource TextFaint}" TextWrapping="Wrap" />
</Border>
<TextBlock Classes="mono" Text="{Binding Selected.Snippet.Notes}" FontSize="11"
Foreground="{StaticResource TextFaint}" TextWrapping="Wrap" Margin="0,6,0,0"
Foreground="{StaticResource TextFaint}" TextWrapping="Wrap"
IsVisible="{Binding Selected.Snippet.Notes, Converter={x:Static StringConverters.IsNotNullOrEmpty}}" />
<!--
The button names the tab it will type into. This screen is not the terminal — the strip
above it is — so "INSERT" alone would leave somebody working out which of six open tabs is
about to receive a command, at the moment that is worst to be wrong about.
VAULT, with the design's own lock glyph. Named here as well as on the row, because this
pane is where somebody decides to put a command into a production terminal, and who else
holds a key to it is part of that decision.
-->
<StackPanel Spacing="4" IsVisible="{Binding HasSelectionVaultBadge}">
<TextBlock Classes="label" Text="VAULT" FontSize="10" />
<Border Height="38" CornerRadius="10" Background="{StaticResource Pane}"
BorderBrush="{StaticResource BorderMid}" BorderThickness="1" Padding="12,0">
<Grid ColumnDefinitions="*,Auto">
<TextBlock Grid.Column="0" Classes="mono" Text="{Binding SelectionVaultBadge}"
FontSize="11.5" Foreground="{StaticResource TextDim}"
VerticalAlignment="Center" />
<TextBlock Grid.Column="1" FontFamily="{StaticResource IconFont}" Text="&#xE897;"
FontSize="14" Foreground="{StaticResource TextFaint}"
VerticalAlignment="Center" />
</Grid>
</Border>
</StackPanel>
<!--
The button names the tab it will type into, over the design's static "Type into
terminal" — see the file-level remark above for why the dynamic label is the truer one.
-->
<StackPanel Spacing="6" IsVisible="{Binding ShowsSelectionActions}">
<Button Classes="accent" Content="{Binding InsertLabel}" Margin="0,14,0,0"
HorizontalAlignment="Left"
<Button Classes="accent" Content="{Binding InsertLabel}" HorizontalAlignment="Stretch"
HorizontalContentAlignment="Center"
Command="{Binding InsertCommand}" IsEnabled="{Binding CanInsert}"
ToolTip.Tip="Types the command at the prompt and stops. Nothing runs until you press Enter there." />
<Button Classes="danger" Content="{Binding RunLabel}" HorizontalAlignment="Left"
<Button Classes="danger" Content="{Binding RunLabel}" HorizontalAlignment="Stretch"
HorizontalContentAlignment="Center"
Command="{Binding RunCommand}"
IsVisible="{Binding SelectionRuns}" IsEnabled="{Binding CanInsert}"
ToolTip.Tip="Types the command and presses Enter. Offered because this snippet is marked as one that runs." />
<TextBlock Classes="hint" FontSize="11" TextWrapping="Wrap" Margin="0,10,0,0"
<TextBlock Classes="hint" FontSize="10.5" TextWrapping="Wrap" TextAlignment="Center"
Text="Whatever is in the terminal receives this. Nothing here can tell whether that is a shell prompt, an editor, or a password prompt with the echo off — so check the tab before you insert." />
</StackPanel>
<!--
◆ SHARING THE SNIPPET, which is what moving it into a team's vault amounts to. A picker and
two buttons, not a question with a yes: what is being asked is which vault, and a move is
undone by moving it back rather than by being careful — so this is not drawn in the danger
colours the deletion question uses.
It takes the insert controls' place while it is up, for the reason the host pane hides
CONNECT: the button that opened this is still on screen otherwise, offering to open it again.
The sentence is the part worth keeping. Unlike a host, a snippet crosses whole — it has no
group and no tags to leave behind — so what there is to say is who can read it afterwards,
and for a command that may carry a hostname or a path that is the whole of the decision.
◆ SHARING THE SNIPPET, which is what moving it into a team's vault amounts to. A picker
and two buttons, not a question with a yes.
-->
<StackPanel Spacing="8" Margin="0,14,0,0" IsVisible="{Binding IsMoving}">
<TextBlock Classes="label" Text="MOVE TO VAULT" />
<StackPanel Spacing="8" IsVisible="{Binding IsMoving}">
<TextBlock Classes="label" Text="MOVE TO VAULT" FontSize="10" />
<ComboBox HorizontalAlignment="Stretch" ItemsSource="{Binding MoveVaultChoices}"
SelectedItem="{Binding SelectedMoveVault}">
<ComboBox.ItemTemplate>
@@ -240,10 +329,30 @@
<TextBlock Classes="hint" FontSize="10.5" TextWrapping="Wrap"
Text="The snippet is re-encrypted with the other vault's key, so everybody who holds that key can read this command and insert it — and nobody else can. Nothing else about it changes." />
<StackPanel Orientation="Horizontal" Spacing="6">
<Button Classes="accent" Content="MOVE" Command="{Binding ConfirmMoveCommand}" />
<Button Classes="ghost" Content="CANCEL" Command="{Binding CancelMoveCommand}" />
<Button Classes="accent" Content="Move" Command="{Binding ConfirmMoveCommand}" />
<Button Classes="ghost" Content="Cancel" Command="{Binding CancelMoveCommand}" />
</StackPanel>
</StackPanel>
<!--
◆ THE DELETE CONFIRMATION. See RequestDelete's own remarks for why this is additive
beside DeleteCommand rather than a change to it.
-->
<Border Background="{StaticResource DangerWash}" BorderBrush="{StaticResource DangerSoft}"
BorderThickness="1" CornerRadius="10" Padding="12"
IsVisible="{Binding IsConfirmingDelete}">
<StackPanel Spacing="8">
<TextBlock Classes="heading" FontSize="14" TextWrapping="Wrap"
Text="{Binding DeleteQuestion}" />
<TextBlock Foreground="{StaticResource WarnText}" FontSize="12" TextWrapping="Wrap"
Text="{Binding DeleteConsequence}" />
<StackPanel Orientation="Horizontal" Spacing="6">
<Button Classes="danger" Content="Delete snippet"
Command="{Binding ConfirmDeleteCommand}" />
<Button Classes="ghost" Content="Cancel" Command="{Binding CancelDeleteCommand}" />
</StackPanel>
</StackPanel>
</Border>
</StackPanel>
</StackPanel>
@@ -252,5 +361,8 @@
</Border>
</Grid>
</Border>
</Grid>
</UserControl>