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 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 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. 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}" <Border Grid.Column="0" Background="{StaticResource Sidebar}"
BorderBrush="{StaticResource Border}" BorderThickness="0,0,1,0"> BorderBrush="{StaticResource Border}" BorderThickness="0,0,1,0">
<ScrollViewer> <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}" <Button Classes="flat cat" Command="{Binding ShowSectionCommand}"
CommandParameter="{x:Static vm:VaultSection.All}" CommandParameter="{x:Static vm:VaultSection.All}"
@@ -44,7 +144,7 @@
<Grid ColumnDefinitions="Auto,*,Auto"> <Grid ColumnDefinitions="Auto,*,Auto">
<Border Grid.Column="0" Classes="rowmark catmark" /> <Border Grid.Column="0" Classes="rowmark catmark" />
<TextBlock Grid.Column="1" Text="ALL" Margin="12,0,0,0" /> <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}" /> Foreground="{StaticResource TextFaint}" />
</Grid> </Grid>
</Button> </Button>
@@ -55,7 +155,7 @@
<Grid ColumnDefinitions="Auto,*,Auto"> <Grid ColumnDefinitions="Auto,*,Auto">
<Border Grid.Column="0" Classes="rowmark catmark" /> <Border Grid.Column="0" Classes="rowmark catmark" />
<TextBlock Grid.Column="1" Text="SSH KEYS" Margin="12,0,0,0" /> <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}" /> Foreground="{StaticResource TextFaint}" />
</Grid> </Grid>
</Button> </Button>
@@ -66,21 +166,21 @@
<Grid ColumnDefinitions="Auto,*,Auto"> <Grid ColumnDefinitions="Auto,*,Auto">
<Border Grid.Column="0" Classes="rowmark catmark" /> <Border Grid.Column="0" Classes="rowmark catmark" />
<TextBlock Grid.Column="1" Text="PASSWORDS" Margin="12,0,0,0" /> <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}" /> Foreground="{StaticResource TextFaint}" />
</Grid> </Grid>
</Button> </Button>
<!-- <!--
Buckets. A category here rather than a screen of its own, unlike the approved host keys: a bucket Buckets. A category here rather than a screen of its own, unlike the approved host keys: a
is something somebody creates, edits and keeps a secret for, which is what the other two bucket is something somebody creates, edits and keeps a secret for, which is what the other
categories are. A pin is a decision recorded at connect time and is not. 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 Tags. The odd category: it is the only one holding nothing secret — a tag is a name. It is
because the reason a tag is an item at all is that renaming it should be one write instead of here because the reason a tag is an item at all is that renaming it should be one write
twenty, and a rename needs somewhere to happen; so does deleting, or the host editor's picker instead of twenty, and a rename needs somewhere to happen; so does deleting, or the host
fills with names nobody uses and never empties. editor's picker fills with names nobody uses and never empties.
--> -->
<Button Classes="flat cat" Command="{Binding ShowSectionCommand}" <Button Classes="flat cat" Command="{Binding ShowSectionCommand}"
CommandParameter="{x:Static vm:VaultSection.Tags}" CommandParameter="{x:Static vm:VaultSection.Tags}"
@@ -88,7 +188,7 @@
<Grid ColumnDefinitions="Auto,*,Auto"> <Grid ColumnDefinitions="Auto,*,Auto">
<Border Grid.Column="0" Classes="rowmark catmark" /> <Border Grid.Column="0" Classes="rowmark catmark" />
<TextBlock Grid.Column="1" Text="TAGS" Margin="12,0,0,0" /> <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}" /> Foreground="{StaticResource TextFaint}" />
</Grid> </Grid>
</Button> </Button>
@@ -99,33 +199,35 @@
<Grid ColumnDefinitions="Auto,*,Auto"> <Grid ColumnDefinitions="Auto,*,Auto">
<Border Grid.Column="0" Classes="rowmark catmark" /> <Border Grid.Column="0" Classes="rowmark catmark" />
<TextBlock Grid.Column="1" Text="BUCKETS" Margin="12,0,0,0" /> <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}" /> Foreground="{StaticResource TextFaint}" />
</Grid> </Grid>
</Button> </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 Still not a selector. Every list on this screen now spans every vault this session holds a
for, and each row names its own vault — so there is nothing to switch to. What the picker below key for, and each row names its own vault — so there is nothing to switch to. What the
chooses is where a *new* item is filed, which is a different question and the only one that has picker below chooses is where a *new* item is filed, which is a different question and the
an answer worth asking for. 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" /> <Ellipse Width="6" Height="6" Fill="{StaticResource Accent}" VerticalAlignment="Center" />
<TextBlock Classes="mono" Text="{Binding HostsHeading}" FontSize="11" <TextBlock Classes="mono" Text="{Binding HostsHeading}" FontSize="11"
Foreground="{StaticResource Text}" VerticalAlignment="Center" /> Foreground="{StaticResource Text}" VerticalAlignment="Center" />
</StackPanel> </StackPanel>
<!-- <!--
Hidden at one vault, which is where most people stay. A control offering a single option is a Hidden at one vault, which is where most people stay. A control offering a single option is
question with no answer. 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}"> <StackPanel Margin="12,14,12,0" Spacing="4" IsVisible="{Binding HasVaultChoice}">
<TextBlock Classes="label" Text="NEW ITEMS GO TO" /> <TextBlock Classes="label" Text="NEW ITEMS FILE TO" FontSize="10" />
<ComboBox ItemsSource="{Binding TargetVaults}" <ComboBox ItemsSource="{Binding TargetVaults}"
SelectedItem="{Binding SelectedTargetVault}" SelectedItem="{Binding SelectedTargetVault}"
HorizontalAlignment="Stretch"> HorizontalAlignment="Stretch">
@@ -140,11 +242,11 @@
</StackPanel> </StackPanel>
<!-- <!--
Items that would not decrypt. Shown here rather than only in the status line because this is the Items that would not decrypt. Shown here rather than only in the status line because this is
screen the number is about, and because a non-zero count after a rekey is the signal that new the screen the number is about, and because a non-zero count after a rekey is the signal that
grants are needed rather than a transient. 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}"> IsVisible="{Binding HasUnreadableItems}">
<TextBlock Text="{Binding UnreadableSummary}" /> <TextBlock Text="{Binding UnreadableSummary}" />
</Border> </Border>
@@ -153,99 +255,67 @@
</ScrollViewer> </ScrollViewer>
</Border> </Border>
<!-- The table --> <!-- ============ The table ============ -->
<Grid Grid.Column="1" RowDefinitions="Auto,Auto,*"> <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"> BorderBrush="{StaticResource Border}" BorderThickness="0,0,0,1">
<!-- <Grid ColumnDefinitions="*,Auto" VerticalAlignment="Center">
◆ THE SUMMARY IS THE COLUMN THAT GIVES WAY, and that is what stops this header overflowing again. <TextBlock Grid.Column="0" Classes="mono" Text="{Binding SectionSummary}" FontSize="10.5"
Foreground="{StaticResource TextFaint}" Margin="0,0,10,0" VerticalAlignment="Center"
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"
TextTrimming="CharacterEllipsis" /> TextTrimming="CharacterEllipsis" />
<StackPanel Grid.Column="2" Orientation="Horizontal" Spacing="6"> <TextBox Grid.Column="1" x:Name="ItemFilterBox" Text="{Binding ItemFilter}" Width="240"
<!-- Height="30" CornerRadius="9" PlaceholderText="filter items"
Always offered. Every category left on this screen is one things can be added to — the one VerticalAlignment="Center" />
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>
</Grid> </Grid>
</Border> </Border>
<!-- <!--
The design's columns are NAME / TYPE / FINGERPRINT / SCOPE / ACCESS / LAST. Three of those six have The design's columns are NAME / TYPE / VAULT / USED BY / MODIFIED. MODIFIED is not here — no
nothing behind them: there is one scope, no roles, and no item carries a last-used or modified time — item this application stores carries a timestamp; see the file-level remark above and
VaultItem is (id, secret, version, three sync flags) and nothing else. What replaces them is the one docs/design-import-gaps.md.
thing this build does know and the design had no column for: whether a change is still sitting in
this machine's outbox.
--> -->
<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}"> IsVisible="{Binding HasVaultItems}">
<TextBlock Grid.Column="1" Classes="label" Text="NAME" FontSize="9.5" LetterSpacing="1" <TextBlock Grid.Column="1" Classes="label" Text="NAME" FontSize="9.5" LetterSpacing="1"
Margin="12,0,8,0" /> Margin="12,0,8,0" />
<TextBlock Grid.Column="2" Classes="label" Text="TYPE" FontSize="9.5" LetterSpacing="1" /> <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="3" Classes="label" Text="VAULT" FontSize="9.5" LetterSpacing="1" />
<TextBlock Grid.Column="4" Classes="label" Text="STATE" FontSize="9.5" LetterSpacing="1" /> <TextBlock Grid.Column="4" Classes="label" Text="USED BY" FontSize="9.5" LetterSpacing="1" />
</Grid> </Grid>
<ListBox Grid.Row="2" x:Name="ItemList" Focusable="True" <ListBox Grid.Row="2" x:Name="ItemList" Classes="filerows" Focusable="True"
ItemsSource="{Binding VaultItems}" ItemsSource="{Binding VaultItems}"
SelectedItem="{Binding SelectedVaultItem}"> SelectedItem="{Binding SelectedVaultItem}"
Margin="8,0,8,10">
<ListBox.ItemTemplate> <ListBox.ItemTemplate>
<DataTemplate x:DataType="vm:VaultItemRowViewModel"> <DataTemplate x:DataType="vm:VaultItemRowViewModel">
<Grid ColumnDefinitions="2,1.3*,74,*,88" Margin="0,7,14,7"> <Grid ColumnDefinitions="2,2.2*,1*,1*,1.4*" Height="40" Margin="6,0,10,0">
<Border Grid.Column="0" Classes="rowmark" /> <StackPanel Grid.Column="1" Orientation="Horizontal" Spacing="10" Margin="10,0,8,0"
<TextBlock Grid.Column="1" Classes="mono" Text="{Binding Name}" FontSize="12" VerticalAlignment="Center">
FontWeight="Medium" Foreground="{StaticResource Text}" Margin="12,0,8,0" <TextBlock FontFamily="{StaticResource IconFont}" FontSize="15"
TextTrimming="CharacterEllipsis" /> Text="{Binding IconGlyph}" Foreground="{StaticResource AccentText}"
<TextBlock Grid.Column="2" Classes="mono" Text="{Binding Type}" FontSize="10" VerticalAlignment="Center" />
Foreground="{StaticResource TextDim}" VerticalAlignment="Center" /> <TextBlock Classes="mono" Text="{Binding Name}" FontSize="12.5" FontWeight="Medium"
<TextBlock Grid.Column="3" Classes="mono" Text="{Binding Detail}" FontSize="10.5" Foreground="{StaticResource Text}" TextTrimming="CharacterEllipsis" />
Foreground="{StaticResource TextFaint}" Margin="0,0,8,0" </StackPanel>
TextTrimming="CharacterEllipsis" VerticalAlignment="Center" /> <TextBlock Grid.Column="2" Classes="mono" Text="{Binding Type}" FontSize="10.5"
<Border Grid.Column="4" Classes="chip warn" HorizontalAlignment="Left" Foreground="{StaticResource TextGhost}" VerticalAlignment="Center" />
VerticalAlignment="Center" IsVisible="{Binding HasBadge}"> <Border Grid.Column="3" Classes="chip" HorizontalAlignment="Left"
<TextBlock Text="{Binding Badge}" FontSize="9.5" /> VerticalAlignment="Center" IsVisible="{Binding HasVaultBadge}">
<TextBlock Text="{Binding VaultBadge}" FontSize="10" />
</Border> </Border>
<TextBlock Grid.Column="4" Classes="mono" Text="{Binding UsedBySummary}" FontSize="10.5"
Foreground="{StaticResource TextFaint}" VerticalAlignment="Center"
TextTrimming="CharacterEllipsis" IsVisible="{Binding HasUsedBySummary}" />
</Grid> </Grid>
</DataTemplate> </DataTemplate>
</ListBox.ItemTemplate> </ListBox.ItemTemplate>
</ListBox> </ListBox>
<!-- <!--
The empty state says which category is empty and what to do about it, rather than showing an empty The empty state says which category is empty and what to do about it, rather than showing an
grid that reads as a list still loading. empty grid that reads as a list still loading.
--> -->
<TextBlock Grid.Row="2" Classes="hint" Text="{Binding EmptySectionMessage}" FontSize="12" <TextBlock Grid.Row="2" Classes="hint" Text="{Binding EmptySectionMessage}" FontSize="12"
Margin="24" HorizontalAlignment="Center" VerticalAlignment="Center" Margin="24" HorizontalAlignment="Center" VerticalAlignment="Center"
@@ -254,54 +324,107 @@
</Grid> </Grid>
<!-- The detail pane, and the editors --> <!-- ============ The detail pane, and the editors ============ -->
<Border Grid.Column="2" Background="{StaticResource Sidebar}" <Border Grid.Column="2" Background="{StaticResource Sidebar}"
BorderBrush="{StaticResource Border}" BorderThickness="1,0,0,0"> BorderBrush="{StaticResource Border}" BorderThickness="1,0,0,0">
<ScrollViewer> <ScrollViewer>
<StackPanel Margin="14,16"> <StackPanel Margin="18,20" Spacing="14">
<!-- Nothing selected. --> <!-- Nothing selected. -->
<TextBlock Classes="hint" FontSize="12" <TextBlock Classes="hint" FontSize="12"
Text="Choose something on the left to see what is known about it." Text="Choose something on the left to see what is known about it."
IsVisible="{Binding !HasSelectedVaultItem}" /> IsVisible="{Binding !HasSelectedVaultItem}" />
<StackPanel Spacing="6" IsVisible="{Binding HasSelectedVaultItem}"> <StackPanel Spacing="14" IsVisible="{Binding HasSelectedVaultItem}">
<TextBlock Classes="mono" Text="{Binding SelectedVaultItem.Name}" FontSize="13"
FontWeight="SemiBold" Foreground="{StaticResource Text}" TextWrapping="Wrap" /> <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"> <StackPanel Orientation="Horizontal" Spacing="6">
<Border Classes="chip"> <Border Classes="chip">
<TextBlock Text="{Binding SelectedVaultItem.Type}" /> <TextBlock Text="{Binding SelectedVaultItem.Type}" />
</Border> </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> </Border>
</StackPanel> </StackPanel>
<TextBlock Classes="label" Text="{Binding SelectedDetailHeading}" Margin="0,12,0,4" /> <!--
<Border Background="{StaticResource Raised}" BorderBrush="{StaticResource Border}" PUBLIC KEY, when the selected key has one stored — never the private half. FINGERPRINT is
BorderThickness="1" CornerRadius="4" Padding="8"> not drawn: this codebase has never computed one, and computing it here would mean parsing
<SelectableTextBlock Classes="mono" Text="{Binding SelectedVaultItem.Detail}" armour the type stores verbatim; see docs/design-import-gaps.md's own recorded gap.
FontSize="10.5" Foreground="{StaticResource TextDim}" -->
TextWrapping="Wrap" /> <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> </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 USED BY: real host rows, off the same scan the "in use" chip above reads, with each host's
button — has nothing behind it. Items carry no author, no timestamps and no sharing, and own two-state dot — never a third colour, since nothing here pings anything.
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.
--> -->
<TextBlock Classes="hint" FontSize="10.5" Margin="0,12,0,0" <StackPanel Spacing="6" IsVisible="{Binding HasSelectedItemUsedByHosts}">
Text="Keychain items record no author, no timestamps and no sharing yet, so there is nothing more to show here." /> <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}" /> <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 Only where there is somewhere to move to, unlike EDIT beside it, which is the same rule
host's MOVE follows on the phone: a button that answers with "this is the only vault you can the host's MOVE follows on the phone: a button that answers with "this is the only vault
write to" is a button that should not have been drawn. Keys and passwords only — a tag and a you can write to" is a button that should not have been drawn. Keys and passwords only —
bucket are read from the active vault alone, so "another vault" is not a question they have. 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}" <Button Classes="ghost" Content="MOVE" Command="{Binding MoveSelectedItemCommand}"
IsVisible="{Binding CanMoveSelectedItem}" IsVisible="{Binding CanMoveSelectedItem}"
@@ -310,17 +433,23 @@
</StackPanel> </StackPanel>
<!-- <!--
◆ MOVING THE ITEM TO ANOTHER VAULT, in the place those buttons were. The host's panel, over The public half only, and there is no button for the other one. Installing a key means
here — see HostDrawer.axaml — and what it is for is the thing a shared vault could not do until pasting this line into a host's authorized_keys; a private key on the clipboard is a
now: a key typed into a personal vault before the team existed was stuck there, and the only private key in every application on the machine.
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.
--> -->
<StackPanel Spacing="8" Margin="0,14,0,0" IsVisible="{Binding IsMovingItem}"> <Button Classes="ghost" Content="Copy public key" HorizontalAlignment="Left"
<TextBlock Classes="label" Text="MOVE TO VAULT" /> 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}" <ComboBox HorizontalAlignment="Stretch" ItemsSource="{Binding MoveItemVaultChoices}"
SelectedItem="{Binding SelectedMoveItemVault}"> SelectedItem="{Binding SelectedMoveItemVault}">
<ComboBox.ItemTemplate> <ComboBox.ItemTemplate>
@@ -342,24 +471,11 @@
</StackPanel> </StackPanel>
<!-- <!--
The public half only, and there is no button for the other one. Installing a key means pasting The question DELETE asks, in the place the buttons above were. Here rather than over the
this line into a host's authorized_keys; a private key on the clipboard is a private key in screen, because this pane is where the item being deleted is described.
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.
--> -->
<Border Background="{StaticResource DangerWash}" BorderBrush="{StaticResource DangerSoft}" <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}"> IsVisible="{Binding IsConfirmingDeletion}">
<views:ConfirmDeleteCard /> <views:ConfirmDeleteCard />
</Border> </Border>
@@ -367,19 +483,13 @@
</StackPanel> </StackPanel>
<!-- <!--
Making a key, as opposed to pasting one in. A step of its own and a short one: an algorithm, a Making a key, as opposed to pasting one in. A step of its own and a short one: an algorithm,
comment, and a button. What it produces lands in the editor below, unsaved — so there is still a comment, and a button.
exactly one thing on this screen that writes a key, and it is still SAVE.
--> -->
<StackPanel Spacing="6" IsVisible="{Binding IsGeneratingKey}"> <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"> <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" <Button Classes="flat choice" Content="ED25519"
Classes.active="{Binding GeneratesEd25519}" Classes.active="{Binding GeneratesEd25519}"
Command="{Binding ChooseKeyAlgorithmCommand}" Command="{Binding ChooseKeyAlgorithmCommand}"
@@ -396,11 +506,6 @@
<TextBlock Classes="hint" FontSize="10.5" TextWrapping="Wrap" <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." /> 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" <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." /> 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. --> <!-- The key editor. -->
<StackPanel Spacing="6" IsVisible="{Binding IsEditingKey}"> <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" /> <TextBox Text="{Binding KeyEditorLabel}" PlaceholderText="name" />
<!-- <!--
Not a password box. The armour has to be visible to be pasted and checked — a masked 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 multi-line box makes "did the whole key arrive?" unanswerable — and the mistake this
prevents is pasting the .pub file, which SshKeySecret.TryValidate rejects by name. actually prevents is pasting the .pub file, which SshKeySecret.TryValidate rejects by name.
--> -->
<TextBox Text="{Binding KeyEditorPrivateKey}" <TextBox Text="{Binding KeyEditorPrivateKey}"
PlaceholderText="-----BEGIN OPENSSH PRIVATE KEY-----" PlaceholderText="-----BEGIN OPENSSH PRIVATE KEY-----"
@@ -439,19 +544,19 @@
<!-- The password editor. --> <!-- The password editor. -->
<StackPanel Spacing="6" IsVisible="{Binding IsEditingCredential}"> <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" /> <TextBox Text="{Binding CredentialEditorLabel}" PlaceholderText="name" />
<!-- <!--
Optional, and the reason a credential is worth being its own item rather than two more fields on Optional, and the reason a credential is worth being its own item rather than two more
a host: one account on twenty machines is described once and rotated once. Left blank, each host fields on a host: one account on twenty machines is described once and rotated once. Left
supplies its own username and only the password is shared. blank, each host supplies its own username and only the password is shared.
--> -->
<TextBox Text="{Binding CredentialEditorUsername}" <TextBox Text="{Binding CredentialEditorUsername}"
PlaceholderText="username (blank: use each host's own)" /> 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 Masked, unlike the private key box, and the difference is not inconsistency. A key's armour
to be visible to be checked for truncation after a paste; a password is short, usually typed, has to be visible to be checked for truncation after a paste; a password is short, usually
and shoulder-surfing is the likelier problem. typed, and shoulder-surfing is the likelier problem.
--> -->
<TextBox Text="{Binding CredentialEditorPassword}" PlaceholderText="password" PasswordChar="•" /> <TextBox Text="{Binding CredentialEditorPassword}" PlaceholderText="password" PasswordChar="•" />
<TextBox Text="{Binding CredentialEditorNotes}" PlaceholderText="notes" AcceptsReturn="True" <TextBox Text="{Binding CredentialEditorNotes}" PlaceholderText="notes" AcceptsReturn="True"
@@ -465,12 +570,12 @@
</StackPanel> </StackPanel>
<!-- <!--
The tag editor, and the whole of it is one box. What it does not have is the point: renaming a The tag editor, and the whole of it is one box. What it does not have is the point: renaming
tag touches no host, because every host wearing it names its id. That is the entire reason a tag a tag touches no host, because every host wearing it names its id. That is the entire reason
is an item rather than a string repeated inside twenty payloads. a tag is an item rather than a string repeated inside twenty payloads.
--> -->
<StackPanel Spacing="6" IsVisible="{Binding IsEditingTag}"> <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 Text="{Binding TagEditorLabel}" PlaceholderText="name">
<TextBox.KeyBindings> <TextBox.KeyBindings>
<KeyBinding Gesture="Enter" Command="{Binding SaveTagCommand}" /> <KeyBinding Gesture="Enter" Command="{Binding SaveTagCommand}" />
@@ -486,20 +591,21 @@
<!-- The bucket editor. --> <!-- The bucket editor. -->
<StackPanel Spacing="6" IsVisible="{Binding IsEditingObjectStore}"> <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 BucketEditorLabel}" PlaceholderText="name" />
<TextBox Text="{Binding BucketEditorBucket}" PlaceholderText="bucket" /> <TextBox Text="{Binding BucketEditorBucket}" PlaceholderText="bucket" />
<TextBox Text="{Binding BucketEditorAccessKeyId}" PlaceholderText="access key id" /> <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 Masked, like a password and for the same reason: a secret access key is one. The access
beside it is an identifier and is shown, which is also why the two are separate boxes. 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" <TextBox Text="{Binding BucketEditorSecretAccessKey}" PlaceholderText="secret access key"
PasswordChar="•" /> PasswordChar="•" />
<TextBox Text="{Binding BucketEditorRegion}" PlaceholderText="region (e.g. eu-west-1)" /> <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 Blank means Amazon, and then the region resolves the host. Anything else is a full URL,
is what makes this work against a self-hosted service. which is what makes this work against a self-hosted service.
--> -->
<TextBox Text="{Binding BucketEditorEndpoint}" <TextBox Text="{Binding BucketEditorEndpoint}"
PlaceholderText="endpoint (blank: Amazon S3)" /> PlaceholderText="endpoint (blank: Amazon S3)" />
@@ -507,7 +613,8 @@
Content="Address the bucket as a path" /> Content="Address the bucket as a path" />
<!-- <!--
Said where the decision is made. Getting this wrong produces a DNS failure whose message 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" <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." /> 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> </Border>
</Grid> </Grid>
</Border>
</Grid>
</UserControl> </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 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 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. 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" <Grid RowDefinitions="Auto,*" Margin="26">
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" />
<Button Grid.Column="1" Classes="flat cat" Content="CONNECTIONS" <!-- ============ THE HEADER ============ -->
Classes.active="{Binding ShowsConnections}" <Grid Grid.Row="0" Margin="0,0,0,20" ColumnDefinitions="Auto,Auto,*,Auto,Auto">
Command="{Binding ShowSectionCommand}"
<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}" /> CommandParameter="{x:Static vm:LogSection.Connections}" />
<Button Grid.Column="2" Classes="flat cat" Content="KEYCHAIN" <Button Grid.Column="1" Classes="navseg" Classes.active="{Binding ShowsActivity}"
Classes.active="{Binding ShowsActivity}" Content="KEYCHAIN" Command="{Binding ShowSectionCommand}"
Command="{Binding ShowSectionCommand}"
CommandParameter="{x:Static vm:LogSection.Activity}" /> 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> </Grid>
</Border> </Border>
<!-- ============ Connections ============ --> <TextBlock Grid.Column="3" Classes="mono" Text="{Binding HeaderStatusLine}" FontSize="10.5"
<Grid Grid.Row="1" RowDefinitions="Auto,*" IsVisible="{Binding ShowsConnections}"> 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}"> IsVisible="{Binding HasConnections}">
<TextBlock Grid.Column="0" Classes="label" Text="HOST" FontSize="9.5" LetterSpacing="1" /> <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" /> <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" /> <TextBlock Grid.Column="5" Classes="label" Text="FROM" FontSize="9.5" LetterSpacing="1" />
</Grid> </Grid>
<ListBox Grid.Row="1" x:Name="ConnectionList" Focusable="True" <ListBox Grid.Row="1" x:Name="ConnectionList" Classes="logrows" Focusable="True"
ItemsSource="{Binding Connections}"> ItemsSource="{Binding Connections}" Margin="12,0,12,10">
<ListBox.ItemTemplate> <ListBox.ItemTemplate>
<DataTemplate x:DataType="vm:ConnectionLogRowViewModel"> <DataTemplate x:DataType="vm:ConnectionLogRowViewModel">
<Grid ColumnDefinitions="1.2*,1.6*,88,72,90,*" Margin="0,6,14,6"> <Grid ColumnDefinitions="1.2*,1.6*,88,72,90,*" Height="40" Margin="8,0,10,0">
<StackPanel Grid.Column="0" Orientation="Horizontal" Spacing="6" Margin="14,0,8,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" /> <Ellipse Classes="dot" Classes.live="{Binding IsLive}" VerticalAlignment="Center" />
<TextBlock Classes="mono" Text="{Binding HostLabel}" FontSize="12" FontWeight="Medium" <TextBlock Classes="mono" Text="{Binding HostLabel}" FontSize="12.5" FontWeight="Medium"
Foreground="{StaticResource Text}" TextTrimming="CharacterEllipsis" Foreground="{StaticResource Text}" TextTrimming="CharacterEllipsis" />
VerticalAlignment="Center" />
</StackPanel> </StackPanel>
<TextBlock Grid.Column="1" Classes="mono" Text="{Binding Address}" FontSize="10.5" <TextBlock Grid.Column="1" Classes="mono" Text="{Binding Address}" FontSize="11"
Foreground="{StaticResource TextDim}" Margin="0,0,8,0" Foreground="{StaticResource TextGhost}" Margin="0,0,8,0"
TextTrimming="CharacterEllipsis" VerticalAlignment="Center" /> TextTrimming="CharacterEllipsis" VerticalAlignment="Center" />
<StackPanel Grid.Column="2" Orientation="Horizontal" Spacing="6" VerticalAlignment="Center"> <TextBlock Grid.Column="2" Classes="mono" Text="{Binding Duration}" FontSize="11"
<TextBlock Classes="mono" Text="{Binding Duration}" FontSize="10.5" Foreground="{StaticResource TextGhost}" VerticalAlignment="Center" />
Foreground="{StaticResource TextDim}" /> <TextBlock Grid.Column="3" Classes="mono" Text="{Binding Kind}" FontSize="10.5"
</StackPanel>
<TextBlock Grid.Column="3" Classes="mono" Text="{Binding Kind}" FontSize="10"
Foreground="{StaticResource TextFaint}" VerticalAlignment="Center" /> 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" /> Foreground="{StaticResource TextFaint}" VerticalAlignment="Center" />
<StackPanel Grid.Column="5" Orientation="Horizontal" Spacing="6" VerticalAlignment="Center"> <StackPanel Grid.Column="5" Orientation="Horizontal" Spacing="8" VerticalAlignment="Center">
<TextBlock Classes="mono" Text="{Binding DeviceName}" FontSize="10" <TextBlock Classes="mono" Text="{Binding DeviceName}" FontSize="10.5"
Foreground="{StaticResource TextFaint}" Foreground="{StaticResource TextFaint}"
TextTrimming="CharacterEllipsis" /> TextTrimming="CharacterEllipsis" />
<!-- <!--
Only when there is something to say. A connection that opened and closed says nothing 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. 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" /> <TextBlock Text="{Binding Outcome}" FontSize="9.5" />
</Border> </Border>
</StackPanel> </StackPanel>
@@ -103,12 +176,27 @@
Margin="24" HorizontalAlignment="Center" VerticalAlignment="Center" Margin="24" HorizontalAlignment="Center" VerticalAlignment="Center"
TextAlignment="Center" MaxWidth="420" TextAlignment="Center" MaxWidth="420"
IsVisible="{Binding !HasConnections}" /> 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> </Grid>
</Border>
<!-- ============ Keychain changes ============ --> <!-- ============ 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}"> IsVisible="{Binding HasActivity}">
<TextBlock Grid.Column="0" Classes="label" Text="ITEM" FontSize="9.5" LetterSpacing="1" /> <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" /> <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" /> <TextBlock Grid.Column="4" Classes="label" Text="WHEN" FontSize="9.5" LetterSpacing="1" />
</Grid> </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> <ListBox.ItemTemplate>
<DataTemplate x:DataType="vm:ActivityLogRowViewModel"> <DataTemplate x:DataType="vm:ActivityLogRowViewModel">
<Grid ColumnDefinitions="1.2*,90,96,*,90" Margin="14,6,14,6"> <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" <TextBlock Grid.Column="0" Classes="mono" Text="{Binding ItemLabel}" FontSize="12.5"
FontWeight="Medium" Foreground="{StaticResource Text}" Margin="0,0,8,0" FontWeight="Medium" Foreground="{StaticResource Text}" Margin="4,0,8,0"
TextTrimming="CharacterEllipsis" VerticalAlignment="Center" /> TextTrimming="CharacterEllipsis" VerticalAlignment="Center" />
<TextBlock Grid.Column="1" Classes="mono" Text="{Binding ItemKind}" FontSize="10" <TextBlock Grid.Column="1" Classes="mono" Text="{Binding ItemKind}" FontSize="10.5"
Foreground="{StaticResource TextFaint}" VerticalAlignment="Center" /> Foreground="{StaticResource TextGhost}" VerticalAlignment="Center" />
<TextBlock Grid.Column="2" Classes="mono" Text="{Binding Operation}" FontSize="10.5" <Border Grid.Column="2" Classes="opchip" Classes.created="{Binding IsCreated}"
Foreground="{StaticResource TextDim}" VerticalAlignment="Center" /> Classes.deleted="{Binding IsDeleted}" Height="19" CornerRadius="5" Padding="8,0"
<!-- HorizontalAlignment="Left" VerticalAlignment="Center">
The names of the fields that changed, and never what they changed to. A log that recorded <TextBlock Text="{Binding Operation}" FontSize="10" FontWeight="SemiBold" />
an old password would be a plaintext credential store with a vault drawn around it. </Border>
-->
<TextBlock Grid.Column="3" Classes="mono" Text="{Binding ChangedFields}" FontSize="10.5" <TextBlock Grid.Column="3" Classes="mono" Text="{Binding ChangedFields}" FontSize="10.5"
Foreground="{StaticResource TextFaint}" Margin="0,0,8,0" Foreground="{StaticResource TextFaint}" Margin="0,0,8,0"
TextTrimming="CharacterEllipsis" VerticalAlignment="Center" TextTrimming="CharacterEllipsis" VerticalAlignment="Center"
IsVisible="{Binding HasChangedFields}" /> 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" /> Foreground="{StaticResource TextFaint}" VerticalAlignment="Center" />
</Grid> </Grid>
</DataTemplate> </DataTemplate>
@@ -147,7 +235,20 @@
Margin="24" HorizontalAlignment="Center" VerticalAlignment="Center" Margin="24" HorizontalAlignment="Center" VerticalAlignment="Center"
TextAlignment="Center" MaxWidth="420" TextAlignment="Center" MaxWidth="420"
IsVisible="{Binding !HasActivity}" /> 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> </Grid>
</Border>
</Grid> </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 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 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. The two buttons at the bottom right of the detail pane are the whole safety design, and their wording is
A terminal is one input stream with no notion of being at a prompt — the remote may be inside vi, or at load-bearing. A terminal is one input stream with no notion of being at a prompt — the remote may be
a sudo password prompt with echo off — so this application cannot say "run this command", only "type inside vi, or at a sudo password prompt with echo off — so this application cannot say "run this
this into whatever is there". RUN appears solely for a snippet whose own flag says it runs, which makes command", only "type this into whatever is there". RUN appears solely for a snippet whose own flag says
that a decision taken once while writing it rather than a button beside every one of them. 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"> <UserControl.Styles>
<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" />
<!-- <!--
The command is searched as well as the name: half of what anybody remembers about a saved A card, not a full-bleed row: Track fill on hover, Track fill plus an accent ring on the selected one,
command is a word that was inside it. 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" <Style Selector="ListBox.snipcards > ListBoxItem /template/ ContentPresenter#PART_ContentPresenter">
PlaceholderText="filter by name or command" VerticalAlignment="Center" /> <Setter Property="CornerRadius" Value="10" />
</Grid> <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> </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}" ItemsSource="{Binding Visible}"
SelectedItem="{Binding Selected}"> SelectedItem="{Binding Selected}"
Margin="10,10,10,4">
<ListBox.ItemTemplate> <ListBox.ItemTemplate>
<DataTemplate x:DataType="vm:SnippetRowViewModel"> <DataTemplate x:DataType="vm:SnippetRowViewModel">
<Grid ColumnDefinitions="2,*" Margin="0,7,14,7"> <StackPanel Margin="6,7" Spacing="6">
<Border Grid.Column="0" Classes="rowmark" /> <StackPanel Orientation="Horizontal" Spacing="8">
<StackPanel Grid.Column="1" Margin="12,0,0,0" Spacing="2"> <TextBlock Classes="mono" Text="{}{ }" FontSize="11" FontWeight="Bold"
<StackPanel Orientation="Horizontal" Spacing="6"> Foreground="{StaticResource AccentText}" VerticalAlignment="Center" />
<TextBlock Classes="mono" Text="{Binding Label}" FontSize="12" FontWeight="Medium" <TextBlock Classes="mono" Text="{Binding Label}" FontSize="13" FontWeight="Medium"
Foreground="{StaticResource Text}" TextTrimming="CharacterEllipsis" /> Foreground="{StaticResource Text}" VerticalAlignment="Center"
TextTrimming="CharacterEllipsis" />
<!-- <!--
The flag, where the decision is made. A snippet that presses Enter for you is not the 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 same kind of thing as one that does not, and the list is where somebody chooses between
them. 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" /> <TextBlock Text="runs immediately" FontSize="9.5" />
</Border> </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}}"> IsVisible="{Binding Badge, Converter={x:Static StringConverters.IsNotNullOrEmpty}}">
<TextBlock Text="{Binding Badge}" FontSize="9.5" /> <TextBlock Text="{Binding Badge}" FontSize="9.5" />
</Border> </Border>
@@ -68,7 +146,8 @@
snippet is a command the rest of a team can read and insert; the row is where somebody 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. 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" /> <TextBlock Text="{Binding VaultBadge}" FontSize="9.5" />
</Border> </Border>
</StackPanel> </StackPanel>
@@ -76,37 +155,35 @@
Newlines shown as ⏎ rather than dropped. A three-line snippet flattened into one run of 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. 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" /> Foreground="{StaticResource TextFaint}" TextTrimming="CharacterEllipsis" />
</StackPanel> </StackPanel>
</Grid>
</DataTemplate> </DataTemplate>
</ListBox.ItemTemplate> </ListBox.ItemTemplate>
</ListBox> </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" Margin="24" HorizontalAlignment="Center" VerticalAlignment="Center"
TextAlignment="Center" MaxWidth="360" TextAlignment="Center" MaxWidth="360"
IsVisible="{Binding !HasVisible}" /> 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"> BorderThickness="0,1,0,0">
<StackPanel Orientation="Horizontal" Spacing="6"> <StackPanel Orientation="Horizontal" Spacing="8">
<Button Classes="ghost" Content="+ NEW SNIPPET" Command="{Binding NewCommand}" /> <Button Classes="ghost" Content="Edit" Command="{Binding EditCommand}"
<Button Classes="ghost" Content="EDIT" Command="{Binding EditCommand}"
IsEnabled="{Binding HasSelection}" />
<Button Classes="ghost" Content="DELETE" Command="{Binding DeleteCommand}"
IsEnabled="{Binding HasSelection}" /> IsEnabled="{Binding HasSelection}" />
<!-- <!--
Sharing a snippet, which is what moving one into a team's vault is. Beside EDIT rather than 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 inside it: the two vaults are encrypted under different keys, so this is a re-seal into one
a tombstone in the other — nothing a SAVE could do — and a picker inside the form would let and a tombstone in the other — nothing a SAVE could do — and a picker inside the form would
somebody correcting a typo hand a command to a team by leaving it where they found it. The let somebody correcting a typo hand a command to a team by leaving it where they found it.
picker itself opens beside the snippet, in the pane on the right. 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}" 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." /> 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> </StackPanel>
</Border> </Border>
@@ -115,20 +192,22 @@
<Border Grid.Column="1" Background="{StaticResource Sidebar}" <Border Grid.Column="1" Background="{StaticResource Sidebar}"
BorderBrush="{StaticResource Border}" BorderThickness="1,0,0,0"> BorderBrush="{StaticResource Border}" BorderThickness="1,0,0,0">
<ScrollViewer> <ScrollViewer>
<StackPanel Margin="14,16" Spacing="8"> <StackPanel Margin="18,20" Spacing="10">
<!-- ============ The editor ============ --> <!-- ============ The editor ============ -->
<StackPanel Spacing="6" IsVisible="{Binding IsEditing}"> <StackPanel Spacing="8" IsVisible="{Binding IsEditing}">
<TextBox Text="{Binding EditorLabel}" PlaceholderText="name" /> <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 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 than through a standing preference elsewhere. Not drawn for an existing snippet — its vault
not a field of this form, and changing it is MOVE — and not drawn at all where there is only is not a field of this form, and changing it is MOVE — and not drawn at all where there is
one vault to choose between, because a control offering one option is a question nobody asked. only one vault to choose between, because a control offering one option is a question
nobody asked.
--> -->
<StackPanel Spacing="4" IsVisible="{Binding ShowsEditorVaultChoice}"> <StackPanel Spacing="4" IsVisible="{Binding ShowsEditorVaultChoice}">
<TextBlock Classes="label" Text="VAULT" /> <TextBlock Classes="label" Text="VAULT" FontSize="10" />
<ComboBox HorizontalAlignment="Stretch" ItemsSource="{Binding EditorVaultChoices}" <ComboBox HorizontalAlignment="Stretch" ItemsSource="{Binding EditorVaultChoices}"
SelectedItem="{Binding EditorSelectedVault}"> SelectedItem="{Binding EditorSelectedVault}">
<ComboBox.ItemTemplate> <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." /> Text="A shared vault means everybody holding its key can read this command and insert it into their own terminals." />
</StackPanel> </StackPanel>
<TextBlock Classes="label" Text="COMMAND" FontSize="10" />
<!-- <!--
Stored exactly as typed — no trimming, no newline normalisation. A here-document's terminator Stored exactly as typed — no trimming, no newline normalisation. A here-document's
has to arrive on a line of its own, and tidying the trailing newline off it leaves the shell terminator has to arrive on a line of its own, and tidying the trailing newline off it
waiting for one that never comes. leaves the shell waiting for one that never comes.
--> -->
<TextBox Text="{Binding EditorCommand}" PlaceholderText="the command" AcceptsReturn="True" <TextBox Text="{Binding EditorCommand}" PlaceholderText="the command" AcceptsReturn="True"
Height="140" TextWrapping="NoWrap" FontFamily="{StaticResource MonoFont}" Height="140" TextWrapping="NoWrap" FontFamily="{StaticResource MonoFont}"
FontSize="12" /> FontSize="12" />
<TextBox Text="{Binding EditorNotes}" PlaceholderText="notes" AcceptsReturn="True" <TextBox Text="{Binding EditorNotes}" PlaceholderText="notes" AcceptsReturn="True"
Height="48" TextWrapping="Wrap" /> Height="48" TextWrapping="Wrap" />
<CheckBox IsChecked="{Binding EditorRunsOnInsert}"
Content="Press Enter after inserting this" /> <Grid ColumnDefinitions="*,Auto" Margin="0,4,0,0">
<TextBlock Classes="hint" FontSize="11" TextWrapping="Wrap" <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." /> 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"> </StackPanel>
<Button Classes="accent" Content="SAVE" Command="{Binding SaveCommand}" /> <CheckBox Grid.Column="1" IsChecked="{Binding EditorRunsOnInsert}"
<Button Classes="ghost" Content="CANCEL" Command="{Binding CancelCommand}" /> 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>
</StackPanel> </StackPanel>
<!-- ============ The selected snippet ============ --> <!-- ============ The selected snippet ============ -->
<StackPanel Spacing="6" IsVisible="{Binding !IsEditing}"> <StackPanel Spacing="10" IsVisible="{Binding !IsEditing}">
<TextBlock Classes="hint" FontSize="12" <TextBlock Classes="hint" FontSize="12"
Text="Choose a snippet to see it in full and put it into a terminal." Text="Choose a snippet to see it in full and put it into a terminal."
IsVisible="{Binding !HasSelection}" /> IsVisible="{Binding !HasSelection}" />
<StackPanel Spacing="6" IsVisible="{Binding HasSelection}"> <StackPanel Spacing="10" IsVisible="{Binding HasSelection}">
<TextBlock Classes="mono" Text="{Binding Selected.Label}" FontSize="13" <TextBlock Classes="mono" Text="{Binding Selected.Label}" FontSize="15"
FontWeight="SemiBold" Foreground="{StaticResource Text}" TextWrapping="Wrap" /> FontWeight="Bold" Foreground="{StaticResource Text}" TextWrapping="Wrap" />
<!-- <TextBlock Classes="label" Text="COMMAND" FontSize="10" />
Named here as well as on the row, because this pane is where somebody decides to put a <Border CornerRadius="10" Background="{StaticResource Pane}"
command into a production terminal, and who else holds a key to it is part of that decision. BorderBrush="{StaticResource BorderMid}" BorderThickness="1" Padding="12">
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">
<SelectableTextBlock Classes="mono" Text="{Binding Selected.Snippet.Command}" <SelectableTextBlock Classes="mono" Text="{Binding Selected.Snippet.Command}"
FontSize="10.5" Foreground="{StaticResource TextDim}" FontSize="11.5" LineHeight="19"
TextWrapping="Wrap" /> Foreground="{StaticResource TextFaint}" TextWrapping="Wrap" />
</Border> </Border>
<TextBlock Classes="mono" Text="{Binding Selected.Snippet.Notes}" FontSize="11" <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}}" /> 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 VAULT, with the design's own lock glyph. Named here as well as on the row, because this
above it is — so "INSERT" alone would leave somebody working out which of six open tabs is pane is where somebody decides to put a command into a production terminal, and who else
about to receive a command, at the moment that is worst to be wrong about. 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}"> <StackPanel Spacing="6" IsVisible="{Binding ShowsSelectionActions}">
<Button Classes="accent" Content="{Binding InsertLabel}" Margin="0,14,0,0" <Button Classes="accent" Content="{Binding InsertLabel}" HorizontalAlignment="Stretch"
HorizontalAlignment="Left" HorizontalContentAlignment="Center"
Command="{Binding InsertCommand}" IsEnabled="{Binding CanInsert}" Command="{Binding InsertCommand}" IsEnabled="{Binding CanInsert}"
ToolTip.Tip="Types the command at the prompt and stops. Nothing runs until you press Enter there." /> 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}" Command="{Binding RunCommand}"
IsVisible="{Binding SelectionRuns}" IsEnabled="{Binding CanInsert}" IsVisible="{Binding SelectionRuns}" IsEnabled="{Binding CanInsert}"
ToolTip.Tip="Types the command and presses Enter. Offered because this snippet is marked as one that runs." /> 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." /> 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> </StackPanel>
<!-- <!--
◆ SHARING THE SNIPPET, which is what moving it into a team's vault amounts to. A picker and ◆ SHARING THE SNIPPET, which is what moving it into a team's vault amounts to. A picker
two buttons, not a question with a yes: what is being asked is which vault, and a move is and two buttons, not a question with a yes.
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.
--> -->
<StackPanel Spacing="8" Margin="0,14,0,0" IsVisible="{Binding IsMoving}"> <StackPanel Spacing="8" IsVisible="{Binding IsMoving}">
<TextBlock Classes="label" Text="MOVE TO VAULT" /> <TextBlock Classes="label" Text="MOVE TO VAULT" FontSize="10" />
<ComboBox HorizontalAlignment="Stretch" ItemsSource="{Binding MoveVaultChoices}" <ComboBox HorizontalAlignment="Stretch" ItemsSource="{Binding MoveVaultChoices}"
SelectedItem="{Binding SelectedMoveVault}"> SelectedItem="{Binding SelectedMoveVault}">
<ComboBox.ItemTemplate> <ComboBox.ItemTemplate>
@@ -240,10 +329,30 @@
<TextBlock Classes="hint" FontSize="10.5" TextWrapping="Wrap" <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." /> 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"> <StackPanel Orientation="Horizontal" Spacing="6">
<Button Classes="accent" Content="MOVE" Command="{Binding ConfirmMoveCommand}" /> <Button Classes="accent" Content="Move" Command="{Binding ConfirmMoveCommand}" />
<Button Classes="ghost" Content="CANCEL" Command="{Binding CancelMoveCommand}" /> <Button Classes="ghost" Content="Cancel" Command="{Binding CancelMoveCommand}" />
</StackPanel> </StackPanel>
</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>
</StackPanel> </StackPanel>
@@ -252,5 +361,8 @@
</Border> </Border>
</Grid> </Grid>
</Border>
</Grid>
</UserControl> </UserControl>