Files
DodoSSH/src/DodoSSH.Client.App/Views/SettingsVaultsPage.axaml
T
jaap-jan 509937c4b8
ci / desktop nightly (push) Canceled after 0s
ci / api image (push) Canceled after 0s
ci / android head (push) Canceled after 4m10s
ci / build and test (push) Canceled after 4m12s
Sync: keep the remembered sign-in when the provider is merely down, and say why SYNC NOW found the machine offline
2026-10-07 11:11:09 +02:00

415 lines
27 KiB
XML

<UserControl xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:vm="using:DodoSSH.Client.Shell.ViewModels"
xmlns:contracts="using:DodoSSH.Contracts"
x:Class="DodoSSH.Client.App.Views.SettingsVaultsPage"
x:DataType="vm:MainWindowViewModel"
x:Name="Root">
<!--
v5c-2: Vaults, against Settings-Vaults.dc.html. Replaces the old full-bleed VaultsScreen, which this
settings page now covers completely — every command it had is reachable here exactly once, and
VaultsScreen.axaml is gone. Its data is DataContext.Vaults (VaultsViewModel), reached as {Binding Vaults.*}
throughout because this page's own DataContext is the shell, the same choice every other settings page
makes.
── WHAT THE DESIGN DREW AND THIS PAGE DOES NOT ─────────────────────────────────────────────────────────
"Manage devices" and "3 devices" in the sync line — no list-devices endpoint exists. The whole VAULT
DEFAULTS card (auto-lock, require-password-on-unlock, relay toggle) and the whole RECOVERY card (kit,
export) — none of the three exists; sync is always on. The magenta "Default" badge — the app does track a
"new items go to" vault (VaultViewModel.TargetVaultId), but that lives on a different view model than the
row being drawn here and cross-referencing the two per card would be more machinery than the badge is
worth; dropped rather than faked. Per-vault UNLOCKED/LOCKED chips — this app locks the keychain as a
whole, not one vault at a time, so there is no per-vault state for a chip to draw; the keychain-level fact
lives in the sync card's dot instead. The "Unlock Rocateq" modal — same reason, there is nothing per-vault
to unlock. Member avatar stacks on the card list — VaultRowViewModel carries a member *count*, not the
members themselves; only the selected vault's Members collection is actually loaded, which is why avatars
are drawn in the members panel below and nowhere else. The "Sorted by name ▾" control — decorative in the
mock; Vaults.Vaults is already ordered personal-first-then-name and there is no second order to switch to.
── THE MEMBERS PANEL ────────────────────────────────────────────────────────────────────────────────────
The design draws an 860px modal. This app's only overlay idiom below a whole-window mode is the scrim a
Border with a translucent Background and a PointerPressed handler draws — see QuickConnect.axaml, the
other place a click-away-to-close panel exists — so that is what this is, capped at MaxWidth 860 rather
than fixed to it: the settings content column is narrower than 860 at the window's minimum, and a modal
that insisted on the full width would arrange its own rows past the edge of what this page is given.
Inside it is the old screen's own right-hand column, restyled rather than reinvented: a selectable list of
members, the SET ROLE buttons, the ADD row, REMOVE, HAND OVER, SHARE KEY/WITHDRAW KEY and the KEY HOLDERS
list, on the same VaultsViewModel.SelectedMember selection the old screen used. Deleting a vault stays a
card-level action (the danger icon below), not a members-panel one, so PendingAction is drawn in only one
of the two places at a time — see the two IsVisible guards below, which key off IsMembersPanelOpen because
HAND OVER can only ever be armed with the panel open (it needs a selected member) and DELETE only with it
closed (the card's own icon is what arms it).
The sentence that must survive from the old screen: a vault cannot be deleted through the membership list
behind it — there is no such call anywhere in the server, and archiving a team while its vault still
exists is refused. That is not the same fact as "a vault cannot be deleted" — the DELETE icon below does
delete the vault itself — and both sentences are kept, each where it is true.
-->
<Panel>
<ScrollViewer>
<StackPanel MaxWidth="1100" Margin="40" HorizontalAlignment="Stretch">
<Grid ColumnDefinitions="*,Auto">
<StackPanel Grid.Column="0" Orientation="Horizontal" Spacing="14" VerticalAlignment="Center">
<TextBlock Classes="settingstitle" Text="Vaults" />
<Border Background="{StaticResource Chip}" CornerRadius="9" MinWidth="34" Height="30">
<TextBlock Text="{Binding Vaults.Vaults.Count}" FontSize="13.5" FontWeight="SemiBold"
Foreground="{StaticResource TextFaint}" Margin="8,0"
HorizontalAlignment="Center" VerticalAlignment="Center" />
</Border>
</StackPanel>
<StackPanel Grid.Column="1" Orientation="Horizontal" Spacing="12" VerticalAlignment="Center">
<Button Classes="ghost" Height="40" Content="SYNC NOW" Command="{Binding Vault.SyncCommand}" />
<Button Classes="accent" Height="40" Content="+ NEW VAULT"
Command="{Binding Vaults.NewVaultCommand}"
IsEnabled="{Binding !Vaults.IsBusy}" />
</StackPanel>
</Grid>
<!-- What SYNC NOW said. The status bar that carries it elsewhere is hidden in settings mode, so
without this the button ran, failed or found the machine offline in perfect silence. -->
<TextBlock Margin="0,10,0,0" FontSize="11.5" TextWrapping="Wrap" HorizontalAlignment="Right"
Foreground="{StaticResource TextFaint}"
IsVisible="{Binding Vault.Status, Converter={x:Static StringConverters.IsNotNullOrEmpty}, FallbackValue=False}"
Text="{Binding Vault.Status}" />
<!-- The name-a-vault and rename-a-vault forms, in place — this window has no modal idiom for a bare
text field, and the members panel below is a different kind of overlay: over one vault's people,
not over a form. -->
<Border Classes="settingscard" Margin="0,20,0,0" Padding="20"
IsVisible="{Binding Vaults.IsCreatingVault}">
<StackPanel Spacing="8">
<TextBlock Classes="settingsrowtitle" Text="New vault" />
<TextBox PlaceholderText="Name" Text="{Binding Vaults.NewVaultName}" />
<TextBlock Classes="settingsrowcaption"
Text="Its key is made on this machine and nobody else has it. Add people to it once it exists, then press SHARE KEY." />
<StackPanel Orientation="Horizontal" Spacing="8">
<Button Classes="accent" Height="36" Content="CREATE"
Command="{Binding Vaults.CreateVaultCommand}" IsEnabled="{Binding !Vaults.IsBusy}" />
<Button Classes="ghost" Height="36" Content="CANCEL"
Command="{Binding Vaults.CancelNewVaultCommand}" />
</StackPanel>
</StackPanel>
</Border>
<Border Classes="settingscard" Margin="0,20,0,0" Padding="20"
IsVisible="{Binding Vaults.IsRenamingVault}">
<StackPanel Spacing="8">
<TextBlock Classes="settingsrowtitle" Text="Rename vault" />
<TextBox PlaceholderText="Name" Text="{Binding Vaults.EditVaultName}" />
<TextBlock Classes="settingsrowcaption"
Text="Everybody who shares this vault sees the new name. Nothing is re-encrypted and no key changes — the name has always been stored in plain text, because a person has to be able to pick a vault before anything is decrypted." />
<StackPanel Orientation="Horizontal" Spacing="8">
<Button Classes="accent" Height="36" Content="SAVE"
Command="{Binding Vaults.SaveVaultNameCommand}" IsEnabled="{Binding !Vaults.IsBusy}" />
<Button Classes="ghost" Height="36" Content="CANCEL"
Command="{Binding Vaults.CancelRenameVaultCommand}" />
</StackPanel>
</StackPanel>
</Border>
<!-- A vault's own destructive confirmation. Only while the members panel is shut — see the remark
above for why the two never compete for this one PendingAction. Both conditions on the same
Border, not one nested inside the other: a Border visible with its content hidden would still
draw an empty danger-tinted card while the members panel's own copy of this confirmation is
showing. -->
<Border Classes="settingscard" Margin="0,20,0,0" Padding="20"
Background="{StaticResource DangerWash}" BorderBrush="{StaticResource DangerSoft}">
<Border.IsVisible>
<MultiBinding Converter="{x:Static BoolConverters.And}">
<Binding Path="Vaults.IsConfirming" />
<Binding Path="!Vaults.IsMembersPanelOpen" />
</MultiBinding>
</Border.IsVisible>
<StackPanel Spacing="8">
<TextBlock Text="{Binding Vaults.PendingAction.Question}" FontSize="14.5" FontWeight="Bold"
Foreground="{StaticResource Text}" TextWrapping="Wrap" />
<TextBlock Text="{Binding Vaults.PendingAction.Consequence}" FontSize="12.5"
Foreground="{StaticResource WarnText}" TextWrapping="Wrap" />
<StackPanel Orientation="Horizontal" Spacing="8">
<Button Classes="danger" Height="36" Content="CONFIRM"
Command="{Binding Vaults.ConfirmActionCommand}" IsEnabled="{Binding !Vaults.IsBusy}" />
<Button Classes="ghost" Height="36" Content="CANCEL"
Command="{Binding Vaults.CancelActionCommand}" />
</StackPanel>
</StackPanel>
</Border>
<!-- Sync status: only what is real — no device count, no "ran N minutes ago" (this session tracks no
such timestamp). The dot and the word are MainWindowViewModel.IsFullySynced/SyncLabel, the exact
fact the titlebar's own dot already tells the truth with. -->
<Border Classes="settingscard" Margin="0,20,0,0" Padding="20,18">
<StackPanel Orientation="Horizontal" Spacing="14">
<Ellipse Classes="dot" Classes.live="{Binding IsFullySynced}" Width="9" Height="9"
Margin="0,7,0,0" VerticalAlignment="Top" />
<StackPanel Spacing="6">
<TextBlock Classes="mono" Text="{Binding SyncLabel}" FontSize="13" FontWeight="Bold"
LetterSpacing="0.5" Foreground="{StaticResource Text}" />
<TextBlock Classes="settingsrowcaption" Text="End-to-end encrypted." />
</StackPanel>
</StackPanel>
</Border>
<TextBlock Classes="settingssection" Text="YOUR VAULTS" Margin="0,32,0,12" />
<StackPanel Spacing="10" Margin="0,0,0,40" IsVisible="{Binding Vaults.HasVaults}">
<ItemsControl ItemsSource="{Binding Vaults.Vaults}">
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<StackPanel Spacing="10" />
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
<ItemsControl.ItemTemplate>
<DataTemplate x:DataType="vm:VaultRowViewModel">
<Border Classes="settingscard" Padding="22,18">
<Grid ColumnDefinitions="Auto,*,Auto">
<Border Grid.Column="0" Width="52" Height="52" CornerRadius="12"
Background="{StaticResource AvatarGradient}">
<TextBlock Text="{Binding Initial}" FontWeight="Bold" FontSize="20"
Foreground="White" HorizontalAlignment="Center" VerticalAlignment="Center" />
</Border>
<StackPanel Grid.Column="1" Spacing="7" Margin="16,0" VerticalAlignment="Center">
<StackPanel Orientation="Horizontal" Spacing="8">
<TextBlock Text="{Binding Name}" FontSize="17" FontWeight="Bold"
LetterSpacing="-0.25" Foreground="{StaticResource Text}"
TextTrimming="CharacterEllipsis" />
<Border Classes="chip">
<TextBlock Text="{Binding RoleLabel}" />
</Border>
</StackPanel>
<TextBlock Classes="mono" FontSize="11.5" Foreground="{StaticResource TextFaint}"
Text="{Binding Detail}" TextWrapping="Wrap" />
<TextBlock Classes="mono" FontSize="10.5" Foreground="{StaticResource WarnText}"
Text="{Binding State}" IsVisible="{Binding HasState}" TextWrapping="Wrap" />
</StackPanel>
<StackPanel Grid.Column="2" Orientation="Horizontal" Spacing="8" VerticalAlignment="Center">
<Button Classes="paneicon" Width="34" Height="34" FontSize="15"
Command="{Binding #Root.((vm:MainWindowViewModel)DataContext).Vaults.OpenMembersPanelCommand}"
CommandParameter="{Binding}" IsVisible="{Binding IsShared}"
ToolTip.Tip="See and manage who is in this vault.">
<TextBlock FontFamily="{StaticResource IconFont}" Text="&#xE7EF;" />
</Button>
<Button Classes="paneicon" Width="34" Height="34" FontSize="15"
Command="{Binding #Root.((vm:MainWindowViewModel)DataContext).Vaults.RenameVaultRowCommand}"
CommandParameter="{Binding}" IsVisible="{Binding CanAdminister}"
ToolTip.Tip="Changes what this vault is called. The name is plaintext on the server, as it always was; nothing inside is re-encrypted.">
<TextBlock FontFamily="{StaticResource IconFont}" Text="&#xE3C9;" />
</Button>
<Button Classes="paneicon danger" Width="34" Height="34" FontSize="15"
Command="{Binding #Root.((vm:MainWindowViewModel)DataContext).Vaults.DeleteVaultRowCommand}"
CommandParameter="{Binding}"
IsVisible="{Binding !IsPersonal}"
ToolTip.Tip="Deletes this vault and withdraws everybody's key to it. It cannot reach a machine that has already synced it.">
<TextBlock FontFamily="{StaticResource IconFont}" Text="&#xE872;" />
</Button>
</StackPanel>
</Grid>
</Border>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</StackPanel>
<TextBlock Classes="settingsrowcaption" Margin="0,20,0,40" TextWrapping="Wrap"
IsVisible="{Binding !Vaults.HasVaults}"
Text="No vaults yet. Unlock your keychain to see the personal one, or make a vault to share hosts and credentials with colleagues." />
<TextBlock Margin="0,0,0,40" FontSize="11.5" TextWrapping="Wrap"
Foreground="{StaticResource TextFaint}"
IsVisible="{Binding Vaults.Status, Converter={x:Static StringConverters.IsNotNullOrEmpty}}"
Text="{Binding Vaults.Status}" />
</StackPanel>
</ScrollViewer>
<!-- ============ THE MEMBERS PANEL ============ -->
<Border x:Name="MembersBackdrop" Background="#9905050A"
IsVisible="{Binding Vaults.IsMembersPanelOpen}" PointerPressed="OnBackdropPressed">
<Border MaxWidth="860" MaxHeight="620" Margin="30"
HorizontalAlignment="Center" VerticalAlignment="Center"
Background="{StaticResource Chrome}" BorderBrush="{StaticResource BorderMid}"
BorderThickness="1" CornerRadius="14">
<Grid RowDefinitions="Auto,*" Margin="26,22">
<Grid Grid.Row="0" ColumnDefinitions="*,Auto" Margin="0,0,0,14">
<TextBlock Grid.Column="0" Text="{Binding Vaults.SelectedVault.Name, StringFormat='{}{0} · Members'}"
FontSize="18" FontWeight="Bold" LetterSpacing="-0.25"
Foreground="{StaticResource Text}" TextTrimming="CharacterEllipsis" />
<Button Grid.Column="1" Classes="paneicon" Width="26" Height="26"
Command="{Binding Vaults.CloseMembersPanelCommand}" ToolTip.Tip="Close">
<TextBlock FontFamily="{StaticResource IconFont}" Text="&#xE5CD;" FontSize="15" />
</Button>
</Grid>
<ScrollViewer Grid.Row="1">
<StackPanel Spacing="16">
<TextBlock Classes="settingsrowcaption" TextWrapping="Wrap"
IsVisible="{Binding Vaults.HasSharedMembershipWarning}"
Text="{Binding Vaults.SharedMembershipWarning}" />
<StackPanel Spacing="8" IsVisible="{Binding !Vaults.SelectedIsPersonal}">
<TextBlock Classes="settingssection" Text="MEMBERS" Margin="0" />
<ListBox ItemsSource="{Binding Vaults.Members}" SelectedItem="{Binding Vaults.SelectedMember}"
Background="Transparent" BorderThickness="0" MaxHeight="220">
<ListBox.ItemTemplate>
<DataTemplate x:DataType="vm:VaultMemberRowViewModel">
<Grid ColumnDefinitions="Auto,*,150,Auto" Margin="0,4">
<Border Grid.Column="0" Width="30" Height="30" CornerRadius="15"
Background="{StaticResource AvatarGradient}">
<TextBlock Text="{Binding Initials}" FontWeight="Bold" FontSize="10"
Foreground="White" HorizontalAlignment="Center"
VerticalAlignment="Center" />
</Border>
<StackPanel Grid.Column="1" Spacing="2" Margin="10,0" VerticalAlignment="Center">
<TextBlock Text="{Binding Name}" FontSize="13" FontWeight="Medium"
Foreground="{StaticResource Text}" TextTrimming="CharacterEllipsis" />
<TextBlock Classes="settingsrowcaption" FontSize="11" Text="{Binding Email}" />
</StackPanel>
<StackPanel Grid.Column="2" Spacing="2" VerticalAlignment="Center">
<TextBlock Classes="settingsrowcaption" FontSize="10.5" Text="{Binding KeyState}"
TextWrapping="Wrap" />
<TextBlock Classes="settingsrowcaption" FontSize="10" Text="{Binding LastActive}" />
</StackPanel>
<TextBlock Grid.Column="3" Classes="mono" Text="{Binding Role}" FontSize="10.5"
Foreground="{StaticResource TextFaint}" VerticalAlignment="Center"
Margin="10,0,0,0" />
</Grid>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
<!-- The hand-over confirmation, only while this panel is the one that armed it. -->
<Border Padding="14" CornerRadius="10" Background="{StaticResource DangerWash}"
BorderBrush="{StaticResource DangerSoft}" BorderThickness="1"
IsVisible="{Binding Vaults.IsConfirming}">
<StackPanel Spacing="8">
<TextBlock Text="{Binding Vaults.PendingAction.Question}" FontSize="13.5" FontWeight="Bold"
Foreground="{StaticResource Text}" TextWrapping="Wrap" />
<TextBlock Text="{Binding Vaults.PendingAction.Consequence}" FontSize="11.5"
Foreground="{StaticResource WarnText}" TextWrapping="Wrap" />
<StackPanel Orientation="Horizontal" Spacing="8">
<Button Classes="danger" Content="CONFIRM" Command="{Binding Vaults.ConfirmActionCommand}"
IsEnabled="{Binding !Vaults.IsBusy}" />
<Button Classes="ghost" Content="CANCEL" Command="{Binding Vaults.CancelActionCommand}" />
</StackPanel>
</StackPanel>
</Border>
<StackPanel Spacing="8" IsVisible="{Binding Vaults.CanAdministerSelected}">
<TextBlock Classes="label" Text="SET THE SELECTED MEMBER'S ROLE" />
<StackPanel Orientation="Horizontal" Spacing="6">
<Button Classes="flat choice" Content="VIEWER" Command="{Binding Vaults.ChangeRoleCommand}"
CommandParameter="{x:Static contracts:TeamMemberRole.Viewer}"
IsEnabled="{Binding !Vaults.IsBusy}" />
<Button Classes="flat choice" Content="MEMBER" Command="{Binding Vaults.ChangeRoleCommand}"
CommandParameter="{x:Static contracts:TeamMemberRole.Member}"
IsEnabled="{Binding !Vaults.IsBusy}" />
<Button Classes="flat choice" Content="ADMIN" Command="{Binding Vaults.ChangeRoleCommand}"
CommandParameter="{x:Static contracts:TeamMemberRole.Admin}"
IsEnabled="{Binding !Vaults.IsBusy}" />
</StackPanel>
<StackPanel Orientation="Horizontal" Spacing="8">
<Button Classes="ghost" Content="HAND OVER" Command="{Binding Vaults.HandOverCommand}"
IsEnabled="{Binding !Vaults.IsBusy}" IsVisible="{Binding Vaults.OwnsSelected}"
ToolTip.Tip="Hands this vault to the selected member. They become its owner and you become an admin; only the new owner can hand it on again." />
<Button Classes="danger" Content="REMOVE" Command="{Binding Vaults.RemoveMemberCommand}"
IsEnabled="{Binding !Vaults.IsBusy}"
ToolTip.Tip="Removes the selected member and withdraws every key they hold to this vault. It blocks future reads only." />
</StackPanel>
</StackPanel>
<StackPanel Spacing="8" IsVisible="{Binding Vaults.CanAdministerSelected}">
<TextBlock Classes="settingssection" Text="INVITE" Margin="0" />
<Grid ColumnDefinitions="*,140,44">
<TextBox Grid.Column="0" PlaceholderText="colleague@example.com"
Text="{Binding Vaults.NewMemberEmail}" Margin="0,0,8,0" />
<StackPanel Grid.Column="1" Orientation="Horizontal" Spacing="4">
<Button Classes="flat choice" Content="VIEWER"
Classes.active="{Binding Vaults.AddsAsViewer}"
Command="{Binding Vaults.ChooseNewMemberRoleCommand}"
CommandParameter="{x:Static contracts:TeamMemberRole.Viewer}" />
<Button Classes="flat choice" Content="MEMBER"
Classes.active="{Binding Vaults.AddsAsMember}"
Command="{Binding Vaults.ChooseNewMemberRoleCommand}"
CommandParameter="{x:Static contracts:TeamMemberRole.Member}" />
</StackPanel>
<Button Grid.Column="2" Classes="accent" Width="44" Content="+"
Command="{Binding Vaults.AddMemberCommand}" IsEnabled="{Binding !Vaults.IsBusy}"
Margin="8,0,0,0"
ToolTip.Tip="Adds the account that signs in with this address. An address with no account here is refused and says so." />
</Grid>
<TextBlock Classes="settingsrowcaption" TextWrapping="Wrap"
Text="Adding somebody lets the server serve them this vault. It does not let them read it — that needs a vault key, which SHARE KEY below wraps for them." />
</StackPanel>
</StackPanel>
<StackPanel Spacing="8" IsVisible="{Binding Vaults.SelectedIsPersonal}">
<TextBlock Classes="settingssection" Text="YOURS ALONE" Margin="0" />
<TextBlock Classes="settingsrowcaption" TextWrapping="Wrap"
Text="Nobody can be added to your personal vault, and the server refuses a key grant on one outright. Make a vault for the things you want to share, and put them in it." />
</StackPanel>
<Border Height="1" Background="{StaticResource BorderSubtle}" />
<StackPanel Spacing="8">
<StackPanel Orientation="Horizontal" Spacing="8" IsVisible="{Binding Vaults.SelectedIsShared}">
<Button Classes="accent" Content="SHARE KEY" Command="{Binding Vaults.ShareVaultCommand}"
IsEnabled="{Binding !Vaults.IsBusy}"
ToolTip.Tip="Wraps this vault's key to the selected member. Their published key is checked against the server's append-only key log first." />
<Button Classes="danger" Content="WITHDRAW KEY" Command="{Binding Vaults.RevokeVaultCommand}"
IsEnabled="{Binding !Vaults.IsBusy}"
ToolTip.Tip="Withdraws the selected member's key to this vault. Blocks future reads only." />
</StackPanel>
<TextBlock Classes="settingssection" Text="KEY HOLDERS" Margin="0" />
<ListBox ItemsSource="{Binding Vaults.Grants}" Background="Transparent" BorderThickness="0"
MaxHeight="120">
<ListBox.ItemTemplate>
<DataTemplate x:DataType="vm:VaultGrantRowViewModel">
<Grid ColumnDefinitions="10,*" Margin="0,3">
<Ellipse Grid.Column="0" Width="6" Height="6" VerticalAlignment="Center"
IsVisible="{Binding IsLive}" Fill="{StaticResource Live}" />
<StackPanel Grid.Column="1" Spacing="2" Margin="6,0,0,0">
<TextBlock Text="{Binding Name}" FontSize="13" FontWeight="Medium"
Foreground="{StaticResource Text}" TextTrimming="CharacterEllipsis" />
<TextBlock Classes="settingsrowcaption" FontSize="11" Text="{Binding State}" />
</StackPanel>
</Grid>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
<!--
Load-bearing, from the old screen: there is no DELETE here for a reason, and the reason is
not "not built yet". The membership list behind a shared vault cannot be archived while the
vault exists — that is a different fact from the vault itself, which the card's own DELETE
icon does remove.
-->
<TextBlock Classes="settingsrowcaption" TextWrapping="Wrap"
Text="A vault's membership list cannot be deleted on its own — the server refuses to archive it while the vault it carries still exists. Deleting the vault itself is the card's own icon, outside this panel." />
</StackPanel>
</StackPanel>
</ScrollViewer>
</Grid>
</Border>
</Border>
</Panel>
</UserControl>